1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-22 22:16:13 -04:00
Files
Sonarr/frontend/src/Commands/useCommands.ts
T
2026-01-06 19:14:57 -08:00

241 lines
5.8 KiB
TypeScript

import { useQueryClient } from '@tanstack/react-query';
import { useCallback, useMemo, useRef } from 'react';
import { showMessage } from 'App/messagesStore';
import Command, { CommandBody, NewCommandBody } from 'Commands/Command';
import useApiMutation from 'Helpers/Hooks/useApiMutation';
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import {
ERROR,
INFO,
type MessageType,
SUCCESS,
} from 'Helpers/Props/messageTypes';
import { isSameCommand } from 'Utilities/Command';
const DEFAULT_COMMANDS: Command[] = [];
const COMMAND_REFETCH_INTERVAL = 5 * 60 * 1000;
const commandFinishedCallbacks: Record<number, (command: Command) => void> = {};
export const useCommands = () => {
const result = useApiQuery<Command[]>({
path: '/command',
queryOptions: {
refetchInterval: COMMAND_REFETCH_INTERVAL,
},
});
return {
...result,
data: result.data ?? DEFAULT_COMMANDS,
};
};
export default useCommands;
export const useExecuteCommand = () => {
const queryClient = useQueryClient();
const lastCommandRef = useRef<{
command: NewCommandBody;
timestamp: number;
} | null>(null);
const { mutate } = useApiMutation<Command, NewCommandBody>({
method: 'POST',
path: '/command',
mutationOptions: {
onSuccess: (newCommand: Command) => {
queryClient.setQueryData<Command[]>(
['/command'],
(oldCommands = []) => {
return [...oldCommands, newCommand];
}
);
},
},
});
const executeCommand = useCallback(
(body: NewCommandBody, commandFinished?: (command: Command) => void) => {
const now = Date.now();
const lastCommand = lastCommandRef.current;
// Check if the same command was run within the last 5 seconds
if (
lastCommand &&
now - lastCommand.timestamp < 5000 &&
isSameCommand(lastCommand.command, body)
) {
console.warn(
'Please wait at least 5 seconds before running this command again'
);
return;
}
// Update last command reference
lastCommandRef.current = {
command: body,
timestamp: now,
};
const executeWithCallback = (commandBody: NewCommandBody) => {
mutate(commandBody, {
onSuccess: (command) => {
if (commandFinished) {
commandFinishedCallbacks[command.id] = commandFinished;
}
},
});
};
executeWithCallback(body);
},
[mutate]
);
return executeCommand;
};
export const useCancelCommand = (id: number) => {
const queryClient = useQueryClient();
const { mutate, isPending, error } = useApiMutation<void, void>({
method: 'DELETE',
path: `/command/${id}`,
mutationOptions: {
onSuccess: () => {
queryClient.setQueryData<Command[]>(
['/command'],
(oldCommands = []) => {
return oldCommands.filter((command) => command.id !== id);
}
);
},
},
});
return {
cancelCommand: mutate,
isCancellingCommand: isPending,
commandCancelError: error,
};
};
export const useCommand = (
commandName: string,
constraints: Partial<CommandBody> = {}
) => {
const { data: commands } = useCommands();
return useMemo(() => {
return commands.findLast((command) => {
if (command.name !== commandName) {
return false;
}
return (Object.keys(constraints) as Array<keyof CommandBody>).every(
(key) => {
const constraintValue = constraints[key];
const commandValue = command.body?.[key];
if (constraintValue === undefined) {
return true;
}
if (Array.isArray(constraintValue) && Array.isArray(commandValue)) {
return constraintValue.every((value) =>
commandValue.includes(value)
);
}
return constraintValue === commandValue;
}
);
});
}, [commands, commandName, constraints]);
};
export const useCommandExecuting = (
commandName: string,
constraints: Partial<CommandBody> = {}
) => {
const command = useCommand(commandName, constraints);
return command
? command.status === 'queued' || command.status === 'started'
: false;
};
export const useExecutingCommands = () => {
const { data: commands } = useCommands();
return commands.filter(
(command) => command.status === 'queued' || command.status === 'started'
);
};
export const useUpdateCommand = () => {
const queryClient = useQueryClient();
return (command: Command) => {
queryClient.setQueryData<Command[]>(['/command'], (oldCommands = []) => {
return oldCommands.map((existingCommand) =>
existingCommand.id === command.id ? command : existingCommand
);
});
// Show command message for user feedback
showCommandMessage(command);
// Both successful and failed commands need to be
// completed, otherwise they spin until they time out.
const isFinished =
command.status === 'completed' || command.status === 'failed';
if (isFinished) {
const commandFinished = commandFinishedCallbacks[command.id];
if (commandFinished) {
commandFinished(command);
delete commandFinishedCallbacks[command.id];
}
}
};
};
function showCommandMessage(command: Command) {
const {
id,
name,
trigger,
message,
body = {} as CommandBody,
status,
} = command;
const { sendUpdatesToClient, suppressMessages } = body;
if (!message || !body || !sendUpdatesToClient || suppressMessages) {
return;
}
let type: MessageType = INFO;
let hideAfter = 0;
if (status === 'completed') {
type = SUCCESS;
hideAfter = 4;
} else if (status === 'failed') {
type = ERROR;
hideAfter = trigger === 'manual' ? 10 : 4;
}
showMessage({
id,
name,
message,
type,
hideAfter,
});
}