Fully operational mopidy middleware; Spotify middleware next
This commit is contained in:
22
src/js/bootstrap.js
vendored
22
src/js/bootstrap.js
vendored
@ -1,16 +1,24 @@
|
||||
|
||||
console.info('Bootstrapping...');
|
||||
|
||||
import { createStore, applyMiddleware } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
import reducer from './reducers/index'
|
||||
import mopidyMiddleware from './services/mopidy/middleware'
|
||||
import { createStore, applyMiddleware, combineReducers } from 'redux'
|
||||
|
||||
import mopidy from './services/mopidy/reducer'
|
||||
import spotify from './services/spotify/reducer'
|
||||
|
||||
import thunk from 'redux-thunk'
|
||||
import mopidyMiddleware from './services/mopidy/middleware'
|
||||
import spotifyMiddleware from './services/spotify/middleware'
|
||||
|
||||
let reducers = combineReducers({
|
||||
mopidy,
|
||||
spotify
|
||||
});
|
||||
|
||||
// create our global store
|
||||
let store = createStore(
|
||||
reducer,
|
||||
reducers,
|
||||
{},
|
||||
applyMiddleware( thunk, mopidyMiddleware )
|
||||
applyMiddleware( thunk, mopidyMiddleware, spotifyMiddleware )
|
||||
);
|
||||
|
||||
export default store;
|
||||
|
||||
@ -18,19 +18,21 @@ class Album extends React.Component{
|
||||
|
||||
// on render
|
||||
componentDidMount(){
|
||||
this.loadAlbum( this.props.params.id );
|
||||
this.loadAlbum( this.props.params.uri );
|
||||
}
|
||||
|
||||
// when props changed
|
||||
componentWillReceiveProps( nextProps ){
|
||||
if( nextProps.params.id != this.props.params.id ){
|
||||
this.loadAlbum( nextProps.params.id );
|
||||
if( nextProps.params.uri != this.props.params.uri ){
|
||||
this.loadAlbum( nextProps.params.uri );
|
||||
}
|
||||
}
|
||||
|
||||
loadAlbum( id ){
|
||||
loadAlbum( uri ){
|
||||
let self = this;
|
||||
|
||||
var id = uri.replace('spotify:album:','');
|
||||
|
||||
$.ajax({
|
||||
method: 'GET',
|
||||
cache: true,
|
||||
|
||||
18
src/js/components/AlbumLink.js
Executable file
18
src/js/components/AlbumLink.js
Executable file
@ -0,0 +1,18 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
export default class AlbumLink extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render(){
|
||||
var link = '/album/' + this.props.album.uri;
|
||||
return (
|
||||
<Link to={link} className="album-link">{ this.props.album.name }</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,8 @@ import { connect } from 'react-redux'
|
||||
|
||||
import * as actions from '../actions/index'
|
||||
import * as mopidyActions from '../services/mopidy/actions'
|
||||
import * as spotifyActions from '../services/spotify/actions'
|
||||
import SpotifyAuthenticationFrame from './SpotifyAuthenticationFrame'
|
||||
|
||||
|
||||
/**
|
||||
@ -28,12 +30,9 @@ class App extends React.Component{
|
||||
<div>
|
||||
<ul role="nav">
|
||||
<li><Link to="/queue">Now playing</Link></li>
|
||||
<li><Link to="/album/6N51k5TP5pSZYPf7bLffLe">808's and Heartbreak</Link></li>
|
||||
<li><Link to="/album/1PgfRdl3lPyACfUGH4pquG">A million</Link></li>
|
||||
<li><Link to="/library/albums">Library: Albums</Link></li>
|
||||
<li><Link to="/album">Album</Link></li>
|
||||
<li><a onClick={this.handleClick}>Authorize</a></li>
|
||||
</ul>
|
||||
<SpotifyAuthenticationFrame />
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
@ -53,7 +52,8 @@ const mapStateToProps = (state, ownProps) => {
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch),
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch)
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
34
src/js/components/ArtistList.js
Executable file
34
src/js/components/ArtistList.js
Executable file
@ -0,0 +1,34 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
export default class ArtistList extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<span className="artist-list">
|
||||
{
|
||||
this.props.artists.map( (artist, index) => {
|
||||
var separator = null;
|
||||
if( index == this.props.artists.length - 2 ){
|
||||
separator = ' and ';
|
||||
}else if( index < this.props.artists.length - 2 ){
|
||||
separator = ', ';
|
||||
}
|
||||
var link = '/artist/' + artist.uri;
|
||||
return (
|
||||
<span className="artist" key={artist.uri}>
|
||||
<Link to={ link }>{ artist.name }</Link>{ separator }
|
||||
</span>
|
||||
);
|
||||
})
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -37,8 +37,12 @@ class Player extends React.Component{
|
||||
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>
|
||||
<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 }
|
||||
|
||||
@ -7,6 +7,8 @@ import FontAwesome from 'react-fontawesome'
|
||||
import TrackList from '../components/TrackList'
|
||||
import Track from '../components/Track'
|
||||
import Player from '../components/Player'
|
||||
import ArtistList from '../components/ArtistList'
|
||||
import AlbumLink from '../components/AlbumLink'
|
||||
import * as actions from '../services/mopidy/actions'
|
||||
|
||||
class Queue extends React.Component{
|
||||
@ -18,7 +20,11 @@ class Queue extends React.Component{
|
||||
renderTrackInFocus(){
|
||||
if( this.props.mopidy && this.props.mopidy.trackInFocus ){
|
||||
return (
|
||||
<div>{ this.props.mopidy.trackInFocus.track.name }</div>
|
||||
<div>
|
||||
<div>{ this.props.mopidy.trackInFocus.track.name }</div>
|
||||
<div><ArtistList artists={ this.props.mopidy.trackInFocus.track.artists } /></div>
|
||||
<div><AlbumLink album={ this.props.mopidy.trackInFocus.track.album } /></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
43
src/js/components/SpotifyAuthenticationFrame.js
Executable file
43
src/js/components/SpotifyAuthenticationFrame.js
Executable file
@ -0,0 +1,43 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { createStore, bindActionCreators } from 'redux'
|
||||
|
||||
import * as actions from '../services/spotify/actions'
|
||||
|
||||
class SpotifyAuthenticationFrame extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
handleClick(e){
|
||||
console.log('handling click')
|
||||
this.props.actions.authorize();
|
||||
}
|
||||
|
||||
render(){
|
||||
var src = '//jamesbarnsley.co.nz/spotmop.php?action=frame';
|
||||
if( this.props.spotify.authorizing ){
|
||||
src = '//jamesbarnsley.co.nz/spotmop.php?action=authorize&app='+location.protocol+'//'+window.location.host;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<button onClick={(e) => this.handleClick(e)}>Authorize</button>
|
||||
<iframe src={src}></iframe>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return state;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SpotifyAuthenticationFrame)
|
||||
@ -1,6 +1,8 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
import ArtistList from './ArtistList'
|
||||
import AlbumLink from './AlbumLink'
|
||||
|
||||
export default class Track extends React.Component{
|
||||
|
||||
@ -24,9 +26,13 @@ export default class Track extends React.Component{
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="track" onDoubleClick={ (e) => this.playTrack(e) }>
|
||||
<FontAwesome name={selectedIcon} fixedWidth onClick={ (e) => this.toggleSelected(e) } />
|
||||
#{this.props.track.uri}: {this.props.track.name}
|
||||
<div
|
||||
className="track"
|
||||
onDoubleClick={ (e) => this.playTrack(e) }>
|
||||
<FontAwesome name={selectedIcon} fixedWidth onClick={ (e) => this.toggleSelected(e) } />
|
||||
{this.props.track.name}
|
||||
<ArtistList artists={this.props.track.artists} />
|
||||
{ this.props.track.album ? <AlbumLink album={this.props.track.album} /> : null }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -24,8 +24,22 @@ class TrackList extends React.Component{
|
||||
|
||||
playTrack( index ){
|
||||
var tracks = this.state.tracks;
|
||||
console.log('playTrack', tracks[index].tlid)
|
||||
this.props.actions.changeTrack( index )
|
||||
this.props.actions.changeTrack( tracks[index].tlid )
|
||||
}
|
||||
|
||||
deleteSelected(){
|
||||
|
||||
function isSelected( track ){
|
||||
return ( typeof(track.selected) !== 'undefined' && track.selected );
|
||||
}
|
||||
|
||||
var selectedTracks = this.state.tracks.filter(isSelected)
|
||||
var selectedTracksTlids = [];
|
||||
for( var i = 0; i < selectedTracks.length; i++ ){
|
||||
selectedTracksTlids.push( selectedTracks[i].tlid )
|
||||
}
|
||||
|
||||
this.props.actions.removeTracks( selectedTracksTlids )
|
||||
}
|
||||
|
||||
componentWillReceiveProps( nextProps ){
|
||||
@ -36,28 +50,31 @@ class TrackList extends React.Component{
|
||||
let self = this;
|
||||
if( this.state.tracks ){
|
||||
return (
|
||||
<ul>
|
||||
{
|
||||
this.state.tracks.map(
|
||||
(track, index) => {
|
||||
<div>
|
||||
<ul>
|
||||
{
|
||||
this.state.tracks.map(
|
||||
(track, index) => {
|
||||
|
||||
// flatten nested track objects (as in the case of TlTracks)
|
||||
if( typeof(track.track) !== 'undefined' ){
|
||||
for( var property in track.track ){
|
||||
if( track.track.hasOwnProperty(property) ){
|
||||
track[property] = track.track[property]
|
||||
// flatten nested track objects (as in the case of TlTracks)
|
||||
if( typeof(track.track) !== 'undefined' ){
|
||||
for( var property in track.track ){
|
||||
if( track.track.hasOwnProperty(property) ){
|
||||
track[property] = track.track[property]
|
||||
}
|
||||
}
|
||||
}
|
||||
return <Track
|
||||
key={index+'_'+track.uri}
|
||||
track={track}
|
||||
playTrack={() => self.playTrack(index)}
|
||||
toggleSelected={() => self.toggleSelected(index)} />
|
||||
}
|
||||
return <Track
|
||||
key={index+'_'+track.uri}
|
||||
track={track}
|
||||
playTrack={() => self.playTrack(index)}
|
||||
toggleSelected={() => self.toggleSelected(index)} />
|
||||
}
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
<button onClick={() => this.deleteSelected()}>Delete selected</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
@ -18,7 +18,7 @@ ReactDOM.render(
|
||||
<Router history={hashHistory}>
|
||||
<Route path="/" component={App}>
|
||||
|
||||
<Route path="album/:id" component={Album} />
|
||||
<Route path="album/:uri" component={Album} />
|
||||
<Route path="queue" component={Queue} />
|
||||
|
||||
</Route>
|
||||
|
||||
@ -1,183 +0,0 @@
|
||||
|
||||
import Mopidy from 'mopidy'
|
||||
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'
|
||||
|
||||
|
||||
/**
|
||||
* Mopidy service
|
||||
*
|
||||
* Handles internal requests and passes them on to our connection
|
||||
**/
|
||||
class MopidyService extends React.Component{
|
||||
|
||||
constructor( props ){
|
||||
super(props);
|
||||
/*
|
||||
this.connection = new Mopidy({
|
||||
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 ){
|
||||
switch( type ){
|
||||
|
||||
case 'state:online':
|
||||
this.props.actions.updateStatus( true );
|
||||
this.get( 'playback', 'State' );
|
||||
this.get( 'playback', 'Volume' );
|
||||
this.get( 'tracklist', 'Consume' );
|
||||
this.get( 'tracklist', 'Random' );
|
||||
this.get( 'tracklist', 'Repeat' );
|
||||
this.get( 'tracklist', 'TlTracks' );
|
||||
this.get( 'playback', 'CurrentTlTrack' );
|
||||
break;
|
||||
|
||||
case 'state:offline':
|
||||
this.props.actions.updateStatus( false );
|
||||
break;
|
||||
|
||||
case 'event:tracklistChanged':
|
||||
this.get( 'tracklist', 'TlTracks' );
|
||||
break;
|
||||
|
||||
//case 'event:trackPlaybackEnded':
|
||||
case 'event:playbackStateChanged':
|
||||
case 'event:trackPlaybackStarted':
|
||||
this.get( 'playback', 'State' );
|
||||
this.get( 'playback', 'CurrentTlTrack' );
|
||||
break;
|
||||
|
||||
case 'event:volumeChanged':
|
||||
this.props.actions.updateVolume( data.volume );
|
||||
break;
|
||||
|
||||
case 'event:optionsChanged':
|
||||
this.get( 'tracklist', 'Consume' );
|
||||
this.get( 'tracklist', 'Random' );
|
||||
this.get( 'tracklist', 'Repeat' );
|
||||
break;
|
||||
|
||||
default:
|
||||
//console.log( 'MopidyService: Unhandled event', type, message );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get something from Mopidy
|
||||
*
|
||||
* Sends request to Mopidy server, and updates our local storage on return
|
||||
* @param string model Mopidy model (playback, tracklist, etc)
|
||||
* @param string property the property to get (TlTracks, Consume, etc)
|
||||
**/
|
||||
get( model, property ){
|
||||
console.log('MopidyService: '+model+'.get'+property);
|
||||
let self = this;
|
||||
this.connection[model]['get'+property]()
|
||||
.then(
|
||||
function( response ){
|
||||
self.props.actions['update'+property]( response );
|
||||
},
|
||||
function( error ){
|
||||
console.error( error );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set something in Mopidy
|
||||
*
|
||||
* Sends request to Mopidy server, and updates our local storage on return
|
||||
* @param string model Mopidy model (playback, tracklist, etc)
|
||||
* @param string property the property to get (TlTracks, Consume, etc)
|
||||
* @param mixed value
|
||||
**/
|
||||
set( model, property, value ){
|
||||
console.log('MopidyServie: '+model+'.set'+property, value);
|
||||
let self = this;
|
||||
this.connection[model]['set'+property]( value )
|
||||
.then(
|
||||
function( response ){
|
||||
self.props.actions['update'+property]( response );
|
||||
},
|
||||
function( error ){
|
||||
console.error( error );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
changeTlTrack( tlid ){
|
||||
console.log('Changing track to '+ tlid)
|
||||
}
|
||||
|
||||
|
||||
render(){
|
||||
console.log( this.props.mopidy );
|
||||
var playButton = <a onClick={() => this.connection.playback.play()}><FontAwesome name="play" /> </a>
|
||||
if( this.props.mopidy.state == 'playing' ){
|
||||
playButton = <a onClick={() => this.connection.playback.pause()}><FontAwesome name="pause" /> </a>
|
||||
}
|
||||
|
||||
var consumeButton = <a onClick={() => this.set('tracklist', 'Consume', [true])}>Consume </a>
|
||||
if( this.props.mopidy.consume ){
|
||||
consumeButton = <a onClick={() => this.set('tracklist', 'Consume', [false])}>Un-Consume </a>
|
||||
}
|
||||
|
||||
var randomButton = <a onClick={() => this.set('tracklist', 'Random', [true])}>Random </a>
|
||||
if( this.props.mopidy.random ){
|
||||
randomButton = <a onClick={() => this.set('tracklist', 'Random', [false])}>Un-Random </a>
|
||||
}
|
||||
|
||||
var repeatButton = <a onClick={() => this.set('tracklist', 'Repeat', [true])}>Repeat </a>
|
||||
if( this.props.mopidy.repeat ){
|
||||
repeatButton = <a onClick={() => this.set('tracklist', 'Repeat', [false])}>Un-Repeat </a>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ playButton }
|
||||
<a onClick={() => this.connection.playback.previous()}><FontAwesome name="step-backward" /> </a>
|
||||
<a onClick={() => this.connection.playback.next()}><FontAwesome name="step-forward" /> </a>
|
||||
{ consumeButton }
|
||||
{ randomButton }
|
||||
{ repeatButton }
|
||||
{ this.props.mopidy.volume }
|
||||
</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)(MopidyService)
|
||||
@ -1,11 +0,0 @@
|
||||
|
||||
import ServicesContainer from 'kioc';
|
||||
|
||||
import MopidyService from './MopidyService';
|
||||
import SpotifyService from './SpotifyService';
|
||||
|
||||
var Services = new ServicesContainer();
|
||||
Services.use(MopidyService, true);
|
||||
//Services.use(SpotifyService, true);
|
||||
|
||||
export default Services;
|
||||
@ -1,79 +0,0 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import * as actions from '../actions/spotifyActions'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
class SpotifyService extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
let self = this;
|
||||
this.urlBase = 'https://api.spotify.com/v1/';
|
||||
console.log('SpotifyProvider > constructor')
|
||||
|
||||
// listen for incoming messages from the authorization iframe
|
||||
// this is triggered when authentication is granted from the popup
|
||||
window.addEventListener('message', function(event){
|
||||
if( !/^https?:\/\/jamesbarnsley\.co\.nz/.test(event.origin) ) return false;
|
||||
self.handleMessage( event );
|
||||
}, false);
|
||||
}
|
||||
|
||||
handleMessage( event ){
|
||||
|
||||
// convert to json
|
||||
var data = JSON.parse(event.data);
|
||||
|
||||
// fire event
|
||||
this.props.actions.authorizeSpotifySuccess( data );
|
||||
}
|
||||
|
||||
test(){
|
||||
console.log('test stuff');
|
||||
}
|
||||
|
||||
getAlbum( albumid ){
|
||||
return $.when(
|
||||
$.ajax({
|
||||
method: 'GET',
|
||||
cache: true,
|
||||
url: this.urlBase+'albums/'+albumid
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
render(){
|
||||
|
||||
if( this.props.config.authorizingSpotify ){
|
||||
var src = '//jamesbarnsley.co.nz/spotmop.php?action=authorize&app='+location.protocol+'//'+window.location.host;
|
||||
return (
|
||||
<div>
|
||||
Authorizing...
|
||||
<iframe id="authorization-frame" src={src}></iframe>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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)(SpotifyService)
|
||||
@ -5,19 +5,35 @@
|
||||
|
||||
export function connect(){
|
||||
return {
|
||||
type: 'CONNECT'
|
||||
type: 'MOPIDY_CONNECT'
|
||||
}
|
||||
}
|
||||
|
||||
export function disconnect(){
|
||||
return {
|
||||
type: 'DISCONNECT'
|
||||
type: 'MOPIDY_DISCONNECT'
|
||||
}
|
||||
}
|
||||
|
||||
export function changeTrack( tlid ){
|
||||
return {
|
||||
type: 'MOPIDY_CHANGE_TRACK',
|
||||
call: 'playback.play',
|
||||
value: { tlid: tlid }
|
||||
}
|
||||
}
|
||||
|
||||
export function removeTracks( tlids ){
|
||||
return {
|
||||
type: 'MOPIDY_REMOVE_TRACKS',
|
||||
call: 'tracklist.remove',
|
||||
value: { tlid: tlids }
|
||||
}
|
||||
}
|
||||
|
||||
export function instruct( call, value ){
|
||||
return {
|
||||
type: 'INSTRUCT',
|
||||
type: 'MOPIDY_INSTRUCT',
|
||||
call: call,
|
||||
value: value
|
||||
}
|
||||
|
||||
@ -4,27 +4,15 @@ import actions from './actions'
|
||||
|
||||
const MopidyMiddleware = (function(){
|
||||
|
||||
// container for the actual Mopidy socket
|
||||
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());
|
||||
}
|
||||
|
||||
// handle all manner of socket messages
|
||||
const handleMessage = (ws, store, type, data) => {
|
||||
|
||||
switch( type ){
|
||||
|
||||
case 'state:online':
|
||||
//store.dispatch( actions.updateStatus( true ) );
|
||||
store.dispatch({ type: 'MOPIDY_CONNECTED' });
|
||||
instruct( ws, store, 'playback.getState' );
|
||||
instruct( ws, store, 'playback.getVolume' );
|
||||
instruct( ws, store, 'tracklist.getConsume' );
|
||||
@ -35,7 +23,7 @@ const MopidyMiddleware = (function(){
|
||||
break;
|
||||
|
||||
case 'state:offline':
|
||||
store.dispatch( actions.updateStatus( false ) );
|
||||
store.dispatch({ type: 'MOPIDY_DISCONNECTED' });
|
||||
break;
|
||||
|
||||
case 'event:tracklistChanged':
|
||||
@ -50,7 +38,7 @@ const MopidyMiddleware = (function(){
|
||||
break;
|
||||
|
||||
case 'event:volumeChanged':
|
||||
store.dispatch( actions.updateVolume( data.volume ) );
|
||||
store.dispatch({ type: 'MOPIDY_Volume', data: data.volume });
|
||||
break;
|
||||
|
||||
case 'event:optionsChanged':
|
||||
@ -60,35 +48,22 @@ const MopidyMiddleware = (function(){
|
||||
break;
|
||||
|
||||
default:
|
||||
//console.log( 'MopidyService: Unhandled event', type, message );
|
||||
//console.log( 'MopidyService: Unhandled message', 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
|
||||
* Call something with 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)
|
||||
* @param object ws = Mopidy class that wraps a Websocket
|
||||
* @param string model = which Mopidy model (playback, tracklist, etc)
|
||||
* @param string property = TlTracks, Consume, etc
|
||||
* @param string value (optional) = value of the property to pass
|
||||
**/
|
||||
const instruct = ( ws, store, call, value = false ) => {
|
||||
console.log('Mopidy: '+call, value);
|
||||
|
||||
var callParts = call.split('.');
|
||||
var model = callParts[0];
|
||||
var method = callParts[1];
|
||||
@ -96,30 +71,38 @@ const MopidyMiddleware = (function(){
|
||||
property = property.replace('get','');
|
||||
property = property.replace('set','');
|
||||
|
||||
// if we have a value, we need to include in our payload
|
||||
if( value ){
|
||||
ws[model][method]( value )
|
||||
.then(
|
||||
response => store.dispatch({ type: property, model: model, data: response }),
|
||||
response => store.dispatch({ type: 'MOPIDY_'+property, model: model, data: response }),
|
||||
error => console.error( error )
|
||||
);
|
||||
|
||||
// no value, so omit the payload
|
||||
}else{
|
||||
ws[model][method]()
|
||||
.then(
|
||||
response => store.dispatch({ type: property, model: model, data: response }),
|
||||
response => store.dispatch({ type: 'MOPIDY_'+property, model: model, data: response }),
|
||||
error => console.error( error )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware
|
||||
*
|
||||
* This behaves like an action interceptor. We listen for specific actions
|
||||
* and handle special functionality. If the action is not in our switch, then
|
||||
* it just proceeds to the next middleware, or default functionality
|
||||
**/
|
||||
return store => next => action => {
|
||||
|
||||
switch(action.type) {
|
||||
|
||||
//The user wants us to connect
|
||||
case 'CONNECT':
|
||||
case 'MOPIDY_CONNECT':
|
||||
|
||||
if(socket != null) socket.close();
|
||||
store.dispatch({ type: 'CONNECTING' });
|
||||
store.dispatch({ type: 'MOPIDY_CONNECTING' });
|
||||
|
||||
socket = new Mopidy({
|
||||
webSocketUrl: "ws://tv.barnsley.nz:6680/mopidy/ws",
|
||||
@ -130,19 +113,20 @@ const MopidyMiddleware = (function(){
|
||||
|
||||
break;
|
||||
|
||||
//The user wants us to disconnect
|
||||
case 'DISCONNECT':
|
||||
case 'MOPIDY_DISCONNECT':
|
||||
if(socket != null) socket.close();
|
||||
socket = null;
|
||||
store.dispatch({ type: 'DISCONNECTED' });
|
||||
store.dispatch({ type: 'MOPIDY_DISCONNECTED' });
|
||||
break;
|
||||
|
||||
//Send the 'SEND_MESSAGE' action down the websocket to the server
|
||||
case 'INSTRUCT':
|
||||
// send an instruction to the websocket
|
||||
case 'MOPIDY_INSTRUCT':
|
||||
case 'MOPIDY_CHANGE_TRACK':
|
||||
case 'MOPIDY_REMOVE_TRACKS':
|
||||
instruct( socket, store, action.call, action.value )
|
||||
break;
|
||||
|
||||
//This action is irrelevant to us, pass it on to the next middleware
|
||||
// This action is irrelevant to us, pass it on to the next middleware
|
||||
default:
|
||||
return next(action);
|
||||
}
|
||||
|
||||
@ -2,53 +2,56 @@
|
||||
export default function reducer(mopidy = {}, action){
|
||||
switch (action.type) {
|
||||
|
||||
case 'CONNECTING':
|
||||
case 'MOPIDY_CONNECTING':
|
||||
return Object.assign({}, mopidy, { connected: false, connecting: true });
|
||||
|
||||
case 'CONNECTED':
|
||||
case 'MOPIDY_CONNECTED':
|
||||
return Object.assign({}, mopidy, { connected: true, connecting: false });
|
||||
|
||||
case 'DISCONNECTED':
|
||||
case 'MOPIDY_DISCONNECTED':
|
||||
return Object.assign({}, mopidy, { connected: false, connecting: false });
|
||||
|
||||
case 'State':
|
||||
case 'MOPIDY_CHANGE_TRACK':
|
||||
return Object.assign({}, mopidy, {
|
||||
tlid: action.tlid
|
||||
});
|
||||
|
||||
/**
|
||||
* Websocket-initiated actions
|
||||
**/
|
||||
case 'MOPIDY_State':
|
||||
return Object.assign({}, mopidy, {
|
||||
state: action.data
|
||||
});
|
||||
|
||||
case 'Consume':
|
||||
case 'MOPIDY_Consume':
|
||||
return Object.assign({}, mopidy, {
|
||||
consume: action.data
|
||||
});
|
||||
|
||||
case 'Random':
|
||||
case 'MOPIDY_Random':
|
||||
return Object.assign({}, mopidy, {
|
||||
random: action.data
|
||||
});
|
||||
|
||||
case 'Repeat':
|
||||
case 'MOPIDY_Repeat':
|
||||
return Object.assign({}, mopidy, {
|
||||
repeat: action.data
|
||||
});
|
||||
|
||||
case 'TlTracks':
|
||||
case 'MOPIDY_TlTracks':
|
||||
return Object.assign({}, mopidy, {
|
||||
tracks: action.data
|
||||
tracks: action.data
|
||||
});
|
||||
|
||||
case 'CurrentTlTrack':
|
||||
case 'MOPIDY_CurrentTlTrack':
|
||||
return Object.assign({}, mopidy, {
|
||||
trackInFocus: action.data
|
||||
trackInFocus: action.data
|
||||
});
|
||||
|
||||
case 'Volume':
|
||||
case 'MOPIDY_Volume':
|
||||
return Object.assign({}, mopidy, {
|
||||
volume: action.volume
|
||||
});
|
||||
|
||||
case 'CHANGE_TRACK':
|
||||
return Object.assign({}, mopidy, {
|
||||
tlid: action.tlid
|
||||
volume: action.data
|
||||
});
|
||||
|
||||
default:
|
||||
|
||||
30
src/js/services/spotify/actions.js
Executable file
30
src/js/services/spotify/actions.js
Executable file
@ -0,0 +1,30 @@
|
||||
|
||||
/**
|
||||
* Actions and Action Creators
|
||||
**/
|
||||
|
||||
export function authorize(){
|
||||
return {
|
||||
type: 'SPOTIFY_AUTHORIZING'
|
||||
}
|
||||
}
|
||||
|
||||
export function connect(){
|
||||
return {
|
||||
type: 'SPOTIFY_CONNECTING'
|
||||
}
|
||||
}
|
||||
|
||||
export function disconnect(){
|
||||
return {
|
||||
type: 'SPOTIFY_DISCONNECT'
|
||||
}
|
||||
}
|
||||
|
||||
export function loadAlbum( uri ){
|
||||
return {
|
||||
type: 'SPOTIFY_LOAD_ALBUM',
|
||||
uri: uri
|
||||
}
|
||||
}
|
||||
|
||||
28
src/js/services/spotify/middleware.js
Executable file
28
src/js/services/spotify/middleware.js
Executable file
@ -0,0 +1,28 @@
|
||||
|
||||
import actions from './actions'
|
||||
|
||||
const SpotifyMiddleware = (function(){
|
||||
|
||||
return store => next => action => {
|
||||
|
||||
switch(action.type) {
|
||||
|
||||
case 'SPOTIFY_CONNECT':
|
||||
|
||||
console.log('Spotify wants to connect')
|
||||
|
||||
break;
|
||||
|
||||
case 'SPOTIFY_DISCONNECT':
|
||||
console.log('Spotify wants to DISconnect')
|
||||
break;
|
||||
|
||||
// This action is irrelevant to us, pass it on to the next middleware
|
||||
default:
|
||||
return next(action);
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
export default SpotifyMiddleware
|
||||
23
src/js/services/spotify/reducer.js
Executable file
23
src/js/services/spotify/reducer.js
Executable file
@ -0,0 +1,23 @@
|
||||
|
||||
export default function reducer(spotify = {}, action){
|
||||
switch (action.type) {
|
||||
|
||||
case 'SPOTIFY_CONNECTING':
|
||||
return Object.assign({}, spotify, { connected: false, connecting: true });
|
||||
|
||||
case 'SPOTIFY_AUTHORIZING':
|
||||
return Object.assign({}, spotify, { authorizing: true });
|
||||
|
||||
case 'SPOTIFY_CONNECTED':
|
||||
return Object.assign({}, spotify, { connected: true, connecting: false });
|
||||
|
||||
case 'SPOTIFY_DISCONNECTED':
|
||||
return Object.assign({}, spotify, { connected: false, connecting: false });
|
||||
|
||||
default:
|
||||
return spotify
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user