Snapcast fully operational

This commit is contained in:
James Barnsley
2019-10-30 06:42:11 +13:00
parent 88a5efaa05
commit 658b828c41
14 changed files with 6440 additions and 10693 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -100,7 +100,7 @@
// Release details
// These are automatically injected to built HTML
var build = "1571860001";
var build = "1571990280";
var version = "3.41.2";
// Construct the script tag

View File

@ -94,6 +94,9 @@ export class App extends React.Component {
// Fire up our services
this.props.mopidyActions.connect();
this.props.pusherActions.connect();
if (this.props.snapcast_enabled) {
this.props.snapcastActions.connect();
}
this.props.coreActions.getBroadcasts();
// Check for url-parsed configuration values
@ -358,6 +361,7 @@ const mapStateToProps = (state, ownProps) => ({
initial_setup_complete: state.ui.initial_setup_complete,
slim_mode: state.ui.slim_mode,
mopidy_connected: state.mopidy.connected,
snapcast_enabled: state.snapcast.enabled,
spotify_authorized: state.spotify.authorization,
sidebar_open: state.ui.sidebar_open,
dragging: state.ui.dragger && state.ui.dragger.active,

View File

@ -72,26 +72,26 @@ class OutputControl extends React.Component {
snapcast_clients = (
<div>
{
clients.map((client) => (
<div className="output-control__item outputs__item--snapcast" key={client.id}>
<div className="output-control__item__name">
{client.name}
</div>
<div className="output-control__item__controls">
<MuteControl
className="output-control__item__mute"
noTooltip
mute={client.mute}
onMuteChange={(mute) => this.props.snapcastActions.setClientMute(client.id, mute)}
/>
<VolumeControl
className="output-control__item__volume"
volume={client.volume}
mute={client.mute}
onVolumeChange={(percent) => this.props.snapcastActions.setClientVolume(client.id, percent)}
/>
</div>
</div>
clients.map(client => (
<div className="output-control__item outputs__item--snapcast" key={client.id}>
<div className="output-control__item__name">
{client.name}
</div>
<div className="output-control__item__controls">
<MuteControl
className="output-control__item__mute"
noTooltip
mute={client.mute}
onMuteChange={(mute) => this.props.snapcastActions.setClientMute(client.id, mute)}
/>
<VolumeControl
className="output-control__item__volume"
volume={client.volume}
mute={client.mute}
onVolumeChange={(percent) => this.props.snapcastActions.setClientVolume(client.id, percent)}
/>
</div>
</div>
))
}
</div>

View File

@ -359,7 +359,15 @@ class Services extends React.Component {
<div className="menu-item__title">
Snapcast
</div>
{this.props.pusher.config.snapcast_enabled ? <span className="status green-text">Enabled</span> : <span className="status mid_grey-text">Disabled</span>}
{!this.props.snapcast.enabled && (
<span className="status mid_grey-text">Disabled</span>
)}
{this.props.snapcast.enabled && !this.props.snapcast.connected && (
<span className="status red-text">Disconnected</span>
)}
{this.props.snapcast.enabled && this.props.snapcast.connected && (
<span className="status green-text">Connected</span>
)}
</div>
</Link>
<Link history={this.props.history} className="menu-item menu-item--icecast" activeClassName="menu-item--active" to="/settings/icecast" scrollTo="#services-menu">

View File

@ -64,7 +64,7 @@ class Snapcast extends React.Component {
if (!clients || clients.length <= 0) {
return (
<div className="text mid_grey-text">
No clients
No clients
</div>
);
}
@ -72,116 +72,121 @@ class Snapcast extends React.Component {
return (
<div className="list snapcast__clients">
{
clients.map((client) => {
let class_name = 'list__item list__item--no-interaction snapcast__client';
if (client.connected) {
class_name += ' snapcast__client--connected';
} else {
class_name += ' snapcast__client--disconnected';
}
clients.map((client) => {
let class_name = 'list__item list__item--no-interaction snapcast__client';
if (client.connected) {
class_name += ' snapcast__client--connected';
} else {
class_name += ' snapcast__client--disconnected';
}
if (this.state.clients_expanded.includes(client.id)) {
return (
<div className={`${class_name} snapcast__client--expanded`} key={client.id}>
<div className="snapcast__client__header" onClick={(e) => this.toggleClientExpanded(client.id)}>
{client.name}
<div className="snapcast__client__header__icons">
{!client.connected ? <Icon name="power_off" className="disconnected" /> : null}
<Icon name="expand_less" />
</div>
</div>
<div className="snapcast__client__details">
<label className="field">
<div className="name">
Name
</div>
<div className="input">
<TextField
onChange={(value) => actions.setClientName(client.id, value)}
value={client.name}
/>
</div>
</label>
<label className="field dropdown">
<div className="name">
Group
</div>
<div className="input">
<select onChange={(e) => actions.setClientGroup(client.id, e.target.value)} value={group.id}>
{
groups.map((group) => (
<option value={group.id} key={group.id}>
{group.name ? group.name : `Group ${group.id.substring(0, 3)}`}
</option>
))
}
<option value={group.id}>
New group
</option>
</select>
</div>
</label>
<div className="snapcast__client__volume field">
<div className="name">
Volume
</div>
<div className="input">
<MuteControl
className="snapcast__client__mute-control"
mute={client.mute}
onMuteChange={(mute) => actions.setClientMute(client.id, mute)}
/>
<VolumeControl
className="snapcast__client__volume-control"
volume={client.volume}
onVolumeChange={(percent) => actions.setClientVolume(client.id, percent)}
/>
</div>
</div>
<div className="snapcast__client__latency field">
<div className="name">
Latency
</div>
<div className="input">
<LatencyControl
max="150"
value={client.latency}
onChange={(value) => actions.setClientLatency(client.id, parseInt(value))}
/>
<TextField
className="tiny"
type="number"
onChange={(value) => actions.setClientLatency(client.id, parseInt(value))}
value={String(client.latency)}
/>
</div>
</div>
</div>
</div>
);
}
return (
<div className={`${class_name} snapcast__client--collapsed`} key={client.id}>
<div className="snapcast__client__header" onClick={(e) => this.toggleClientExpanded(client.id)}>
{client.name}
<div className="snapcast__client__header__icons">
{!client.connected ? <Icon name="power_off" className="disconnected" /> : null}
<Icon name="expand_more" />
</div>
</div>
</div>
);
})
}
if (this.state.clients_expanded.includes(client.id)) {
return (
<div className={`${class_name} snapcast__client--expanded`} key={client.id}>
<div className="snapcast__client__header" onClick={(e) => this.toggleClientExpanded(client.id)}>
{client.name}
<div className="snapcast__client__header__icons">
{!client.connected ? <Icon name="power_off" className="disconnected" /> : null}
<Icon name="expand_less" />
</div>
</div>
<div className="snapcast__client__details">
<label className="field">
<div className="name">
Name
</div>
<div className="input">
<TextField
onChange={(value) => actions.setClientName(client.id, value)}
value={client.name}
/>
</div>
</label>
<label className="field dropdown">
<div className="name">
Group
</div>
<div className="input">
<select onChange={(e) => actions.setClientGroup(client.id, e.target.value)} value={group.id}>
{
groups.map((group) => (
<option value={group.id} key={group.id}>
{group.name ? group.name : `Group ${group.id.substring(0, 3)}`}
</option>
))
}
<option value={group.id}>
New group
</option>
</select>
</div>
</label>
<div className="snapcast__client__volume field">
<div className="name">
Volume
</div>
<div className="input">
<MuteControl
className="snapcast__client__mute-control"
mute={client.mute}
onMuteChange={(mute) => actions.setClientMute(client.id, mute)}
/>
<VolumeControl
className="snapcast__client__volume-control"
volume={client.volume}
onVolumeChange={(percent) => actions.setClientVolume(client.id, percent)}
/>
</div>
</div>
<div className="snapcast__client__latency field">
<div className="name">
Latency
</div>
<div className="input">
<LatencyControl
max="150"
value={client.latency}
onChange={(value) => actions.setClientLatency(client.id, parseInt(value))}
/>
<TextField
className="tiny"
type="number"
onChange={(value) => actions.setClientLatency(client.id, parseInt(value))}
value={String(client.latency)}
/>
</div>
</div>
</div>
</div>
);
}
return (
<div className={`${class_name} snapcast__client--collapsed`} key={client.id}>
<div className="snapcast__client__header" onClick={(e) => this.toggleClientExpanded(client.id)}>
{client.name}
<div className="snapcast__client__header__icons">
{!client.connected ? <Icon name="power_off" className="disconnected" /> : null}
<Icon name="expand_more" />
</div>
</div>
</div>
);
})
}
</div>
);
}
render() {
const {
enabled,
snapcastActions: actions,
uiActions,
snapcast: {
host,
port,
enabled,
connected,
},
} = this.props;
const streams = [];
@ -202,13 +207,14 @@ class Snapcast extends React.Component {
<div className="snapcast">
<div className="field checkbox">
<div className="name">Enabled</div>
<div className="input">
<label>
<input
type="checkbox"
name="enabled"
checked={enabled}
onChange={() => actions.setEnabled(!enabled)}
onChange={() => actions.set({ enabled: !enabled })}
/>
<span className="label">
Enabled
@ -222,93 +228,107 @@ class Snapcast extends React.Component {
onChange={() => uiActions.set({ snapcast_show_disconnected_clients: !this.props.show_disconnected_clients })}
/>
<span className="label">
Show disconnected clients
Show disconnected clients
</span>
</label>
</div>
</div>
<div className="field">
<div className="name">Host</div>
<div className="input">
<TextField
value={host}
onChange={value => actions.set({ host: value })}
/>
</div>
</div>
<div className="field">
<div className="name">Port</div>
<div className="input">
<TextField
value={port}
onChange={value => actions.set({ port: value })}
/>
</div>
</div>
<div className="snapcast__groups">
{
groups.map((group) => {
group = helpers.collate(group, { clients: this.props.clients });
groups.map((group) => {
group = helpers.collate(group, { clients: this.props.clients });
// Average our clients' volume for an overall group volume
let group_volume = 0;
for (let i = 0; i < group.clients.length; i++) {
const client = group.clients[i];
group_volume += client.volume;
}
group_volume /= group.clients.length;
// Average our clients' volume for an overall group volume
let group_volume = 0;
for (let i = 0; i < group.clients.length; i++) {
const client = group.clients[i];
group_volume += client.volume;
}
group_volume /= group.clients.length;
return (
<div className="snapcast__group" key={group.id}>
<div className="field">
<div className="name">
Name
</div>
<div className="input">
<div className="text">
{group.name}
{' '}
&nbsp;
<span className="mid_grey-text">
(
{group.id}
)
</span>
</div>
</div>
</div>
<div className="field dropdown">
<div className="name">
Stream
</div>
<div className="input">
<select onChange={(e) => actions.setGroupStream(group.id, e.target.value)} value={group.stream_id}>
{
streams.map((stream) => (
<option value={stream.id} key={stream.id}>
{stream.id}
{' '}
(
{stream.status}
)
</option>
))
}
</select>
</div>
</div>
<div className="field">
<div className="name">
Volume
</div>
<div className="input">
<MuteControl
className="snapcast__group__mute-control"
mute={group.muted}
onMuteChange={(mute) => actions.setGroupMute(group.id, mute)}
/>
<VolumeControl
className="snapcast__group__volume-control"
volume={group_volume}
onVolumeChange={(percent, old_percent) => actions.setGroupVolume(group.id, percent, old_percent)}
/>
</div>
</div>
<div className="field">
<div className="name">
Clients
</div>
<div className="input">
{this.renderClientsList(group, groups)}
</div>
</div>
</div>
);
})
}
return (
<div className="snapcast__group" key={group.id}>
<div className="field text">
<div className="name">
Name
</div>
<div className="input">
<TextField
value={group.name}
onChange={value => actions.setGroupName(group.id, value)}
/>
</div>
</div>
<div className="field dropdown">
<div className="name">
Stream
</div>
<div className="input">
<select onChange={(e) => actions.setGroupStream(group.id, e.target.value)} value={group.stream_id}>
{
streams.map((stream) => (
<option value={stream.id} key={stream.id}>
{stream.id}
{' '}
(
{stream.status}
)
</option>
))
}
</select>
</div>
</div>
<div className="field">
<div className="name">
Volume
</div>
<div className="input">
<MuteControl
className="snapcast__group__mute-control"
mute={group.muted}
onMuteChange={(mute) => actions.setGroupMute(group.id, mute)}
/>
<VolumeControl
className="snapcast__group__volume-control"
volume={group_volume}
onVolumeChange={(percent, old_percent) => actions.setGroupVolume(group.id, percent, old_percent)}
/>
</div>
</div>
<div className="field">
<div className="name">
Clients
</div>
<div className="input">
{this.renderClientsList(group, groups)}
</div>
</div>
</div>
);
})
}
</div>
</div>
);
@ -316,8 +336,7 @@ class Snapcast extends React.Component {
}
const mapStateToProps = (state, ownProps) => ({
enabled: state.snapcast.enabled,
connected: state.snapcast.connected,
snapcast: state.snapcast,
show_disconnected_clients: (
state.ui.snapcast_show_disconnected_clients !== undefined
? state.ui.snapcast_show_disconnected_clients

View File

@ -243,18 +243,18 @@ export const digestMopidyImages = function (mopidy, images) {
};
export const generateGuid = function (type = 'numeric') {
export const generateGuid = function (type = 'numeric', length = 12) {
// numeric
if (type == 'numeric') {
const date = new Date().valueOf().toString();
const random_number = Math.floor((Math.random() * 100)).toString();
return parseInt(date + random_number);
}
const format = 'xxxxxxxxxx';
const format = 'x'.repeat(length);
return format.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0; const
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
return v.toString(length);
});
};

View File

@ -1,11 +1,11 @@
const helpers = require('../../helpers.js');
const localstorageMiddleware = (function () {
const localstorageMiddleware = (function () {
/**
* The actual middleware inteceptor
* */
return (store) => (next) => (action) => {
return (store) => (next) => (action) => {
// proceed as normal first
// this way, any reducers and middleware do their thing BEFORE we store our new state
next(action);
@ -14,7 +14,7 @@ const localstorageMiddleware = (function () {
window._store = store;
// if debug enabled
if (store.getState().ui.log_actions) {
if (store.getState().ui.log_actions) {
const ignored_actions = [
'START_LOADING',
'STOP_LOADING',
@ -26,7 +26,7 @@ const localstorageMiddleware = (function () {
}
}
switch (action.type) {
switch (action.type) {
case 'PUSHER_CONNECTED':
helpers.setStorage(
'pusher',
@ -236,6 +236,13 @@ const localstorageMiddleware = (function () {
);
break;
case 'SNAPCAST_SET':
helpers.setStorage(
'snapcast',
action.data,
);
break;
case 'SNAPCAST_COMMANDS_UPDATED':
helpers.setStorage(
'snapcast',
@ -290,7 +297,7 @@ const localstorageMiddleware = (function () {
break;
*/
}
};
};
}());
export default localstorageMiddleware;

View File

@ -159,11 +159,6 @@ const PusherMiddleware = (function () {
store.dispatch(uiActions.createNotification({ type: 'bad', content: message.params.message, description: message.params.description }));
break;
}
// Pass snapcast events to the Snapcast service
if (message.method.startsWith('snapcast_')) {
store.dispatch(snapcastActions.eventReceived(message));
}
}
}
};

View File

@ -1,8 +1,8 @@
export function setEnabled(enabled) {
export function set(data) {
return {
type: 'SNAPCAST_SET_ENABLED',
enabled,
type: 'SNAPCAST_SET',
data,
};
}
@ -18,6 +18,23 @@ export function disconnect() {
};
}
export function request(method, params = null, response_callback = null, error_callback = null) {
return {
type: 'SNAPCAST_REQUEST',
method,
params,
response_callback,
error_callback,
};
}
export function debug(message = null) {
return {
type: 'SNAPCAST_DEBUG',
message,
};
}
export function getServer() {
return {
type: 'SNAPCAST_GET_SERVER',
@ -71,6 +88,14 @@ export function deleteClient(id) {
};
}
export function setGroupName(id, name) {
return {
type: 'SNAPCAST_SET_GROUP_NAME',
id,
name,
};
}
export function setGroupStream(id, stream_id) {
return {
type: 'SNAPCAST_SET_GROUP_STREAM',
@ -96,14 +121,6 @@ export function setGroupVolume(id, percent, old_percent = 0) {
};
}
export function eventReceived(message) {
return {
type: 'SNAPCAST_EVENT_RECEIVED',
method: message.method,
params: message.params,
};
}
/**
* Record loaders

View File

@ -1,5 +1,6 @@
import ReactGA from 'react-ga';
import { sha256 } from 'js-sha256';
const helpers = require('../../helpers');
const coreActions = require('../core/actions');
@ -63,8 +64,66 @@ const SnapcastMiddleware = (function () {
));
} else {
switch (message.method) {
case 'connection_added':
store.dispatch(pusherActions.connectionAdded(message.params.connection));
case 'Client.OnConnect':
store.dispatch(snapcastActions.clientLoaded(
{
id: message.params.client.id,
name: message.params.client.name,
volume: message.params.client.config.volume.percent,
mute: message.params.client.config.volume.muted,
connected: message.params.client.connected,
},
));
break;
case 'Client.OnDisconnect':
store.dispatch(snapcastActions.clientLoaded(
{
id: message.params.client.id,
connected: message.params.client.connected,
},
));
break;
case 'Client.OnVolumeChanged':
store.dispatch(snapcastActions.clientLoaded(
{
id: message.params.id,
mute: message.params.volume.muted,
volume: message.params.volume.percent,
},
));
break;
case 'Client.OnLatencyChanged':
store.dispatch(snapcastActions.clientLoaded(
{
id: message.params.id,
latency: message.params.latency,
},
));
break;
case 'Client.OnNameChanged':
store.dispatch(snapcastActions.clientLoaded(
{
id: message.params.id,
name: message.params.name,
},
));
break;
case 'Group.OnMute':
store.dispatch(snapcastActions.groupLoaded(
{
id: message.params.id,
mute: message.params.mute,
},
));
break;
case 'Server.OnUpdate':
store.dispatch(snapcastActions.serverLoaded(message.param));
break;
}
}
@ -72,7 +131,7 @@ const SnapcastMiddleware = (function () {
};
const request = (store, method, params = null) => new Promise((resolve, reject) => {
const id = helpers.generateGuid();
const id = helpers.generateGuid(8);
const message = {
jsonrpc: '2.0',
id,
@ -82,7 +141,7 @@ const SnapcastMiddleware = (function () {
message.params = params;
}
if (store.getState().ui.log_pusher) {
if (store.getState().ui.log_snapcast) {
console.log('Snapcast log (outgoing)', message);
}
@ -121,14 +180,46 @@ const SnapcastMiddleware = (function () {
}
store.dispatch({ type: 'SNAPCAST_CONNECTING' });
var state = store.getState();
socket = new Mopidy({
webSocketUrl: `ws${window.location.protocol === 'https:' ? 's' : ''}://${state.snapcast.host}:${state.snapcast.port}/jsonrpc/`,
callingConvention: 'by-position-or-by-name',
});
socket = new WebSocket(
`ws${window.location.protocol === 'https:' ? 's' : ''}://${state.snapcast.host}:${state.snapcast.port}/jsonrpc`,
);
socket.on((type, data) => handleMessage(socket, store, type, data));
socket.onopen = () => {
store.dispatch({
type: 'SNAPCAST_CONNECTED',
});
};
socket.onclose = (e) => {
store.dispatch({
type: 'SNAPCAST_DISCONNECTED',
});
// attempt to reconnect every 5 seconds
if (state.snapcast.enabled) {
setTimeout(() => {
store.dispatch(snapcastActions.connect());
}, 5000);
}
};
socket.onerror = (e) => {
if (socket.readyState == 1) {
store.dispatch(coreActions.handleException(
'Snapcast websocket error',
e,
e.type,
));
}
};
socket.onmessage = (message) => {
var message = JSON.parse(message.data);
handleMessage(socket, store, message);
};
break;
case 'SNAPCAST_CONNECTED':
@ -137,6 +228,7 @@ const SnapcastMiddleware = (function () {
ReactGA.event({ category: 'Snapcast', action: 'Connected', label: hashed_hostname });
}
store.dispatch(uiActions.createNotification({ content: 'Snapcast connected' }));
store.dispatch(snapcastActions.getServer());
next(action);
break;
@ -152,39 +244,59 @@ const SnapcastMiddleware = (function () {
break;
case 'SNAPCAST_DEBUG':
request(socket, store, action.call, action.value)
.then((response) => {
store.dispatch({ type: 'DEBUG', response });
});
request(store, action.message.method, action.message.data)
.then(
(response) => {
store.dispatch({ type: 'DEBUG', response });
},
(error) => {
store.dispatch(coreActions.handleException(
'Could not debug',
error,
error.message,
));
},
);
break;
case 'SNAPCAST_REQUEST':
request(store, action.method, action.params)
.then(
(response) => {
if (action.response_callback) {
action.response_callback.call(this, response);
}
},
(error) => {
if (action.error_callback) {
action.error_callback.call(this, error);
} else {
store.dispatch(coreActions.handleException(
'Snapcast request failed',
error,
action.method,
action,
));
}
},
);
break;
case 'SNAPCAST_GET_SERVER':
request(
store,
{
method: 'Server.GetStatus',
},
(response) => {
store.dispatch(snapcastActions.serverLoaded(response.server));
},
(error) => {
store.dispatch(coreActions.handleException(
'Could not get server',
error,
));
},
);
break;
case 'SNAPCAST_SERVER_LOADED':
store.dispatch(snapcastActions.groupsLoaded(action.server.groups, true));
store.dispatch(snapcastActions.streamsLoaded(action.server.streams, true));
// Snapcast double-nests the server object
action.server = action.server.server;
next(action);
request(store, 'Server.GetStatus')
.then(
(response) => {
store.dispatch(snapcastActions.serverLoaded(response.server.server, true));
store.dispatch(snapcastActions.groupsLoaded(response.server.groups, true));
store.dispatch(snapcastActions.streamsLoaded(response.server.streams, true));
},
(error) => {
store.dispatch(coreActions.handleException(
'Could not get Snapcast server',
error,
));
},
);
break;
case 'SNAPCAST_GROUPS_LOADED':
@ -237,127 +349,110 @@ const SnapcastMiddleware = (function () {
case 'SNAPCAST_SET_CLIENT_NAME':
var client = snapcast.clients[action.id];
var data = {
method: 'Client.SetName',
params: {
id: action.id,
name: action.name,
},
var params = {
id: action.id,
name: action.name,
};
request(
store,
data,
(response) => {
store.dispatch(snapcastActions.clientLoaded(
{
id: action.id,
name: response.name,
},
));
},
);
request(store, 'Client.SetName', params)
.then(
response => {
store.dispatch(snapcastActions.clientLoaded(
{
id: action.id,
name: response.name,
},
));
},
);
break;
case 'SNAPCAST_SET_CLIENT_MUTE':
var client = store.getState().snapcast.clients[action.id];
var data = {
method: 'Client.SetVolume',
params: {
id: action.id,
volume: {
muted: action.mute,
percent: client.volume,
},
var params = {
id: action.id,
volume: {
muted: action.mute,
percent: client.volume,
},
};
request(
store,
data,
(response) => {
store.dispatch(snapcastActions.clientLoaded(
{
id: action.id,
volume: response.volume.percent,
mute: response.volume.muted,
},
));
},
(error) => {
store.dispatch(coreActions.handleException(
'Error',
error,
error.message,
));
},
);
request(store, 'Client.SetVolume', params)
.then(
response => {
store.dispatch(snapcastActions.clientLoaded(
{
id: action.id,
volume: response.volume.percent,
mute: response.volume.muted,
},
));
},
error => {
store.dispatch(coreActions.handleException(
'Error',
error,
error.message,
));
},
);
break;
case 'SNAPCAST_SET_CLIENT_VOLUME':
var client = snapcast.clients[action.id];
var data = {
method: 'Client.SetVolume',
params: {
id: action.id,
volume: {
muted: client.mute,
percent: action.volume,
},
var params = {
id: action.id,
volume: {
muted: client.mute,
percent: action.volume,
},
};
request(
store,
data,
(response) => {
store.dispatch(snapcastActions.clientLoaded(
{
id: action.id,
volume: response.volume.percent,
},
));
},
(error) => {
store.dispatch(coreActions.handleException(
'Error',
error,
error.message,
));
},
);
request(store, 'Client.SetVolume', params)
.then(
response => {
store.dispatch(snapcastActions.clientLoaded(
{
id: action.id,
volume: response.volume.percent,
},
));
},
(error) => {
store.dispatch(coreActions.handleException(
'Error',
error,
error.message,
));
}
);
break;
case 'SNAPCAST_SET_CLIENT_LATENCY':
var client = store.getState().snapcast.clients[action.id];
var data = {
method: 'Client.SetLatency',
params: {
id: action.id,
latency: action.latency,
},
var params = {
id: action.id,
latency: action.latency,
};
request(
store,
data,
(response) => {
store.dispatch(snapcastActions.clientLoaded(
{
id: action.id,
latency: response.latency,
},
));
},
(error) => {
store.dispatch(coreActions.handleException(
'Error',
error,
error.message,
));
},
);
request(store, 'Client.SetLatency', params)
.then(
response => {
store.dispatch(snapcastActions.clientLoaded(
{
id: action.id,
latency: response.latency,
},
));
},
error => {
store.dispatch(coreActions.handleException(
'Error',
error,
error.message,
));
},
);
break;
case 'SNAPCAST_SET_CLIENT_GROUP':
@ -375,117 +470,121 @@ const SnapcastMiddleware = (function () {
clients_ids.splice(clients_ids_index, 1);
}
var data = {
method: 'Group.SetClients',
params: {
id: action.group_id,
clients: clients_ids,
},
var params = {
id: action.group_id,
clients: clients_ids,
};
request(
store,
data,
(response) => {
store.dispatch(snapcastActions.serverLoaded(response.server));
},
(error) => {
store.dispatch(coreActions.handleException(
'Error',
error,
error.message,
));
},
);
request(store, 'Group.SetClients', params)
.then(
response => {
store.dispatch(snapcastActions.serverLoaded(response.server));
},
error => {
store.dispatch(coreActions.handleException(
'Error',
error,
error.message,
));
},
);
break;
case 'SNAPCAST_DELETE_CLIENT':
var data = {
method: 'Server.DeleteClient',
params: {
id: action.id,
},
var params = {
id: action.id,
};
request(
store,
data,
(response) => {
store.dispatch({
type: 'SNAPCAST_CLIENT_REMOVED',
key: action.data.params.id,
});
},
(error) => {
store.dispatch(coreActions.handleException(
'Error',
error,
error.message,
));
},
);
request(store, 'Server.DeleteClient', params)
.then(
response => {
store.dispatch({
type: 'SNAPCAST_CLIENT_REMOVED',
key: action.data.params.id,
});
},
error => {
store.dispatch(coreActions.handleException(
'Error',
error,
error.message,
));
},
);
break;
case 'SNAPCAST_SET_GROUP_NAME':
var group = snapcast.groups[action.id];
var params = {
id: action.id,
name: action.name,
};
request(store, 'Group.SetName', params)
.then(
response => {
store.dispatch(snapcastActions.groupLoaded(
{
id: action.id,
name: response.name,
},
));
},
);
break;
case 'SNAPCAST_SET_GROUP_STREAM':
var group = store.getState().snapcast.groups[action.id];
var data = {
method: 'Group.SetStream',
params: {
id: action.id,
stream_id: action.stream_id,
},
var params = {
id: action.id,
stream_id: action.stream_id,
};
request(
store,
data,
(response) => {
store.dispatch(snapcastActions.groupLoaded(
{
id: action.id,
stream_id: action.stream_id,
},
));
},
(error) => {
store.dispatch(coreActions.handleException(
'Could not change stream',
error,
error.message,
));
},
);
request(store, 'Group.SetStream', params)
.then(
response => {
store.dispatch(snapcastActions.groupLoaded(
{
id: action.id,
stream_id: action.stream_id,
},
));
},
error => {
store.dispatch(coreActions.handleException(
'Could not change stream',
error,
error.message,
));
},
);
break;
case 'SNAPCAST_SET_GROUP_MUTE':
var group = store.getState().snapcast.groups[action.id];
var data = {
method: 'Group.SetMute',
params: {
id: action.id,
mute: action.mute,
},
var params = {
id: action.id,
mute: action.mute,
};
request(
store,
data,
(response) => {
store.dispatch(snapcastActions.groupLoaded(
{
id: action.id,
muted: response.mute,
},
));
},
(error) => {
store.dispatch(coreActions.handleException(
'Could not toggle mute',
error,
error.message,
));
},
);
request(store, 'Group.SetMute', params)
.then(
response => {
store.dispatch(snapcastActions.groupLoaded(
{
id: action.id,
muted: response.mute,
},
));
},
error => {
store.dispatch(coreActions.handleException(
'Could not toggle mute',
error,
error.message,
));
},
);
break;
case 'SNAPCAST_SET_GROUP_VOLUME':
@ -522,78 +621,7 @@ const SnapcastMiddleware = (function () {
store.dispatch(snapcastActions.setClientVolume(client_to_update.id, volume));
}
break;
case 'SNAPCAST_EVENT_RECEIVED':
// Drop the prefix
action.method = action.method.replace('snapcast_', '');
switch (action.method) {
case 'Client.OnConnect':
store.dispatch(snapcastActions.clientLoaded(
{
id: action.params.client.id,
name: action.params.client.name,
volume: action.params.client.volume.percent,
mute: action.params.client.volume.muted,
connected: action.params.client.connected,
},
));
break;
case 'Client.OnDisconnect':
store.dispatch(snapcastActions.clientLoaded(
{
id: action.params.client.id,
connected: action.params.client.connected,
},
));
break;
case 'Client.OnVolumeChanged':
store.dispatch(snapcastActions.clientLoaded(
{
id: action.params.id,
mute: action.params.volume.muted,
volume: action.params.volume.percent,
},
));
break;
case 'Client.OnLatencyChanged':
store.dispatch(snapcastActions.clientLoaded(
{
id: action.params.id,
latency: action.params.latency,
},
));
break;
case 'Client.OnNameChanged':
store.dispatch(snapcastActions.clientLoaded(
{
id: action.params.id,
name: action.params.name,
},
));
break;
case 'Group.OnMute':
store.dispatch(snapcastActions.groupLoaded(
{
id: action.params.id,
mute: action.params.mute,
},
));
break;
case 'Server.OnUpdate':
store.dispatch(snapcastActions.serverLoaded(action.params));
break;
}
break;
// This action is irrelevant to us, pass it on to the next middleware
default:
return next(action);
}

View File

@ -1,9 +1,17 @@
export default function reducer(snapcast = {}, action) {
console.log(action);
switch (action.type) {
case 'SNAPCAST_SET_ENABLED':
return { ...snapcast, enabled: action.enabled };
case 'SNAPCAST_SET':
return { ...snapcast, ...action.data };
case 'SNAPCAST_CONNECTED':
return { ...snapcast, connected: true, connecting: false };
case 'SNAPCAST_CONNECTING':
return { ...snapcast, connecting: true, connected: false };
case 'SNAPCAST_DISCONNECTED':
return { ...snapcast, connected: false, connecting: false };
case 'SNAPCAST_SERVER_LOADED':
return { ...snapcast, server: action.server };

View File

@ -13,6 +13,7 @@ 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';
import * as snapcastActions from '../services/snapcast/actions';
class Debug extends React.Component {
constructor(props) {
@ -21,6 +22,7 @@ class Debug extends React.Component {
mopidy_call: 'playlists.asList',
mopidy_data: '{}',
pusher_data: '{"method":"get_config"}',
snapcast_data: '{"method":"Server.GetStatus"}',
access_token: this.props.access_token,
toggling_test_mode: false,
};
@ -40,6 +42,11 @@ class Debug extends React.Component {
this.props.pusherActions.debug(JSON.parse(this.state.pusher_data));
}
callSnapcast(e) {
e.preventDefault();
this.props.snapcastActions.debug(JSON.parse(this.state.snapcast_data));
}
toggleTestMode(e) {
this.setState({ toggling_test_mode: true });
this.props.uiActions.set({ test_mode: !this.props.test_mode });
@ -216,6 +223,25 @@ Back
</div>
</form>
<h4 className="underline">Snapcast</h4>
<form onSubmit={(e) => this.callSnapcast(e)}>
<label className="field">
<div className="name">Data</div>
<div className="input">
<textarea
onChange={(e) => this.setState({ snapcast_data: e.target.value })}
value={this.state.snapcast_data}
/>
</div>
</label>
<div className="field">
<div className="name" />
<div className="input">
<button type="submit" className="button button--default">Send</button>
</div>
</div>
</form>
<h4 className="underline">Response</h4>
<pre>
{ this.props.debug_response ? JSON.stringify(this.props.debug_response, null, 2) : null }
@ -244,6 +270,7 @@ const mapDispatchToProps = (dispatch) => ({
pusherActions: bindActionCreators(pusherActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch),
spotifyActions: bindActionCreators(spotifyActions, dispatch),
snapcastActions: bindActionCreators(snapcastActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Debug);