Skip to content

Commit

Permalink
feat: add a simple express rest api to remotly control the player, th…
Browse files Browse the repository at this point in the history
…e settings and the window
  • Loading branch information
Charles Jacquin committed Mar 9, 2019
1 parent 7aa7d95 commit 671e2ef
Show file tree
Hide file tree
Showing 19 changed files with 390 additions and 39 deletions.
8 changes: 6 additions & 2 deletions app/containers/IpcContainer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { bindActionCreators } from 'redux';
import { ipcRenderer } from 'electron';
import * as PlayerActions from '../../actions/player';
import * as QueueActions from '../../actions/queue';
import * as SettingsActions from '../../actions/settings';

import { onNext, onPrevious, onPause, onPlayPause, onStop, onPlay, onSongChange} from '../../mpris';
import { onNext, onPrevious, onPause, onPlayPause, onStop, onPlay, onSongChange, onSettings, onVolume, onSeek } from '../../mpris';

class IpcContainer extends React.Component {
constructor(props) {
Expand All @@ -21,6 +22,9 @@ class IpcContainer extends React.Component {
ipcRenderer.on('playpause', event => onPlayPause(event, this.props.actions, this.props.player));
ipcRenderer.on('stop', event => onStop(event, this.props.actions));
ipcRenderer.on('play', event => onPlay(event, this.props.actions));
ipcRenderer.on('settings', (event, data) => onSettings(event, data, this.props.actions));
ipcRenderer.on('volume', (event, data) => onVolume(event, data, this.props.actions));
ipcRenderer.on('seek', (event, data) => onSeek(event, data, this.props.actions));
}

componentWillReceiveProps(nextProps){
Expand All @@ -44,7 +48,7 @@ function mapStateToProps(state) {

function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Object.assign({}, PlayerActions, QueueActions), dispatch)
actions: bindActionCreators(Object.assign({}, PlayerActions, QueueActions, SettingsActions), dispatch)
};
}

Expand Down
26 changes: 26 additions & 0 deletions app/mpris.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,32 @@ export function onPlay(event, actions) {
actions.startPlayback();
}

export function onSettings(event, data, actions) {
const key = Object.keys(data).pop();
const value = Object.values(data).pop();

switch (typeof value) {
case 'boolean':
actions.setBooleanOption(key, value);
break;
case 'number':
actions.setNumberOption(key, value);
break;
case 'string':
default:
actions.setStringOption(key, value);
break;
}
}

export function onVolume(event, data, actions) {
actions.updateVolume(data);
}

export function onSeek(event, data, actions) {
actions.updateSeek(data);
}

export function onSongChange(song) {
ipcRenderer.send('songChange', song);
}
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,13 @@
"dependencies": {
"billboard-top-100": "^2.0.8",
"bluebird": "^3.5.3",
"body-parser": "^1.18.3",
"cheerio": "^1.0.0-rc.2",
"electron-platform": "^1.2.0",
"electron-store": "^2.0.0",
"electron-timber": "^0.5.1",
"express": "^4.16.4",
"express-json-validator-middleware": "^1.2.3",
"fast-levenshtein": "^2.0.6",
"font-awesome": "^4.7.0",
"get-artist-title": "^1.1.1",
Expand Down
6 changes: 6 additions & 0 deletions server/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../.eslintrc.js",
"env": {
"node": true
}
}
71 changes: 71 additions & 0 deletions server/http/api/_schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import settings from '../../../app/constants/settings';

export const RESTRICTED_SETTINGS = [];
export const READONLY_SETTINGS = [];

export const getSettingsSchema = {
params: {
type: 'object',
required: ['option'],
properties: {
option: {
type: 'string',
enum: settings
.filter(({ name }) => !RESTRICTED_SETTINGS.includes(name))
.map(({ name }) => name)
}
}
}
};

export const updateSettingsSchema = {
params: {
type: 'object',
required: ['option'],
properties: {
option: {
type: 'string',
enum: settings
.filter(({ name }) => !READONLY_SETTINGS.includes(name))
.filter(({ name }) => !RESTRICTED_SETTINGS.includes(name))
.map(({ name }) => name)
}
}
},
body: {
type: 'object',
required: ['value'],
properties: {
value: {
type: ['string', 'boolean', 'number']
}
}
}
};

export const volumeSchema = {
body: {
type: 'object',
required: ['value'],
properties: {
value: {
type: 'number',
minimum: 0,
maximum: 100
}
}
}
};

export const seekSchema = {
body: {
type: 'object',
required: ['value'],
properties: {
value: {
type: 'number',
minimum: 0
}
}
}
};
3 changes: 3 additions & 0 deletions server/http/api/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './window';
export * from './settings';
export * from './player';
63 changes: 63 additions & 0 deletions server/http/api/player.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import express from 'express';
import { Validator } from 'express-json-validator-middleware';

import {
onNext,
onPrevious,
onPause,
onPlayPause,
onStop,
onPlay,
onVolume,
onSeek
} from '../../mpris';
import { volumeSchema, seekSchema } from './_schema';

const { validate } = new Validator({ allErrors: true });

export function playerRouter() {

const router = express.Router();

router.post('/next', (req, res) => {
onNext();
res.send();
});

router.post('/previous', (req, res) => {
onPrevious();
res.send();
});

router.post('/pause', (req, res) => {
onPause();
res.send();
});

router.post('/play-pause', (req, res) => {
onPlayPause();
res.send();
});

router.post('/stop', (req, res) => {
onStop();
res.send();
});

router.post('/play', (req, res) => {
onPlay();
res.send();
});

router.post('/volume', validate(volumeSchema), (req, res) => {
onVolume(req.body.value);
res.send();
});

router.post('/seek', validate(seekSchema), (req, res) => {
onSeek(req.body.value);
res.send();
});

return router;
}
45 changes: 45 additions & 0 deletions server/http/api/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import express from 'express';
import { Validator } from 'express-json-validator-middleware';

import { onSettings } from '../../mpris';
import { getOption, store } from '../../store';
import { getSettingsSchema, updateSettingsSchema, RESTRICTED_SETTINGS } from './_schema';
import settingsParams from '../../../app/constants/settings';

const { validate } = new Validator({ allErrors: true });

export function settingsRouter() {

const router = express.Router();

router.get('/', (req, res) => {
const settings = store.get('settings');
const filteredSettings = settingsParams
.filter(({ name }) => !RESTRICTED_SETTINGS.includes(name))
.reduce((acc, item) => ({
...acc,
[item.name]: settings[item.name] || item.default
}), {});


res.json(filteredSettings);
});

router
.route('/:option')
.get(
validate(getSettingsSchema),
(req, res) => {
res.send(getOption(req.params.option));
}
)
.post(
validate(updateSettingsSchema),
(req, res) => {
onSettings({ [req.params.option]: req.body.value });
res.send();
}
);

return router;
}
25 changes: 25 additions & 0 deletions server/http/api/window.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import express from 'express';
const { ipcMain } = require('electron');

export function windowRouter() {

const router = express.Router();

router.post('/quit', (req, res) => {
ipcMain.emit('close');
res.send();
});

router.post('/maximize', (req, res) => {
ipcMain.emit('maximize');
res.send();
});

router.post('/minimize', (req, res) => {
ipcMain.emit('minimize');
res.send();
});

return router;
}

30 changes: 30 additions & 0 deletions server/http/middlewares.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ValidationError } from 'express-json-validator-middleware';

const getValidationMessage = ({ validationErrors }) => {
if (validationErrors.params) {
const err = validationErrors.params.shift();
return `${err.dataPath} ${err.message} ${err.params.allowedValues.toString()}`;
} else {
return `request body ${validationErrors.body.shift().message}`;
}
};

export function errorMiddleware(logger) {
return (err, req, res, next) => {
if (err instanceof ValidationError) {
const message = getValidationMessage(err);

res.status(400).send(message);
next();
} else {
logger.error(err);
res.status(500).send('Internal Server Error');
}
};
}

export function notFoundMiddleware() {
return (req, res) => {
res.status(404).send('Not Found');
};
}
36 changes: 36 additions & 0 deletions server/http/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Logger from 'electron-timber';
import express from 'express';
import bodyParser from 'body-parser';

import { windowRouter, playerRouter, settingsRouter } from './api';
import { errorMiddleware, notFoundMiddleware } from './middlewares';

function runHttpServer({
log,
port = 3000,
host = '0.0.0.0',
prefix = '/nuclear'
}) {
const app = express();
const logger = log
? Logger.create({ name: 'http api' })
: { log: () => {}, error: () => {} };

return app
.use(bodyParser.urlencoded({ extended: false }))
.use(bodyParser.json())
.use(`${prefix}/window`, windowRouter())
.use(`${prefix}/player`, playerRouter())
.use(`${prefix}/settings`, settingsRouter())
.use(notFoundMiddleware())
.use(errorMiddleware(logger))
.listen(port, host, err => {
if (err) {
logger.error(err);
} else {
logger.log(`nuclear api available on port ${port}`);
}
});
}

module.exports = runHttpServer;
Loading

0 comments on commit 671e2ef

Please sign in to comment.