parent
6f871a1bfb
commit
c3f9cd12af
@ -1,422 +0,0 @@
|
|||||||
import * as signalR from '@microsoft/signalr/dist/browser/signalr.js';
|
|
||||||
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 { removeItem, update, updateItem } from 'Store/Actions/baseActions';
|
|
||||||
import { fetchCommands, finishCommand, updateCommand } from 'Store/Actions/commandActions';
|
|
||||||
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
|
|
||||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
|
||||||
import { fetchSeries } from 'Store/Actions/seriesActions';
|
|
||||||
import { fetchQualityDefinitions } from 'Store/Actions/settingsActions';
|
|
||||||
import { fetchHealth } from 'Store/Actions/systemActions';
|
|
||||||
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
|
|
||||||
import { repopulatePage } from 'Utilities/pagePopulator';
|
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
|
||||||
|
|
||||||
function getHandlerName(name) {
|
|
||||||
name = titleCase(name);
|
|
||||||
name = name.replace('/', '');
|
|
||||||
|
|
||||||
return `handle${name}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.app.isReconnecting,
|
|
||||||
(state) => state.app.isDisconnected,
|
|
||||||
(state) => state.queue.paged.isPopulated,
|
|
||||||
(isReconnecting, isDisconnected, isQueuePopulated) => {
|
|
||||||
return {
|
|
||||||
isReconnecting,
|
|
||||||
isDisconnected,
|
|
||||||
isQueuePopulated
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchCommands: fetchCommands,
|
|
||||||
dispatchUpdateCommand: updateCommand,
|
|
||||||
dispatchFinishCommand: finishCommand,
|
|
||||||
dispatchSetAppValue: setAppValue,
|
|
||||||
dispatchSetVersion: setVersion,
|
|
||||||
dispatchUpdate: update,
|
|
||||||
dispatchUpdateItem: updateItem,
|
|
||||||
dispatchRemoveItem: removeItem,
|
|
||||||
dispatchFetchHealth: fetchHealth,
|
|
||||||
dispatchFetchQualityDefinitions: fetchQualityDefinitions,
|
|
||||||
dispatchFetchQueue: fetchQueue,
|
|
||||||
dispatchFetchQueueDetails: fetchQueueDetails,
|
|
||||||
dispatchFetchRootFolders: fetchRootFolders,
|
|
||||||
dispatchFetchSeries: fetchSeries,
|
|
||||||
dispatchFetchTags: fetchTags,
|
|
||||||
dispatchFetchTagDetails: fetchTagDetails
|
|
||||||
};
|
|
||||||
|
|
||||||
function Logger(minimumLogLevel) {
|
|
||||||
this.minimumLogLevel = minimumLogLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.prototype.cleanse = function(message) {
|
|
||||||
const apikey = new RegExp(`access_token=${encodeURIComponent(window.Sonarr.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class SignalRConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.connection = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
console.log('[signalR] starting');
|
|
||||||
|
|
||||||
const url = `${window.Sonarr.urlBase}/signalr/messages`;
|
|
||||||
|
|
||||||
this.connection = new signalR.HubConnectionBuilder()
|
|
||||||
.configureLogging(new Logger(signalR.LogLevel.Information))
|
|
||||||
.withUrl(`${url}?access_token=${encodeURIComponent(window.Sonarr.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);
|
|
||||||
|
|
||||||
this.connection.on('receiveMessage', this.onReceiveMessage);
|
|
||||||
|
|
||||||
this.connection.start().then(this.onStart, this.onStartFail);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.connection.stop();
|
|
||||||
this.connection = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
handleMessage = (message) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
body
|
|
||||||
} = message;
|
|
||||||
|
|
||||||
const handler = this[getHandlerName(name)];
|
|
||||||
|
|
||||||
if (handler) {
|
|
||||||
handler(body);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(`signalR: Unable to find handler for ${name}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleCalendar = (body) => {
|
|
||||||
if (body.action === 'updated') {
|
|
||||||
this.props.dispatchUpdateItem({
|
|
||||||
section: 'calendar',
|
|
||||||
updateOnly: true,
|
|
||||||
...body.resource
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleCommand = (body) => {
|
|
||||||
if (body.action === 'sync') {
|
|
||||||
this.props.dispatchFetchCommands();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resource = body.resource;
|
|
||||||
const status = resource.status;
|
|
||||||
|
|
||||||
// Both successful and failed commands need to be
|
|
||||||
// completed, otherwise they spin until they time out.
|
|
||||||
|
|
||||||
if (status === 'completed' || status === 'failed') {
|
|
||||||
this.props.dispatchFinishCommand(resource);
|
|
||||||
} else {
|
|
||||||
this.props.dispatchUpdateCommand(resource);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleEpisode = (body) => {
|
|
||||||
if (body.action === 'updated') {
|
|
||||||
this.props.dispatchUpdateItem({
|
|
||||||
section: 'episodes',
|
|
||||||
updateOnly: true,
|
|
||||||
...body.resource
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleEpisodefile = (body) => {
|
|
||||||
const section = 'episodeFiles';
|
|
||||||
|
|
||||||
if (body.action === 'updated') {
|
|
||||||
this.props.dispatchUpdateItem({ section, ...body.resource });
|
|
||||||
|
|
||||||
// Repopulate the page to handle recently imported file
|
|
||||||
repopulatePage('episodeFileUpdated');
|
|
||||||
} else if (body.action === 'deleted') {
|
|
||||||
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
|
||||||
|
|
||||||
repopulatePage('episodeFileDeleted');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleDownloadclient = ({ action, resource }) => {
|
|
||||||
const section = 'settings.downloadClients';
|
|
||||||
|
|
||||||
if (action === 'created' || action === 'updated') {
|
|
||||||
this.props.dispatchUpdateItem({ section, ...resource });
|
|
||||||
} else if (action === 'deleted') {
|
|
||||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleHealth = () => {
|
|
||||||
this.props.dispatchFetchHealth();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleImportlist = ({ action, resource }) => {
|
|
||||||
const section = 'settings.importLists';
|
|
||||||
|
|
||||||
if (action === 'created' || action === 'updated') {
|
|
||||||
this.props.dispatchUpdateItem({ section, ...resource });
|
|
||||||
} else if (action === 'deleted') {
|
|
||||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleIndexer = ({ action, resource }) => {
|
|
||||||
const section = 'settings.indexers';
|
|
||||||
|
|
||||||
if (action === 'created' || action === 'updated') {
|
|
||||||
this.props.dispatchUpdateItem({ section, ...resource });
|
|
||||||
} else if (action === 'deleted') {
|
|
||||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMetadata = ({ action, resource }) => {
|
|
||||||
const section = 'settings.metadata';
|
|
||||||
|
|
||||||
if (action === 'updated') {
|
|
||||||
this.props.dispatchUpdateItem({ section, ...resource });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleNotification = ({ action, resource }) => {
|
|
||||||
const section = 'settings.notifications';
|
|
||||||
|
|
||||||
if (action === 'created' || action === 'updated') {
|
|
||||||
this.props.dispatchUpdateItem({ section, ...resource });
|
|
||||||
} else if (action === 'deleted') {
|
|
||||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSeries = (body) => {
|
|
||||||
const action = body.action;
|
|
||||||
const section = 'series';
|
|
||||||
|
|
||||||
if (action === 'updated') {
|
|
||||||
this.props.dispatchUpdateItem({ section, ...body.resource });
|
|
||||||
|
|
||||||
repopulatePage('seriesUpdated');
|
|
||||||
} else if (action === 'deleted') {
|
|
||||||
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleQualitydefinition = () => {
|
|
||||||
this.props.dispatchFetchQualityDefinitions();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleQueue = () => {
|
|
||||||
if (this.props.isQueuePopulated) {
|
|
||||||
this.props.dispatchFetchQueue();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleQueueDetails = () => {
|
|
||||||
this.props.dispatchFetchQueueDetails();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleQueueStatus = (body) => {
|
|
||||||
this.props.dispatchUpdate({ section: 'queue.status', data: body.resource });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleVersion = (body) => {
|
|
||||||
const version = body.version;
|
|
||||||
|
|
||||||
this.props.dispatchSetVersion({ version });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleWantedCutoff = (body) => {
|
|
||||||
if (body.action === 'updated') {
|
|
||||||
this.props.dispatchUpdateItem({
|
|
||||||
section: 'wanted.cutoffUnmet',
|
|
||||||
updateOnly: true,
|
|
||||||
...body.resource
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleWantedMissing = (body) => {
|
|
||||||
if (body.action === 'updated') {
|
|
||||||
this.props.dispatchUpdateItem({
|
|
||||||
section: 'wanted.missing',
|
|
||||||
updateOnly: true,
|
|
||||||
...body.resource
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSystemTask = () => {
|
|
||||||
this.props.dispatchFetchCommands();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleRootfolder = () => {
|
|
||||||
this.props.dispatchFetchRootFolders();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleTag = (body) => {
|
|
||||||
if (body.action === 'sync') {
|
|
||||||
this.props.dispatchFetchTags();
|
|
||||||
this.props.dispatchFetchTagDetails();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onStartFail = (error) => {
|
|
||||||
console.error('[signalR] failed to connect');
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
this.props.dispatchSetAppValue({
|
|
||||||
isConnected: false,
|
|
||||||
isReconnecting: false,
|
|
||||||
isDisconnected: false,
|
|
||||||
isRestarting: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onStart = () => {
|
|
||||||
console.debug('[signalR] connected');
|
|
||||||
|
|
||||||
this.props.dispatchSetAppValue({
|
|
||||||
isConnected: true,
|
|
||||||
isReconnecting: false,
|
|
||||||
isDisconnected: false,
|
|
||||||
isRestarting: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onReconnecting = () => {
|
|
||||||
this.props.dispatchSetAppValue({ isReconnecting: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onReconnected = () => {
|
|
||||||
|
|
||||||
const {
|
|
||||||
dispatchFetchCommands,
|
|
||||||
dispatchFetchSeries,
|
|
||||||
dispatchSetAppValue
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
dispatchSetAppValue({
|
|
||||||
isConnected: true,
|
|
||||||
isReconnecting: false,
|
|
||||||
isDisconnected: false,
|
|
||||||
isRestarting: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Repopulate the page (if a repopulator is set) to ensure things
|
|
||||||
// are in sync after reconnecting.
|
|
||||||
dispatchFetchSeries();
|
|
||||||
dispatchFetchCommands();
|
|
||||||
repopulatePage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onClose = () => {
|
|
||||||
console.debug('[signalR] connection closed');
|
|
||||||
};
|
|
||||||
|
|
||||||
onReceiveMessage = (message) => {
|
|
||||||
console.debug('[signalR] received', message.name, message.body);
|
|
||||||
|
|
||||||
this.handleMessage(message);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SignalRConnector.propTypes = {
|
|
||||||
isReconnecting: PropTypes.bool.isRequired,
|
|
||||||
isDisconnected: PropTypes.bool.isRequired,
|
|
||||||
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,
|
|
||||||
dispatchFetchHealth: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchQualityDefinitions: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchQueue: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchSeries: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchTags: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchTagDetails: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(SignalRConnector);
|
|
@ -0,0 +1,358 @@
|
|||||||
|
import signalR, { HubConnection } from '@microsoft/signalr';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Command from 'Commands/Command';
|
||||||
|
import { setAppValue, setVersion } from 'Store/Actions/appActions';
|
||||||
|
import { removeItem, update, updateItem } from 'Store/Actions/baseActions';
|
||||||
|
import {
|
||||||
|
fetchCommands,
|
||||||
|
finishCommand,
|
||||||
|
updateCommand,
|
||||||
|
} from 'Store/Actions/commandActions';
|
||||||
|
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||||
|
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
|
import { fetchSeries } from 'Store/Actions/seriesActions';
|
||||||
|
import { fetchQualityDefinitions } from 'Store/Actions/settingsActions';
|
||||||
|
import { fetchHealth } from 'Store/Actions/systemActions';
|
||||||
|
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
|
||||||
|
import { repopulatePage } from 'Utilities/pagePopulator';
|
||||||
|
import SignalRLogger from 'Utilities/SignalRLogger';
|
||||||
|
|
||||||
|
type SignalRAction = 'sync' | 'created' | 'updated' | 'deleted';
|
||||||
|
|
||||||
|
interface SignalRMessage {
|
||||||
|
name: string;
|
||||||
|
body: {
|
||||||
|
action: SignalRAction;
|
||||||
|
resource: ModelBase;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function SignalRListener() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const isQueuePopulated = useSelector(
|
||||||
|
(state: AppState) => state.queue.paged.isPopulated
|
||||||
|
);
|
||||||
|
|
||||||
|
const connection = useRef<HubConnection | null>(null);
|
||||||
|
|
||||||
|
const handleStartFail = useRef((error: unknown) => {
|
||||||
|
console.error('[signalR] failed to connect');
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
setAppValue({
|
||||||
|
isConnected: false,
|
||||||
|
isReconnecting: false,
|
||||||
|
isDisconnected: false,
|
||||||
|
isRestarting: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleStart = useRef(() => {
|
||||||
|
console.debug('[signalR] connected');
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
setAppValue({
|
||||||
|
isConnected: true,
|
||||||
|
isReconnecting: false,
|
||||||
|
isDisconnected: false,
|
||||||
|
isRestarting: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleReconnecting = useRef(() => {
|
||||||
|
dispatch(setAppValue({ isReconnecting: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleReconnected = useRef(() => {
|
||||||
|
dispatch(
|
||||||
|
setAppValue({
|
||||||
|
isConnected: true,
|
||||||
|
isReconnecting: false,
|
||||||
|
isDisconnected: false,
|
||||||
|
isRestarting: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Repopulate the page (if a repopulator is set) to ensure things
|
||||||
|
// are in sync after reconnecting.
|
||||||
|
dispatch(fetchSeries());
|
||||||
|
dispatch(fetchCommands());
|
||||||
|
repopulatePage();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = useRef(() => {
|
||||||
|
console.debug('[signalR] connection closed');
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleReceiveMessage = useRef((message: SignalRMessage) => {
|
||||||
|
console.debug('[signalR] received', message.name, message.body);
|
||||||
|
|
||||||
|
const { name, body } = message;
|
||||||
|
|
||||||
|
if (name === 'calendar') {
|
||||||
|
if (body.action === 'updated') {
|
||||||
|
dispatch(
|
||||||
|
updateItem({
|
||||||
|
section: 'calendar',
|
||||||
|
updateOnly: true,
|
||||||
|
...body.resource,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'command') {
|
||||||
|
if (body.action === 'sync') {
|
||||||
|
dispatch(fetchCommands());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resource = body.resource as Command;
|
||||||
|
const status = resource.status;
|
||||||
|
|
||||||
|
// Both successful and failed commands need to be
|
||||||
|
// completed, otherwise they spin until they time out.
|
||||||
|
|
||||||
|
if (status === 'completed' || status === 'failed') {
|
||||||
|
dispatch(finishCommand(resource));
|
||||||
|
} else {
|
||||||
|
dispatch(updateCommand(resource));
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'downloadclient') {
|
||||||
|
const section = 'settings.downloadClients';
|
||||||
|
|
||||||
|
if (body.action === 'created' || body.action === 'updated') {
|
||||||
|
dispatch(updateItem({ section, ...body.resource }));
|
||||||
|
} else if (body.action === 'deleted') {
|
||||||
|
dispatch(removeItem({ section, id: body.resource.id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episode') {
|
||||||
|
if (body.action === 'updated') {
|
||||||
|
dispatch(
|
||||||
|
updateItem({
|
||||||
|
section: 'episodes',
|
||||||
|
updateOnly: true,
|
||||||
|
...body.resource,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episodefile') {
|
||||||
|
const section = 'episodeFiles';
|
||||||
|
|
||||||
|
if (body.action === 'updated') {
|
||||||
|
dispatch(updateItem({ section, ...body.resource }));
|
||||||
|
|
||||||
|
// Repopulate the page to handle recently imported file
|
||||||
|
repopulatePage('episodeFileUpdated');
|
||||||
|
} else if (body.action === 'deleted') {
|
||||||
|
dispatch(removeItem({ section, id: body.resource.id }));
|
||||||
|
|
||||||
|
repopulatePage('episodeFileDeleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'health') {
|
||||||
|
dispatch(fetchHealth());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'importlist') {
|
||||||
|
const section = 'settings.importLists';
|
||||||
|
|
||||||
|
if (body.action === 'created' || body.action === 'updated') {
|
||||||
|
dispatch(updateItem({ section, ...body.resource }));
|
||||||
|
} else if (body.action === 'deleted') {
|
||||||
|
dispatch(removeItem({ section, id: body.resource.id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
const section = 'settings.indexers';
|
||||||
|
|
||||||
|
if (body.action === 'created' || body.action === 'updated') {
|
||||||
|
dispatch(updateItem({ section, ...body.resource }));
|
||||||
|
} else if (body.action === 'deleted') {
|
||||||
|
dispatch(removeItem({ section, id: body.resource.id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'metadata') {
|
||||||
|
const section = 'settings.metadata';
|
||||||
|
|
||||||
|
if (body.action === 'updated') {
|
||||||
|
dispatch(updateItem({ section, ...body.resource }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'notification') {
|
||||||
|
const section = 'settings.notifications';
|
||||||
|
|
||||||
|
if (body.action === 'created' || body.action === 'updated') {
|
||||||
|
dispatch(updateItem({ section, ...body.resource }));
|
||||||
|
} else if (body.action === 'deleted') {
|
||||||
|
dispatch(removeItem({ section, id: body.resource.id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'qualitydefinition') {
|
||||||
|
dispatch(fetchQualityDefinitions());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'queue') {
|
||||||
|
if (isQueuePopulated) {
|
||||||
|
dispatch(fetchQueue());
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'queue/details') {
|
||||||
|
dispatch(fetchQueueDetails());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'queue/status') {
|
||||||
|
dispatch(update({ section: 'queue.status', data: body.resource }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'rootfolder') {
|
||||||
|
dispatch(fetchRootFolders());
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'series') {
|
||||||
|
if (body.action === 'updated') {
|
||||||
|
dispatch(updateItem({ section: 'series', ...body.resource }));
|
||||||
|
|
||||||
|
repopulatePage('seriesUpdated');
|
||||||
|
} else if (body.action === 'deleted') {
|
||||||
|
dispatch(removeItem({ section: 'series', id: body.resource.id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'system/task') {
|
||||||
|
dispatch(fetchCommands());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'tag') {
|
||||||
|
if (body.action === 'sync') {
|
||||||
|
dispatch(fetchTags());
|
||||||
|
dispatch(fetchTagDetails());
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'version') {
|
||||||
|
dispatch(setVersion({ version: body.version }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'wanted/cutoff') {
|
||||||
|
if (body.action === 'updated') {
|
||||||
|
dispatch(
|
||||||
|
updateItem({
|
||||||
|
section: 'wanted.cutoffUnmet',
|
||||||
|
updateOnly: true,
|
||||||
|
...body.resource,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'wanted/missing') {
|
||||||
|
if (body.action === 'updated') {
|
||||||
|
dispatch(
|
||||||
|
updateItem({
|
||||||
|
section: 'wanted.missing',
|
||||||
|
updateOnly: true,
|
||||||
|
...body.resource,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`signalR: Unable to find handler for ${name}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('[signalR] starting');
|
||||||
|
|
||||||
|
const url = `${window.Sonarr.urlBase}/signalr/messages`;
|
||||||
|
|
||||||
|
connection.current = new signalR.HubConnectionBuilder()
|
||||||
|
.configureLogging(new SignalRLogger(signalR.LogLevel.Information))
|
||||||
|
.withUrl(
|
||||||
|
`${url}?access_token=${encodeURIComponent(window.Sonarr.apiKey)}`
|
||||||
|
)
|
||||||
|
.withAutomaticReconnect({
|
||||||
|
nextRetryDelayInMilliseconds: (retryContext) => {
|
||||||
|
if (retryContext.elapsedMilliseconds > 180000) {
|
||||||
|
dispatch(setAppValue({ isDisconnected: true }));
|
||||||
|
}
|
||||||
|
return Math.min(retryContext.previousRetryCount, 10) * 1000;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
connection.current.onreconnecting(handleReconnecting.current);
|
||||||
|
connection.current.onreconnected(handleReconnected.current);
|
||||||
|
connection.current.onclose(handleClose.current);
|
||||||
|
|
||||||
|
connection.current.on('receiveMessage', handleReceiveMessage.current);
|
||||||
|
|
||||||
|
connection.current
|
||||||
|
.start()
|
||||||
|
.then(handleStart.current, handleStartFail.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
connection.current?.stop();
|
||||||
|
connection.current = null;
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignalRListener;
|
@ -0,0 +1,47 @@
|
|||||||
|
import { LogLevel } from '@microsoft/signalr';
|
||||||
|
|
||||||
|
export default class SignalRLogger {
|
||||||
|
private _minimumLogLevel = LogLevel.Information;
|
||||||
|
|
||||||
|
constructor(minimumLogLevel: LogLevel) {
|
||||||
|
this._minimumLogLevel = minimumLogLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public log(logLevel: LogLevel, message: string) {
|
||||||
|
// see https://github.com/aspnet/AspNetCore/blob/21c9e2cc954c10719878839cd3f766aca5f57b34/src/SignalR/clients/ts/signalr/src/Utils.ts#L147
|
||||||
|
if (logLevel >= this._minimumLogLevel) {
|
||||||
|
switch (logLevel) {
|
||||||
|
case LogLevel.Critical:
|
||||||
|
case LogLevel.Error:
|
||||||
|
console.error(
|
||||||
|
`[signalR] ${LogLevel[logLevel]}: ${this._cleanse(message)}`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case LogLevel.Warning:
|
||||||
|
console.warn(
|
||||||
|
`[signalR] ${LogLevel[logLevel]}: ${this._cleanse(message)}`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case LogLevel.Information:
|
||||||
|
console.info(
|
||||||
|
`[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] ${LogLevel[logLevel]}: ${this._cleanse(message)}`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _cleanse(message: string) {
|
||||||
|
const apikey = new RegExp(
|
||||||
|
`access_token=${encodeURIComponent(window.Sonarr.apiKey)}`,
|
||||||
|
'g'
|
||||||
|
);
|
||||||
|
return message.replace(apikey, 'access_token=(removed)');
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue