WIP: Snapcast

This commit is contained in:
James Barnsley
2019-10-25 15:47:25 +13:00
parent b75c2a67f8
commit 88a5efaa05
10 changed files with 10442 additions and 5607 deletions

View File

@ -1,4 +1,23 @@
export function setEnabled(enabled) {
return {
type: 'SNAPCAST_SET_ENABLED',
enabled,
};
}
export function connect() {
return {
type: 'SNAPCAST_CONNECT',
};
}
export function disconnect() {
return {
type: 'SNAPCAST_DISCONNECT',
};
}
export function getServer() {
return {
type: 'SNAPCAST_GET_SERVER',
@ -130,4 +149,4 @@ export function streamsLoaded(streams, flush = false) {
streams,
flush,
};
}
}

View File

@ -7,23 +7,158 @@ const uiActions = require('../ui/actions');
const pusherActions = require('../pusher/actions');
const snapcastActions = require('./actions');
const SnapcastMiddleware = (function () {
// A snapcast request is an alias of the Pusher request
const request = (store, params = null, response_callback = null, error_callback = null) => {
store.dispatch(
pusherActions.request(
'snapcast',
params,
response_callback,
error_callback,
),
);
const SnapcastMiddleware = (function () {
let socket = null;
// requests pending
const deferredRequests = [];
// handle all manner of socket messages
const handleMessage = (ws, store, message) => {
if (store.getState().ui.log_snapcast) {
console.log('Snapcast log (incoming)', message);
}
// Pull our ID. JSON-RPC nests the ID under the error object,
// so make sure we handle that.
// TODO: Use this as our measure of a successful response vs error
let id = null;
if (message.id) {
id = message.id;
} else if (message.error && message.error.id) {
id = message.error.id;
}
// Response with request_id
if (id) {
// Response matches a pending request
if (deferredRequests[id] !== undefined) {
store.dispatch(uiActions.stopLoading(id));
// Response is an error
if (message.error !== undefined) {
deferredRequests[id].reject(message.error);
// Successful response
} else {
deferredRequests[id].resolve(message.result);
}
// Hmm, the response doesn't appear to be for us?
} else {
store.dispatch(coreActions.handleException(
'Snapcast: Response received with no matching request',
message,
));
}
// General broadcast received
} else {
// Broadcast of an error
if (message.error !== undefined) {
store.dispatch(coreActions.handleException(
`Snapcast: ${message.error.message}`,
message,
(message.error.data !== undefined && message.error.data.description !== undefined ? message.error.data.description : null),
));
} else {
switch (message.method) {
case 'connection_added':
store.dispatch(pusherActions.connectionAdded(message.params.connection));
break;
}
}
}
};
const request = (store, method, params = null) => new Promise((resolve, reject) => {
const id = helpers.generateGuid();
const message = {
jsonrpc: '2.0',
id,
method,
};
if (params) {
message.params = params;
}
if (store.getState().ui.log_pusher) {
console.log('Snapcast log (outgoing)', message);
}
socket.send(JSON.stringify(message));
store.dispatch(uiActions.startLoading(id, `snapcast_${method}`));
// Start our 30 second timeout
const timeout = setTimeout(
() => {
store.dispatch(uiActions.stopLoading(id));
reject({
id,
code: 32300,
message: 'Request timed out',
});
},
30000,
);
// add query to our deferred responses
deferredRequests[id] = {
resolve,
reject,
};
});
return (store) => (next) => (action) => {
const { snapcast } = store.getState();
switch (action.type) {
switch (action.type) {
case 'SNAPCAST_CONNECT':
if (socket != null) {
socket.close();
}
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.on((type, data) => handleMessage(socket, store, type, data));
break;
case 'SNAPCAST_CONNECTED':
if (store.getState().ui.allow_reporting) {
const hashed_hostname = sha256(window.location.hostname);
ReactGA.event({ category: 'Snapcast', action: 'Connected', label: hashed_hostname });
}
store.dispatch(uiActions.createNotification({ content: 'Snapcast connected' }));
next(action);
break;
case 'SNAPCAST_DISCONNECT':
if (socket != null) socket.close();
socket = null;
store.dispatch({ type: 'SNAPCAST_DISCONNECTED' });
break;
case 'SNAPCAST_DISCONNECTED':
store.dispatch(uiActions.createNotification({ type: 'bad', content: 'Snapcast disconnected' }));
helpers.setFavicon('favicon_error.png');
break;
case 'SNAPCAST_DEBUG':
request(socket, store, action.call, action.value)
.then((response) => {
store.dispatch({ type: 'DEBUG', response });
});
break;
case 'SNAPCAST_GET_SERVER':
request(
store,
@ -358,7 +493,7 @@ const SnapcastMiddleware = (function () {
var group = snapcast.groups[action.id];
var change = action.percent - action.old_percent;
for (const client_id of group.clients_ids) {
for (const client_id of group.clients_ids) {
// Apply the change proportionately to each client
var client = snapcast.clients[client_id];
const current_volume = client.volume;
@ -393,7 +528,7 @@ const SnapcastMiddleware = (function () {
// Drop the prefix
action.method = action.method.replace('snapcast_', '');
switch (action.method) {
switch (action.method) {
case 'Client.OnConnect':
store.dispatch(snapcastActions.clientLoaded(
{
@ -462,7 +597,7 @@ const SnapcastMiddleware = (function () {
default:
return next(action);
}
};
};
}());
export default SnapcastMiddleware;

View File

@ -1,9 +1,12 @@
export default function reducer(snapcast = {}, action) {
switch (action.type) {
console.log(action);
switch (action.type) {
case 'SNAPCAST_SET_ENABLED':
return { ...snapcast, enabled: action.enabled };
case 'SNAPCAST_SERVER_LOADED':
var server = { ...action.server };
return { ...snapcast, server };
return { ...snapcast, server: action.server };
case 'SNAPCAST_CLIENTS_LOADED':
if (action.flush) {