New internal state for CORS requests, prototype server switcher modal

This commit is contained in:
James Barnsley
2021-10-09 16:40:27 +13:00
parent 2e8205bd26
commit e96344d4f2
15 changed files with 677 additions and 562 deletions

View File

@ -33,6 +33,7 @@ bind_to_address = 0.0.0.0
# serve a website from the doc_root location
# doc_root = /etc/default/snapweb
doc_root = /usr/share/snapserver/snapweb/
#
###############################################################################
@ -72,7 +73,8 @@ bind_to_address = 0.0.0.0
# stream URI of the PCM input stream, can be configured multiple times
# Format: TYPE://host/path?name=NAME[&codec=CODEC][&sampleformat=SAMPLEFORMAT]
#stream = pipe:///tmp/snapfifo?name=default
stream = pipe:///tmp/snapfifo?name=Mopidy&sampleformat=48000:16:2
stream = pipe:///tmp/snapfifo?name=Default&sampleformat=48000:16:2
stream = pipe:///tmp_secondary/snapfifo?name=Secondary&sampleformat=48000:16:2
# Default sample format
#sampleformat = 48000:16:2

View File

@ -13,6 +13,7 @@ import pickle
from pkg_resources import parse_version
from tornado.escape import json_encode
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
from mopidy.models.serialize import ModelJSONEncoder
from . import Extension
from .system import IrisSystemThread
@ -1150,6 +1151,34 @@ class IrisCore(pykka.ThreadingActor):
logger.debug(error)
return error
##
# Get a summary of this server's state.
# This is a collection of RPC-available requests, but those cannot be called via CORS, so this
# serves as a proxy for other Mopidy instances to be able to collect states.
##
async def get_server_state(self, *args, **kwargs):
callback = kwargs.get("callback", False)
request = kwargs.get("request", False)
current_track = self.core.playback.get_current_track().get()
# We dump the JSON to convert the Track to JSON, but we need to then loads back to JSON
# for the response.
response = json.loads(
json.dumps(
{
"playback_state": self.core.playback.get_state().get(),
"current_track": current_track,
},
cls=ModelJSONEncoder
)
)
if callback:
callback(response)
else:
return response
##
# Simple test method to debug access to system tasks
##

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -115,7 +115,7 @@
// Release details
// These are automatically injected to built HTML
var build = "1633159406";
var build = "1633750792";
var version = "3.59.1";
// Construct the script tag

View File

