New internal state for CORS requests, prototype server switcher modal
This commit is contained in:
@ -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
|
||||
|
||||
@ -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
85
mopidy_iris/static/app.min.css
vendored
85
mopidy_iris/static/app.min.css
vendored
File diff suppressed because one or more lines are too long
135
mopidy_iris/static/app.min.js
vendored
135
mopidy_iris/static/app.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
70
src/js/views/modals/Servers.js
Normal file
70
src/js/views/modals/Servers.js
Normal 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;
|
||||
Reference in New Issue
Block a user