2019-10-28 21:30:08 +00:00
|
|
|
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';
|
2017-10-07 06:47:28 +00:00
|
|
|
import { repopulatePage } from 'Utilities/pagePopulator';
|
2018-08-08 00:57:15 +00:00
|
|
|
import titleCase from 'Utilities/String/titleCase';
|
2018-08-31 03:07:50 +00:00
|
|
|
import { fetchCommands, updateCommand, finishCommand } from 'Store/Actions/commandActions';
|
2017-09-04 02:20:56 +00:00
|
|
|
import { setAppValue, setVersion } from 'Store/Actions/appActions';
|
|
|
|
import { update, updateItem, removeItem } from 'Store/Actions/baseActions';
|
2019-07-21 02:11:10 +00:00
|
|
|
import { fetchArtist } from 'Store/Actions/artistActions';
|
2017-09-04 02:20:56 +00:00
|
|
|
import { fetchHealth } from 'Store/Actions/systemActions';
|
|
|
|
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
|
2020-02-09 19:15:43 +00:00
|
|
|
import { fetchRootFolders } from 'Store/Actions/settingsActions';
|
2018-10-09 00:56:05 +00:00
|
|
|
import { fetchTags, fetchTagDetails } from 'Store/Actions/tagActions';
|
2017-09-04 02:20:56 +00:00
|
|
|
|
2018-08-08 00:57:15 +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 = {
|
2018-08-31 03:07:50 +00:00
|
|
|
dispatchFetchCommands: fetchCommands,
|
|
|
|
dispatchUpdateCommand: updateCommand,
|
|
|
|
dispatchFinishCommand: finishCommand,
|
|
|
|
dispatchSetAppValue: setAppValue,
|
|
|
|
dispatchSetVersion: setVersion,
|
|
|
|
dispatchUpdate: update,
|
|
|
|
dispatchUpdateItem: updateItem,
|
|
|
|
dispatchRemoveItem: removeItem,
|
2019-07-21 02:11:10 +00:00
|
|
|
dispatchFetchArtist: fetchArtist,
|
2018-08-31 03:07:50 +00:00
|
|
|
dispatchFetchHealth: fetchHealth,
|
|
|
|
dispatchFetchQueue: fetchQueue,
|
2018-10-09 00:56:05 +00:00
|
|
|
dispatchFetchQueueDetails: fetchQueueDetails,
|
2019-02-23 22:39:11 +00:00
|
|
|
dispatchFetchRootFolders: fetchRootFolders,
|
2018-10-09 00:56:05 +00:00
|
|
|
dispatchFetchTags: fetchTags,
|
|
|
|
dispatchFetchTagDetails: fetchTagDetails
|
2017-09-04 02:20:56 +00:00
|
|
|
};
|
|
|
|
|
2019-10-28 21:30:08 +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);
|
|
|
|
|
2019-10-28 21:12:26 +00:00
|
|
|
this.connection = null;
|
2017-09-04 02:20:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
2019-10-28 21:12:26 +00:00
|
|
|
console.log('[signalR] starting');
|
|
|
|
|
|
|
|
const url = `${window.Lidarr.urlBase}/signalr/messages`;
|
2017-09-04 02:20:56 +00:00
|
|
|
|
2019-10-28 21:12:26 +00:00
|
|
|
this.connection = new signalR.HubConnectionBuilder()
|
2019-10-28 21:30:08 +00:00
|
|
|
.configureLogging(new Logger(signalR.LogLevel.Information))
|
2019-10-28 21:12:26 +00:00
|
|
|
.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();
|
2019-03-09 02:10:23 +00:00
|
|
|
|
2019-10-28 21:12:26 +00:00
|
|
|
this.connection.onreconnecting(this.onReconnecting);
|
|
|
|
this.connection.onreconnected(this.onReconnected);
|
|
|
|
this.connection.onclose(this.onClose);
|
2017-09-04 02:20:56 +00:00
|
|
|
|
2019-10-28 21:12:26 +00:00
|
|
|
this.connection.on('receiveMessage', this.onReceiveMessage);
|
2017-09-04 02:20:56 +00:00
|
|
|
|
2020-05-18 20:36:48 +00:00
|
|
|
this.connection.start().then(this.onStart, this.onStartFail);
|
2017-09-04 02:20:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
2019-10-28 21:12:26 +00:00
|
|
|
this.connection.stop();
|
|
|
|
this.connection = null;
|
2017-09-04 02:20:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Control
|
|
|
|
handleMessage = (message) => {
|
|
|
|
const {
|
|
|
|
name,
|
|
|
|
body
|
|
|
|
} = message;
|
|
|
|
|
2018-08-08 00:57:15 +00:00
|
|
|
const handler = this[getHandlerName(name)];
|
2017-09-04 02:20:56 +00:00
|
|
|
|
2018-08-08 00:57:15 +00:00
|
|
|
if (handler) {
|
|
|
|
handler(body);
|
2017-09-24 05:10:24 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-08-08 00:57:15 +00:00
|
|
|
console.error(`signalR: Unable to find handler for ${name}`);
|
2017-09-04 02:20:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
handleCalendar = (body) => {
|
|
|
|
if (body.action === 'updated') {
|
2018-08-31 03:07:50 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleCommand = (body) => {
|
2018-08-31 03:07:50 +00:00
|
|
|
if (body.action === 'sync') {
|
|
|
|
this.props.dispatchFetchCommands();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-09-04 02:20:56 +00:00
|
|
|
const resource = body.resource;
|
2018-08-31 03:07:50 +00:00
|
|
|
const status = resource.status;
|
2017-09-04 02:20:56 +00:00
|
|
|
|
2017-09-17 20:39:13 +00:00
|
|
|
// Both sucessful and failed commands need to be
|
|
|
|
// completed, otherwise they spin until they timeout.
|
|
|
|
|
2018-08-31 03:07:50 +00:00
|
|
|
if (status === 'completed' || status === 'failed') {
|
|
|
|
this.props.dispatchFinishCommand(resource);
|
2017-09-04 02:20:56 +00:00
|
|
|
} else {
|
2018-08-31 03:07:50 +00:00
|
|
|
this.props.dispatchUpdateCommand(resource);
|
2017-09-04 02:20:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-01 06:05:24 +00:00
|
|
|
handleAlbum = (body) => {
|
2019-12-16 21:21:32 +00:00
|
|
|
const action = body.action;
|
|
|
|
const section = 'albums';
|
|
|
|
|
|
|
|
if (action === 'updated') {
|
2018-08-31 03:07:50 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-24 05:10:24 +00:00
|
|
|
handleTrack = (body) => {
|
2018-02-16 04:03:44 +00:00
|
|
|
if (body.action === 'updated') {
|
2018-08-31 03:07:50 +00:00
|
|
|
this.props.dispatchUpdateItem({
|
2018-02-16 04:03:44 +00:00
|
|
|
section: 'tracks',
|
|
|
|
updateOnly: true,
|
2017-10-07 06:47:28 +00:00
|
|
|
...body.resource
|
|
|
|
});
|
2017-09-24 05:10:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-02 03:12:02 +00:00
|
|
|
handleTrackfile = (body) => {
|
2018-01-26 03:01:53 +00:00
|
|
|
const section = 'trackFiles';
|
|
|
|
|
2017-09-04 02:20:56 +00:00
|
|
|
if (body.action === 'updated') {
|
2018-08-31 03:07:50 +00:00
|
|
|
this.props.dispatchUpdateItem({ section, ...body.resource });
|
2018-01-26 03:01:53 +00:00
|
|
|
} else if (body.action === 'deleted') {
|
2018-08-31 03:07:50 +00:00
|
|
|
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
2017-09-04 02:20:56 +00:00
|
|
|
}
|
2019-08-25 15:49:30 +00:00
|
|
|
|
|
|
|
// Repopulate the page to handle recently imported file
|
|
|
|
repopulatePage('trackFileUpdated');
|
2017-09-04 02:20:56 +00:00
|
|
|
}
|
|
|
|
|
2018-08-08 00:57:15 +00:00
|
|
|
handleHealth = () => {
|
2018-08-31 03:07:50 +00:00
|
|
|
this.props.dispatchFetchHealth();
|
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') {
|
2018-08-31 03:07:50 +00:00
|
|
|
this.props.dispatchUpdateItem({ section, ...body.resource });
|
2017-09-04 02:20:56 +00:00
|
|
|
} else if (action === 'deleted') {
|
2018-08-31 03:07:50 +00:00
|
|
|
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
2017-09-04 02:20:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-08 00:57:15 +00:00
|
|
|
handleQueue = () => {
|
2017-09-04 02:20:56 +00:00
|
|
|
if (this.props.isQueuePopulated) {
|
2018-08-31 03:07:50 +00:00
|
|
|
this.props.dispatchFetchQueue();
|
2017-09-04 02:20:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-08 00:57:15 +00:00
|
|
|
handleQueueDetails = () => {
|
2018-08-31 03:07:50 +00:00
|
|
|
this.props.dispatchFetchQueueDetails();
|
2017-09-04 02:20:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
handleQueueStatus = (body) => {
|
2018-08-31 03:07:50 +00:00
|
|
|
this.props.dispatchUpdate({ section: 'queue.status', data: body.resource });
|
2017-09-04 02:20:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
handleVersion = (body) => {
|
2019-10-28 21:12:26 +00:00
|
|
|
const version = body.version;
|
2017-09-04 02:20:56 +00:00
|
|
|
|
2018-08-31 03:07:50 +00:00
|
|
|
this.props.dispatchSetVersion({ version });
|
2017-09-04 02:20:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
handleWantedCutoff = (body) => {
|
|
|
|
if (body.action === 'updated') {
|
2018-08-31 03:07:50 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleWantedMissing = (body) => {
|
|
|
|
if (body.action === 'updated') {
|
2018-08-31 03:07:50 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-08 00:57:15 +00:00
|
|
|
handleSystemTask = () => {
|
2020-07-26 20:27:30 +00:00
|
|
|
this.props.dispatchFetchCommands();
|
2018-08-08 00:57:15 +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
|
|
|
|
});
|
|
|
|
}
|
2019-02-23 22:39:11 +00:00
|
|
|
}
|
|
|
|
|
2018-10-09 00:56:05 +00:00
|
|
|
handleTag = (body) => {
|
|
|
|
if (body.action === 'sync') {
|
|
|
|
this.props.dispatchFetchTags();
|
|
|
|
this.props.dispatchFetchTagDetails();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-04 02:20:56 +00:00
|
|
|
//
|
|
|
|
// Listeners
|
|
|
|
|
2020-05-18 20:36:48 +00:00
|
|
|
onStartFail = (error) => {
|
|
|
|
console.error('[signalR] failed to connect');
|
|
|
|
console.error(error);
|
|
|
|
|
|
|
|
this.props.dispatchSetAppValue({
|
|
|
|
isConnected: false,
|
|
|
|
isReconnecting: false,
|
|
|
|
isDisconnected: false,
|
|
|
|
isRestarting: false
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
onStart = () => {
|
2019-10-28 21:12:26 +00:00
|
|
|
console.debug('[signalR] connected');
|
2017-09-04 02:20:56 +00:00
|
|
|
|
2019-10-28 21:12:26 +00:00
|
|
|
this.props.dispatchSetAppValue({
|
|
|
|
isConnected: true,
|
|
|
|
isReconnecting: false,
|
|
|
|
isDisconnected: false,
|
|
|
|
isRestarting: false
|
|
|
|
});
|
2017-09-04 02:20:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
onReconnecting = () => {
|
2019-10-28 21:12:26 +00:00
|
|
|
this.props.dispatchSetAppValue({ isReconnecting: true });
|
|
|
|
}
|
2017-09-04 02:20:56 +00:00
|
|
|
|
2019-10-28 21:12:26 +00:00
|
|
|
onReconnected = () => {
|
2017-11-16 02:24:33 +00:00
|
|
|
|
2019-10-28 21:12:26 +00:00
|
|
|
const {
|
|
|
|
dispatchFetchCommands,
|
|
|
|
dispatchFetchArtist,
|
|
|
|
dispatchSetAppValue
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
dispatchSetAppValue({
|
|
|
|
isConnected: true,
|
|
|
|
isReconnecting: false,
|
|
|
|
isDisconnected: false,
|
|
|
|
isRestarting: false
|
2017-09-04 02:20:56 +00:00
|
|
|
});
|
|
|
|
|
2019-10-28 21:12:26 +00:00
|
|
|
// Repopulate the page (if a repopulator is set) to ensure things
|
|
|
|
// are in sync after reconnecting.
|
|
|
|
dispatchFetchArtist();
|
|
|
|
dispatchFetchCommands();
|
|
|
|
repopulatePage();
|
|
|
|
}
|
2017-10-02 03:05:28 +00:00
|
|
|
|
2019-10-28 21:12:26 +00:00
|
|
|
onClose = () => {
|
|
|
|
console.debug('[signalR] connection closed');
|
|
|
|
}
|
2017-11-16 02:24:33 +00:00
|
|
|
|
2019-10-28 21:12:26 +00:00
|
|
|
onReceiveMessage = (message) => {
|
|
|
|
console.debug('[signalR] received', message.name, message.body);
|
2017-10-02 03:05:28 +00:00
|
|
|
|
2019-10-28 21:12:26 +00:00
|
|
|
this.handleMessage(message);
|
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,
|
2018-08-31 03:07:50 +00:00
|
|
|
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,
|
2019-07-21 02:11:10 +00:00
|
|
|
dispatchFetchArtist: PropTypes.func.isRequired,
|
2018-08-31 03:07:50 +00:00
|
|
|
dispatchFetchHealth: PropTypes.func.isRequired,
|
|
|
|
dispatchFetchQueue: PropTypes.func.isRequired,
|
2018-10-09 00:56:05 +00:00
|
|
|
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
2019-02-23 22:39:11 +00:00
|
|
|
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
2018-10-09 00:56:05 +00:00
|
|
|
dispatchFetchTags: PropTypes.func.isRequired,
|
|
|
|
dispatchFetchTagDetails: PropTypes.func.isRequired
|
2017-09-04 02:20:56 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
export default connect(createMapStateToProps, mapDispatchToProps)(SignalRConnector);
|