@ -53,6 +53,7 @@ import ImageZoom from './views/modals/ImageZoom';
import HotkeysInfo from './views/modals/HotkeysInfo';
import EditCommand from './views/modals/EditCommand';
import Reset from './views/modals/Reset';
import Servers from './views/modals/Servers';
import { scrollTo, isTouchDevice } from './util/helpers';
import storage from './util/storage';
@ -312,6 +313,7 @@ export class App extends React.Component {
<Route path="/hotkeys" component={HotkeysInfo} />
<Route path="/share-configuration" component={ShareConfiguration} />
<Route path="/reset" component={Reset} />
<Route path="/servers" component={Servers} />
<Route path="/edit-command/:id?" component={EditCommand} />
<Route path="/queue/radio" component={EditRadio} />

View File

@ -5,6 +5,7 @@ import Icon from './Icon';
import TextField from './Fields/TextField';
import { indexToArray } from '../util/arrays';
import { Button } from './Button';
import * as pusherActions from '../services/pusher/actions';
import * as mopidyActions from '../services/mopidy/actions';
import { iconFromKeyword } from '../util/helpers';
import { I18n } from '../locale';
@ -15,25 +16,25 @@ import {
} from '../util/format';
import Thumbnail from './Thumbnail';
const callServer = ({ endpoint, method, params }) => new Promise((resolve, reject) => {
const callServer = ({ server, method, params }) => new Promise((resolve, reject) => {
const body = {
jsonrpc: '2.0',
id: 1,
host: server.host,
port: server.port,
method,
params,
};
fetch(endpoint, {
method: 'POST',
mode: 'no-cors',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
fetch(
`http://192.168.1.201:6680/iris/http/get_from_peer`,
{
method: 'POST',
body: JSON.stringify(body),
},
body: JSON.stringify(body),
})
.then((response) => response.json())
.then(({ result }) => resolve(result))
.catch((error) => { console.debug({ error }); reject() });
)
.then((response) => response.json())
.then(({ result }) => resolve(result))
.catch(() => reject());
});
const Server = ({
@ -41,63 +42,29 @@ const Server = ({
current_server,
}) => {
if (!server) return null;
const { id } = server;
const { id, current_track, playback_state } = server;
const dispatch = useDispatch();
const [currentTrack, setCurrentTrack] = useState();
const [playState, setPlayState] = useState();
useEffect(() => {
const endpoint = `http://${server.host}:${server.port}/mopidy/rpc`;
callServer({
endpoint,
method: 'core.playback.get_current_track',
}).then(
(track) => {
callServer({
endpoint,
method: 'core.library.get_images',
params: {
uris: [track.uri],
},
}).then(({ [track.uri]: images }) => {
console.debug({ track })
setCurrentTrack({
...track,
images: images ? formatImages(digestMopidyImages(server, images)) : null,
});
});
},
() => setCurrentTrack(null),
);
callServer({
endpoint,
method: 'core.playback.get_state',
}).then(setPlayState, setPlayState);
if (id !== 'default') dispatch(mopidyActions.getServerState(id));
}, [id]);
const remove = () => {
dispatch(mopidyActions.removeServer(server.id));
};
const setAsCurrent = () => {
dispatch(mopidyActions.setCurrentServer(server));
};
const remove = () => dispatch(mopidyActions.removeServer(server.id));
const setAsCurrent = () => dispatch(mopidyActions.setCurrentServer(server));
return (
<div className="sub-tabs__content">
<label className="field">
<div className="name">
<I18n path="settings.servers.current_track" />
{playState ? ` (${playState})` : ''}
{playback_state ? ` (${playback_state})` : ''}
</div>
<div className="input">
<div style={{ display: 'flex', alignItems: 'center' }}>
<Thumbnail images={currentTrack?.images} size="small" />
<Thumbnail images={current_track?.images} size="small" />
<div style={{ paddingLeft: '1rem' }}>
<div>{currentTrack?.name}</div>
<div>{current_track?.name}</div>
<em>
<LinksSentence items={currentTrack?.artists} type="artist" nolinks />
<LinksSentence items={current_track?.artists} type="artist" nolinks />
</em>
</div>
</div>

View File

@ -1,5 +1,5 @@
import React from 'react';
import { connect, useSelector, useDispatch } from 'react-redux';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators } from 'redux';
import { withRouter } from 'react-router';
import Link from './Link';
@ -7,37 +7,10 @@ import Icon from './Icon';
import Dropzones from './Fields/Dropzones';
import PinList from './Fields/PinList';
import DropdownField from './Fields/DropdownField';
import Button from './Button';
import * as uiActions from '../services/ui/actions';
import * as mopidyActions from '../services/mopidy/actions';
import { I18n, i18n } from '../locale';
import { indexToArray } from '../util/arrays';
const ServerSwitcher = () => {
const servers = useSelector((state) => state.mopidy.servers);
const serversArray = indexToArray(servers);
const current_server = useSelector((state) => state.mopidy.current_server);
const dispatch = useDispatch();
if (!serversArray || serversArray.length <= 1) return null;
const onChange = (id) => dispatch(mopidyActions.setCurrentServer(servers[id]));
return (
<div style={{
width: '100%',
color: 'black',
}}>
<DropdownField
className="sidebar__menu__item"
icon="dns"
name="Server"
value={current_server}
valueAsLabel
options={serversArray.map((server) => ({ value: server.id, label: server.name }))}
handleChange={onChange}
/>
</div>
);
}
class Sidebar extends React.Component {
closeSidebar = () => {
@ -118,8 +91,6 @@ class Sidebar extends React.Component {
<aside className="sidebar">
<div className="sidebar__liner">
<nav className="sidebar__menu">
<ServerSwitcher />
<section className="sidebar__menu__section">
<Link to="/queue" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="play_arrow" type="material" />
@ -188,6 +159,10 @@ class Sidebar extends React.Component {
<I18n path="sidebar.settings" />
{this.renderStatusIcon()}
</Link>
<Link to="/servers" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="dns" type="material" />
<I18n path="sidebar.servers" />
</Link>
</section>
</nav>

View File

@ -171,6 +171,7 @@ sidebar:
tracks: Tracks
browse: Browse
settings: Settings
servers: Servers
not_connected: '%{name} not connected'
browser_offline: Browser offline
update_available: Update available
@ -610,3 +611,5 @@ modal:
snapcast_volume_up: Volume up for Snapcast client (where n is 1-9)
snapcast_volume_down: Volume down for Snapcast client (where n is 1-9)
snapcast_mute: Toggle mute for Snapcast client (where n is 1-9)
servers:
title: Server selector

View File

@ -42,6 +42,13 @@ export function setCurrentServer(server) {
};
}
export function getServerState(id) {
return {
type: 'MOPIDY_GET_SERVER_STATE',
id,
};
}
export function removeServer(id) {
return {
type: 'MOPIDY_REMOVE_SERVER',

View File

@ -478,6 +478,17 @@ const MopidyMiddleware = (function () {
);
break;
case 'MOPIDY_GET_SERVER_STATE': {
const server = store.getState().mopidy.servers[action.id];
fetch(`http://${server.host}:${server.port}/iris/http/get_server_state`)
.then((response) => response.json())
.then(({ result }) => {
store.dispatch(mopidyActions.updateServer({ ...server, ...result }));
});
break;
}
case 'MOPIDY_REMOVE_SERVER': {
const servers = { ...store.getState().mopidy.servers };
delete servers[action.id];

View File

@ -1,6 +1,10 @@
import ReactGA from 'react-ga';
import { uriType, generateGuid } from '../../util/helpers';
import { trackEvent } from '../../components/Trackable';
import {
digestMopidyImages,
formatImages,
} from '../../util/format';
const coreActions = require('../core/actions');
const uiActions = require('../ui/actions');
@ -8,8 +12,9 @@ const pusherActions = require('./actions');
const lastfmActions = require('../lastfm/actions');
const geniusActions = require('../genius/actions');
const spotifyActions = require('../spotify/actions');
const mopidyActions = require('../mopidy/actions');
const PusherMiddleware = (function () {
const PusherMiddleware = (function () {
// container for the actual websocket
let socket = null;

View File

@ -0,0 +1,70 @@
import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import Thumbnail from '../../components/Thumbnail';
import LinksSentence from '../../components/LinksSentence';
import Modal from './Modal';
import * as uiActions from '../../services/ui/actions';
import * as mopidyActions from '../../services/mopidy/actions';
import { i18n, I18n } from '../../locale';
import { indexToArray } from '../../util/arrays';
const Servers = () => {
const history = useHistory();
const dispatch = useDispatch();
const servers = indexToArray(useSelector((state) => state.mopidy.servers || {}));
useEffect(() => dispatch(uiActions.setWindowTitle(i18n('modal.servers.title'))), []);
useEffect(() => {
servers.forEach(({ id }) => {
dispatch(mopidyActions.getServerState(id));
});
}, []);
const onClick = (server) => {
dispatch(mopidyActions.setCurrentServer(server));
history.push('/queue');
}
return (
<Modal className="modal--servers">
<h1>
<I18n path="modal.servers.title" />
</h1>
<form>
<>
{servers.map((server) => {
const {
id,
name,
playback_state,
current_track,
} = server;
return (
<div key={id} onClick={() => onClick(server)}>
<h3>
{name}
{playback_state ? ` (${playback_state})` : ''}
</h3>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Thumbnail images={current_track?.images} size="small" />
<div style={{ paddingLeft: '1rem' }}>
<div>{current_track?.name}</div>
<em>
<LinksSentence items={current_track?.artists} type="artist" nolinks />
</em>
</div>
</div>
</div>
);
})}
</>
</form>
</Modal>
);
}
export default Servers;