mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: Project Aphrodite
This commit is contained in:
351
frontend/src/Components/SignalRConnector.js
Normal file
351
frontend/src/Components/SignalRConnector.js
Normal file
@@ -0,0 +1,351 @@
|
||||
import $ from 'jquery';
|
||||
import 'signalr';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { repopulatePage } from 'Utilities/pagePopulator';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import { fetchCommands, updateCommand, finishCommand } from 'Store/Actions/commandActions';
|
||||
import { setAppValue, setVersion } from 'Store/Actions/appActions';
|
||||
import { update, updateItem, removeItem } from 'Store/Actions/baseActions';
|
||||
import { fetchHealth } from 'Store/Actions/systemActions';
|
||||
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchTags, fetchTagDetails } from 'Store/Actions/tagActions';
|
||||
|
||||
function getState(status) {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return 'connecting';
|
||||
case 1:
|
||||
return 'connected';
|
||||
case 2:
|
||||
return 'reconnecting';
|
||||
case 4:
|
||||
return 'disconnected';
|
||||
default:
|
||||
throw new Error(`invalid status ${status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function isAppDisconnected(disconnectedTime) {
|
||||
if (!disconnectedTime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Math.floor(new Date().getTime() / 1000) - disconnectedTime > 180;
|
||||
}
|
||||
|
||||
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,
|
||||
dispatchFetchQueue: fetchQueue,
|
||||
dispatchFetchQueueDetails: fetchQueueDetails,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchFetchTags: fetchTags,
|
||||
dispatchFetchTagDetails: fetchTagDetails
|
||||
};
|
||||
|
||||
class SignalRConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.signalRconnectionOptions = { transport: ['webSockets', 'longPolling'] };
|
||||
this.signalRconnection = null;
|
||||
this.retryInterval = 1;
|
||||
this.retryTimeoutId = null;
|
||||
this.disconnectedTime = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
console.log('Starting signalR');
|
||||
|
||||
this.signalRconnection = $.connection('/signalr', { apiKey: window.Radarr.apiKey });
|
||||
|
||||
this.signalRconnection.stateChanged(this.onStateChanged);
|
||||
this.signalRconnection.received(this.onReceived);
|
||||
this.signalRconnection.reconnecting(this.onReconnecting);
|
||||
this.signalRconnection.disconnected(this.onDisconnected);
|
||||
|
||||
this.signalRconnection.start(this.signalRconnectionOptions);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.retryTimeoutId) {
|
||||
this.retryTimeoutId = clearTimeout(this.retryTimeoutId);
|
||||
}
|
||||
|
||||
this.signalRconnection.stop();
|
||||
this.signalRconnection = null;
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
retryConnection = () => {
|
||||
if (isAppDisconnected(this.disconnectedTime)) {
|
||||
this.setState({
|
||||
isDisconnected: true
|
||||
});
|
||||
}
|
||||
|
||||
this.retryTimeoutId = setTimeout(() => {
|
||||
if (!this.signalRconnection) {
|
||||
console.error('signalR: Connection was disposed');
|
||||
return;
|
||||
}
|
||||
|
||||
this.signalRconnection.start(this.signalRconnectionOptions);
|
||||
this.retryInterval = Math.min(this.retryInterval + 1, 10);
|
||||
}, this.retryInterval * 1000);
|
||||
}
|
||||
|
||||
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 sucessful and failed commands need to be
|
||||
// completed, otherwise they spin until they timeout.
|
||||
|
||||
if (status === 'completed' || status === 'failed') {
|
||||
this.props.dispatchFinishCommand(resource);
|
||||
} else {
|
||||
this.props.dispatchUpdateCommand(resource);
|
||||
}
|
||||
}
|
||||
|
||||
handleMoviefile = (body) => {
|
||||
const section = 'movieFiles';
|
||||
|
||||
if (body.action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...body.resource });
|
||||
|
||||
// Repopulate the page to handle recently imported file
|
||||
repopulatePage('movieFileUpdated');
|
||||
} else if (body.action === 'deleted') {
|
||||
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
||||
}
|
||||
}
|
||||
|
||||
handleHealth = () => {
|
||||
this.props.dispatchFetchHealth();
|
||||
}
|
||||
|
||||
handleMovie = (body) => {
|
||||
const action = body.action;
|
||||
const section = 'movies';
|
||||
|
||||
if (action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...body.resource });
|
||||
} else if (action === 'deleted') {
|
||||
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
||||
}
|
||||
}
|
||||
|
||||
handleQueue = () => {
|
||||
if (this.props.isQueuePopulated) {
|
||||
this.props.dispatchFetchQueue();
|
||||
}
|
||||
}
|
||||
|
||||
handleQueueDetails = () => {
|
||||
this.props.dispatchFetchQueueDetails();
|
||||
}
|
||||
|
||||
handleQueueStatus = (body) => {
|
||||
this.props.dispatchUpdate({ section: 'queue.status', data: body.resource });
|
||||
}
|
||||
|
||||
handleRootfolder = () => {
|
||||
this.props.dispatchFetchRootFolders();
|
||||
}
|
||||
|
||||
handleVersion = (body) => {
|
||||
const version = body.Version;
|
||||
|
||||
this.props.dispatchSetVersion({ version });
|
||||
}
|
||||
|
||||
handleSystemTask = () => {
|
||||
// No-op for now, we may want this later
|
||||
}
|
||||
|
||||
handleTag = (body) => {
|
||||
if (body.action === 'sync') {
|
||||
this.props.dispatchFetchTags();
|
||||
this.props.dispatchFetchTagDetails();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onStateChanged = (change) => {
|
||||
const state = getState(change.newState);
|
||||
console.log(`signalR: ${state}`);
|
||||
|
||||
if (state === 'connected') {
|
||||
// Clear disconnected time
|
||||
this.disconnectedTime = null;
|
||||
|
||||
const {
|
||||
dispatchFetchCommands,
|
||||
dispatchSetAppValue
|
||||
} = this.props;
|
||||
|
||||
// Repopulate the page (if a repopulator is set) to ensure things
|
||||
// are in sync after reconnecting.
|
||||
|
||||
if (this.props.isReconnecting || this.props.isDisconnected) {
|
||||
dispatchFetchCommands();
|
||||
repopulatePage();
|
||||
}
|
||||
|
||||
dispatchSetAppValue({
|
||||
isConnected: true,
|
||||
isReconnecting: false,
|
||||
isDisconnected: false,
|
||||
isRestarting: false
|
||||
});
|
||||
|
||||
this.retryInterval = 5;
|
||||
|
||||
if (this.retryTimeoutId) {
|
||||
clearTimeout(this.retryTimeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReceived = (message) => {
|
||||
console.debug('signalR: received', message.name, message.body);
|
||||
|
||||
this.handleMessage(message);
|
||||
}
|
||||
|
||||
onReconnecting = () => {
|
||||
if (window.Radarr.unloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.disconnectedTime) {
|
||||
this.disconnectedTime = Math.floor(new Date().getTime() / 1000);
|
||||
}
|
||||
|
||||
this.props.dispatchSetAppValue({
|
||||
isReconnecting: true
|
||||
});
|
||||
}
|
||||
|
||||
onDisconnected = () => {
|
||||
if (window.Radarr.unloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.disconnectedTime) {
|
||||
this.disconnectedTime = Math.floor(new Date().getTime() / 1000);
|
||||
}
|
||||
|
||||
this.props.dispatchSetAppValue({
|
||||
isConnected: false,
|
||||
isReconnecting: true,
|
||||
isDisconnected: isAppDisconnected(this.disconnectedTime)
|
||||
});
|
||||
|
||||
this.retryConnection();
|
||||
}
|
||||
|
||||
//
|
||||
// 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,
|
||||
dispatchFetchQueue: PropTypes.func.isRequired,
|
||||
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchFetchTags: PropTypes.func.isRequired,
|
||||
dispatchFetchTagDetails: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(SignalRConnector);
|
Reference in New Issue
Block a user