Lidarr/frontend/src/Components/SignalRConnector.js

393 lines
10 KiB
JavaScript
Raw Normal View History

import * as signalR from '@microsoft/signalr/dist/browser/signalr.js';
2017-09-04 02:20:56 +00:00
import PropTypes from 'prop-types';
import { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setAppValue, setVersion } from 'Store/Actions/appActions';
import { fetchArtist } from 'Store/Actions/artistActions';
2020-09-07 01:33:10 +00:00
import { removeItem, update, updateItem } from 'Store/Actions/baseActions';
import { fetchCommands, finishCommand, updateCommand } from 'Store/Actions/commandActions';
2017-09-04 02:20:56 +00:00
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
2020-02-09 19:15:43 +00:00
import { fetchRootFolders } from 'Store/Actions/settingsActions';
2020-09-07 01:33:10 +00:00
import { fetchHealth } from 'Store/Actions/systemActions';
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
import { repopulatePage } from 'Utilities/pagePopulator';
import titleCase from 'Utilities/String/titleCase';
2017-09-04 02:20:56 +00:00
function getHandlerName(name) {
name = titleCase(name);
name = name.replace('/', '');
return `handle${name}`;
}
2017-09-04 02:20:56 +00:00
function createMapStateToProps() {
return createSelector(
(state) => state.app.isReconnecting,
2017-10-07 06:47:28 +00:00
(state) => state.app.isDisconnected,
2017-09-04 02:20:56 +00:00
(state) => state.queue.paged.isPopulated,
2017-10-07 06:47:28 +00:00
(isReconnecting, isDisconnected, isQueuePopulated) => {
2017-09-04 02:20:56 +00:00
return {
isReconnecting,
2017-10-07 06:47:28 +00:00
isDisconnected,
2017-09-04 02:20:56 +00:00
isQueuePopulated
};
}
);
}
const mapDispatchToProps = {
dispatchFetchCommands: fetchCommands,
dispatchUpdateCommand: updateCommand,
dispatchFinishCommand: finishCommand,
dispatchSetAppValue: setAppValue,
dispatchSetVersion: setVersion,
dispatchUpdate: update,
dispatchUpdateItem: updateItem,
dispatchRemoveItem: removeItem,
dispatchFetchArtist: fetchArtist,
dispatchFetchHealth: fetchHealth,
dispatchFetchQueue: fetchQueue,
dispatchFetchQueueDetails: fetchQueueDetails,
dispatchFetchRootFolders: fetchRootFolders,
dispatchFetchTags: fetchTags,
dispatchFetchTagDetails: fetchTagDetails
2017-09-04 02:20:56 +00:00
};
function Logger(minimumLogLevel) {
this.minimumLogLevel = minimumLogLevel;
}
Logger.prototype.cleanse = function(message) {
const apikey = new RegExp(`access_token=${window.Lidarr.apiKey}`, 'g');
return message.replace(apikey, 'access_token=(removed)');
};
Logger.prototype.log = function(logLevel, message) {
// see https://github.com/aspnet/AspNetCore/blob/21c9e2cc954c10719878839cd3f766aca5f57b34/src/SignalR/clients/ts/signalr/src/Utils.ts#L147
if (logLevel >= this.minimumLogLevel) {
switch (logLevel) {
case signalR.LogLevel.Critical:
case signalR.LogLevel.Error:
console.error(`[signalR] ${signalR.LogLevel[logLevel]}: ${this.cleanse(message)}`);
break;
case signalR.LogLevel.Warning:
console.warn(`[signalR] ${signalR.LogLevel[logLevel]}: ${this.cleanse(message)}`);
break;
case signalR.LogLevel.Information:
console.info(`[signalR] ${signalR.LogLevel[logLevel]}: ${this.cleanse(message)}`);
break;
default:
// console.debug only goes to attached debuggers in Node, so we use console.log for Trace and Debug
console.log(`[signalR] ${signalR.LogLevel[logLevel]}: ${this.cleanse(message)}`);
break;
}
}
};
2017-09-04 02:20:56 +00:00
class SignalRConnector extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.connection = null;
2017-09-04 02:20:56 +00:00
}
componentDidMount() {
console.log('[signalR] starting');
const url = `${window.Lidarr.urlBase}/signalr/messages`;
2017-09-04 02:20:56 +00:00
this.connection = new signalR.HubConnectionBuilder()
.configureLogging(new Logger(signalR.LogLevel.Information))
.withUrl(`${url}?access_token=${window.Lidarr.apiKey}`)
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (retryContext) => {
if (retryContext.elapsedMilliseconds > 180000) {
this.props.dispatchSetAppValue({ isDisconnected: true });
}
return Math.min(retryContext.previousRetryCount, 10) * 1000;
}
})
.build();
this.connection.onreconnecting(this.onReconnecting);
this.connection.onreconnected(this.onReconnected);
this.connection.onclose(this.onClose);
2017-09-04 02:20:56 +00:00
this.connection.on('receiveMessage', this.onReceiveMessage);
2017-09-04 02:20:56 +00:00
this.connection.start().then(this.onStart, this.onStartFail);
2017-09-04 02:20:56 +00:00
}
componentWillUnmount() {
this.connection.stop();
this.connection = null;
2017-09-04 02:20:56 +00:00
}
//
// Control
handleMessage = (message) => {
const {
name,
body
} = message;
const handler = this[getHandlerName(name)];
2017-09-04 02:20:56 +00:00
if (body && body.action === 'updated' && !body.resource) {
console.error(`signalR: Null resource broadcast ignored for ${name}`);
return;
}
if (handler) {
handler(body);
return;
}
console.error(`signalR: Unable to find handler for ${name}`);
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
handleCalendar = (body) => {
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
2017-09-04 02:20:56 +00:00
section: 'calendar',
updateOnly: true,
2017-10-07 06:47:28 +00:00
...body.resource
});
2017-09-04 02:20:56 +00:00
}
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
handleCommand = (body) => {
if (body.action === 'sync') {
this.props.dispatchFetchCommands();
return;
}
2017-09-04 02:20:56 +00:00
const resource = body.resource;
const status = resource.status;
2017-09-04 02:20:56 +00:00
// Both sucessful and failed commands need to be
// completed, otherwise they spin until they timeout.
if (status === 'completed' || status === 'failed') {
this.props.dispatchFinishCommand(resource);
2017-09-04 02:20:56 +00:00
} else {
this.props.dispatchUpdateCommand(resource);
2017-09-04 02:20:56 +00:00
}
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
handleAlbum = (body) => {
2019-12-16 21:21:32 +00:00
const action = body.action;
const section = 'albums';
if (action === 'updated') {
this.props.dispatchUpdateItem({
2019-12-16 21:21:32 +00:00
section,
2017-09-04 02:20:56 +00:00
updateOnly: true,
2017-10-07 06:47:28 +00:00
...body.resource
});
2019-12-16 21:21:32 +00:00
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({
section,
id: body.resource.id
});
2017-09-04 02:20:56 +00:00
}
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
handleTrack = (body) => {
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
section: 'tracks',
updateOnly: true,
2017-10-07 06:47:28 +00:00
...body.resource
});
}
2021-12-24 18:18:14 +00:00
};
handleTrackfile = (body) => {
const section = 'trackFiles';
2017-09-04 02:20:56 +00:00
if (body.action === 'updated') {
this.props.dispatchUpdateItem({ section, ...body.resource });
} else if (body.action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: body.resource.id });
2017-09-04 02:20:56 +00:00
}
// Repopulate the page to handle recently imported file
repopulatePage('trackFileUpdated');
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
handleHealth = () => {
this.props.dispatchFetchHealth();
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
2017-10-07 22:38:31 +00:00
handleArtist = (body) => {
2017-09-04 02:20:56 +00:00
const action = body.action;
2017-10-07 22:38:31 +00:00
const section = 'artist';
2017-09-04 02:20:56 +00:00
if (action === 'updated') {
this.props.dispatchUpdateItem({ section, ...body.resource });
2017-09-04 02:20:56 +00:00
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: body.resource.id });
2017-09-04 02:20:56 +00:00
}
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
handleQueue = () => {
2017-09-04 02:20:56 +00:00
if (this.props.isQueuePopulated) {
this.props.dispatchFetchQueue();
2017-09-04 02:20:56 +00:00
}
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
handleQueueDetails = () => {
this.props.dispatchFetchQueueDetails();
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
handleQueueStatus = (body) => {
this.props.dispatchUpdate({ section: 'queue.status', data: body.resource });
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
handleVersion = (body) => {
const version = body.version;
2017-09-04 02:20:56 +00:00
this.props.dispatchSetVersion({ version });
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
handleWantedCutoff = (body) => {
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
2017-09-04 02:20:56 +00:00
section: 'cutoffUnmet',
updateOnly: true,
2017-10-07 06:47:28 +00:00
...body.resource
});
2017-09-04 02:20:56 +00:00
}
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
handleWantedMissing = (body) => {
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
2017-09-04 02:20:56 +00:00
section: 'missing',
updateOnly: true,
2017-10-07 06:47:28 +00:00
...body.resource
});
2017-09-04 02:20:56 +00:00
}
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
handleSystemTask = () => {
this.props.dispatchFetchCommands();
2021-12-24 18:18:14 +00:00
};
2020-02-09 19:15:43 +00:00
handleRootfolder = (body) => {
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
section: 'settings.rootFolders',
updateOnly: true,
...body.resource
});
}
2021-12-24 18:18:14 +00:00
};
handleTag = (body) => {
if (body.action === 'sync') {
this.props.dispatchFetchTags();
this.props.dispatchFetchTagDetails();
return;
}
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
//
// Listeners
onStartFail = (error) => {
console.error('[signalR] failed to connect');
console.error(error);
this.props.dispatchSetAppValue({
isConnected: false,
isReconnecting: false,
isDisconnected: false,
isRestarting: false
});
2021-12-24 18:18:14 +00:00
};
onStart = () => {
console.debug('[signalR] connected');
2017-09-04 02:20:56 +00:00
this.props.dispatchSetAppValue({
isConnected: true,
isReconnecting: false,
isDisconnected: false,
isRestarting: false
});
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
onReconnecting = () => {
this.props.dispatchSetAppValue({ isReconnecting: true });
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
onReconnected = () => {
const {
dispatchFetchCommands,
dispatchFetchArtist,
dispatchSetAppValue
} = this.props;
dispatchSetAppValue({
isConnected: true,
isReconnecting: false,
isDisconnected: false,
isRestarting: false
2017-09-04 02:20:56 +00:00
});
// Repopulate the page (if a repopulator is set) to ensure things
// are in sync after reconnecting.
dispatchFetchArtist();
dispatchFetchCommands();
repopulatePage();
2021-12-24 18:18:14 +00:00
};
2017-10-02 03:05:28 +00:00
onClose = () => {
console.debug('[signalR] connection closed');
2021-12-24 18:18:14 +00:00
};
onReceiveMessage = (message) => {
console.debug('[signalR] received', message.name, message.body);
2017-10-02 03:05:28 +00:00
this.handleMessage(message);
2021-12-24 18:18:14 +00:00
};
2017-09-04 02:20:56 +00:00
//
// Render
render() {
return null;
}
}
SignalRConnector.propTypes = {
isReconnecting: PropTypes.bool.isRequired,
2017-10-07 06:47:28 +00:00
isDisconnected: PropTypes.bool.isRequired,
2017-09-04 02:20:56 +00:00
isQueuePopulated: PropTypes.bool.isRequired,
dispatchFetchCommands: PropTypes.func.isRequired,
dispatchUpdateCommand: PropTypes.func.isRequired,
dispatchFinishCommand: PropTypes.func.isRequired,
dispatchSetAppValue: PropTypes.func.isRequired,
dispatchSetVersion: PropTypes.func.isRequired,
dispatchUpdate: PropTypes.func.isRequired,
dispatchUpdateItem: PropTypes.func.isRequired,
dispatchRemoveItem: PropTypes.func.isRequired,
dispatchFetchArtist: PropTypes.func.isRequired,
dispatchFetchHealth: PropTypes.func.isRequired,
dispatchFetchQueue: PropTypes.func.isRequired,
dispatchFetchQueueDetails: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchTagDetails: PropTypes.func.isRequired
2017-09-04 02:20:56 +00:00
};
export default connect(createMapStateToProps, mapDispatchToProps)(SignalRConnector);