mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-09 15:00:03 -04:00
Compare commits
1 Commits
v5-provide
...
api-docs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a6aeb082b |
@@ -1,24 +1,14 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import useApiMutation, {
|
||||
getValidationFailures,
|
||||
} from 'Helpers/Hooks/useApiMutation';
|
||||
import useApiMutation from 'Helpers/Hooks/useApiMutation';
|
||||
import useApiQuery, { QueryOptions } from 'Helpers/Hooks/useApiQuery';
|
||||
import { usePendingChangesStore } from 'Helpers/Hooks/usePendingChangesStore';
|
||||
import { usePendingFieldsStore } from 'Helpers/Hooks/usePendingFieldsStore';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import { PendingSection } from 'typings/pending';
|
||||
import Provider from 'typings/Provider';
|
||||
import fetchJson, { ApiError } from 'Utilities/Fetch/fetchJson';
|
||||
import getQueryPath from 'Utilities/Fetch/getQueryPath';
|
||||
import getQueryString, { QueryParams } from 'Utilities/Fetch/getQueryString';
|
||||
|
||||
export type SkipValidation = 'none' | 'warnings' | 'all';
|
||||
export interface SaveOptions {
|
||||
skipTesting?: boolean;
|
||||
skipValidation?: SkipValidation;
|
||||
}
|
||||
import { ApiError } from 'Utilities/Fetch/fetchJson';
|
||||
|
||||
interface BaseManageProviderSettings<T extends ModelBase>
|
||||
extends Omit<ReturnType<typeof selectSettings<T>>, 'settings'> {
|
||||
@@ -90,60 +80,28 @@ export const useSaveProviderSettings = <T extends ModelBase>(
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate, isPending, error } = useMutation<
|
||||
T,
|
||||
ApiError,
|
||||
{
|
||||
data: T;
|
||||
} & SaveOptions
|
||||
>({
|
||||
mutationFn: async ({ data, skipTesting, skipValidation }) => {
|
||||
const queryParams: QueryParams = {};
|
||||
const { mutate, isPending, error } = useApiMutation<T, T>({
|
||||
path: id ? `${path}/${id}` : path,
|
||||
method: id ? 'PUT' : 'POST',
|
||||
mutationOptions: {
|
||||
onSuccess: (updatedSettings: T) => {
|
||||
queryClient.setQueryData<T[]>([path], (oldData = []) => {
|
||||
if (id) {
|
||||
return oldData.map((item) =>
|
||||
item.id === updatedSettings.id ? updatedSettings : item
|
||||
);
|
||||
}
|
||||
|
||||
if (skipTesting) {
|
||||
queryParams.skipTesting = true;
|
||||
}
|
||||
|
||||
if (skipValidation && skipValidation !== 'none') {
|
||||
queryParams.skipValidation = skipValidation;
|
||||
}
|
||||
|
||||
return fetchJson<T, T>({
|
||||
path:
|
||||
getQueryPath(id ? `${path}/${id}` : path) +
|
||||
getQueryString(queryParams),
|
||||
method: id ? 'PUT' : 'POST',
|
||||
headers: {
|
||||
'X-Api-Key': window.Sonarr.apiKey,
|
||||
'X-Sonarr-Client': 'Sonarr',
|
||||
},
|
||||
body: data,
|
||||
});
|
||||
return [...oldData, updatedSettings];
|
||||
});
|
||||
onSuccess?.(updatedSettings);
|
||||
},
|
||||
onError,
|
||||
},
|
||||
onSuccess: (updatedSettings: T) => {
|
||||
queryClient.setQueryData<T[]>([path], (oldData = []) => {
|
||||
if (id) {
|
||||
return oldData.map((item) =>
|
||||
item.id === updatedSettings.id ? updatedSettings : item
|
||||
);
|
||||
}
|
||||
|
||||
return [...oldData, updatedSettings];
|
||||
});
|
||||
onSuccess?.(updatedSettings);
|
||||
},
|
||||
onError,
|
||||
});
|
||||
|
||||
const save = useCallback(
|
||||
(data: T, options?: SaveOptions) => {
|
||||
mutate({ data, ...options });
|
||||
},
|
||||
[mutate]
|
||||
);
|
||||
|
||||
return {
|
||||
save,
|
||||
save: mutate,
|
||||
isSaving: isPending,
|
||||
saveError: error,
|
||||
};
|
||||
@@ -154,41 +112,17 @@ export const useTestProvider = <T extends ModelBase>(
|
||||
onSuccess?: () => void,
|
||||
onError?: (error: ApiError) => void
|
||||
) => {
|
||||
const { mutate, isPending, error } = useMutation<
|
||||
void,
|
||||
ApiError,
|
||||
{ data: T } & SaveOptions
|
||||
>({
|
||||
mutationFn: async ({ data, skipValidation }) => {
|
||||
const queryParams: QueryParams = {};
|
||||
|
||||
if (skipValidation && skipValidation !== 'none') {
|
||||
queryParams.skipValidation = skipValidation;
|
||||
}
|
||||
|
||||
return fetchJson<void, T>({
|
||||
path: getQueryPath(`${path}/test`) + getQueryString(queryParams),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Api-Key': window.Sonarr.apiKey,
|
||||
'X-Sonarr-Client': 'Sonarr',
|
||||
},
|
||||
body: data,
|
||||
});
|
||||
const { mutate, isPending, error } = useApiMutation<void, T>({
|
||||
path: `${path}/test`,
|
||||
method: 'POST',
|
||||
mutationOptions: {
|
||||
onSuccess,
|
||||
onError,
|
||||
},
|
||||
onSuccess,
|
||||
onError,
|
||||
});
|
||||
|
||||
const test = useCallback(
|
||||
(data: T, options?: SaveOptions) => {
|
||||
mutate({ data, ...options });
|
||||
},
|
||||
[mutate]
|
||||
);
|
||||
|
||||
return {
|
||||
test,
|
||||
test: mutate,
|
||||
isTesting: isPending,
|
||||
testError: error,
|
||||
};
|
||||
@@ -201,14 +135,12 @@ export const useManageProviderSettings = <T extends ModelBase>(
|
||||
): ManageProviderSettings<T> => {
|
||||
const provider = useProviderWithDefault<T>(id, defaultProvider, path);
|
||||
const [mutationError, setMutationError] = useState<ApiError | null>(null);
|
||||
const lastSaveData = useRef<string | null>(null);
|
||||
|
||||
const {
|
||||
pendingChanges,
|
||||
setPendingChange,
|
||||
unsetPendingChange,
|
||||
clearPendingChanges,
|
||||
hasPendingChanges,
|
||||
} = usePendingChangesStore<T>({});
|
||||
|
||||
const {
|
||||
@@ -222,7 +154,6 @@ export const useManageProviderSettings = <T extends ModelBase>(
|
||||
setMutationError(null);
|
||||
clearPendingChanges();
|
||||
clearPendingFields();
|
||||
lastSaveData.current = null;
|
||||
}, [clearPendingChanges, clearPendingFields]);
|
||||
|
||||
const handleTestSuccess = useCallback(() => {
|
||||
@@ -288,40 +219,8 @@ export const useManageProviderSettings = <T extends ModelBase>(
|
||||
} as T;
|
||||
}
|
||||
|
||||
const serializedSettings = JSON.stringify(updatedSettings);
|
||||
const isResave = lastSaveData.current === serializedSettings;
|
||||
lastSaveData.current = serializedSettings;
|
||||
|
||||
const saveOptions: SaveOptions = {};
|
||||
|
||||
// For existing providers with no pending changes, skip testing and all validation.
|
||||
if (provider.id > 0 && !hasPendingChanges && !hasPendingFields) {
|
||||
saveOptions.skipTesting = true;
|
||||
saveOptions.skipValidation = 'all';
|
||||
} else {
|
||||
// If resaving the exact same settings as the previous attempt, skip testing.
|
||||
if (isResave) {
|
||||
saveOptions.skipTesting = true;
|
||||
}
|
||||
|
||||
// If the last save returned only warnings, skip warning validation on the next save.
|
||||
const { errors, warnings } = getValidationFailures(mutationError);
|
||||
|
||||
if (errors.length === 0 && warnings.length > 0) {
|
||||
saveOptions.skipValidation = 'warnings';
|
||||
}
|
||||
}
|
||||
|
||||
save(updatedSettings, saveOptions);
|
||||
}, [
|
||||
provider,
|
||||
pendingChanges,
|
||||
pendingFields,
|
||||
hasPendingChanges,
|
||||
hasPendingFields,
|
||||
mutationError,
|
||||
save,
|
||||
]);
|
||||
save(updatedSettings);
|
||||
}, [provider, pendingChanges, pendingFields, save]);
|
||||
|
||||
const testProvider = useCallback(() => {
|
||||
let updatedSettings: T = {
|
||||
@@ -347,17 +246,8 @@ export const useManageProviderSettings = <T extends ModelBase>(
|
||||
} as T;
|
||||
}
|
||||
|
||||
const testOptions: SaveOptions = {};
|
||||
|
||||
// If the last operation returned only warnings, skip warning validation on the next test.
|
||||
const { errors, warnings } = getValidationFailures(mutationError);
|
||||
|
||||
if (errors.length === 0 && warnings.length > 0) {
|
||||
testOptions.skipValidation = 'warnings';
|
||||
}
|
||||
|
||||
test(updatedSettings, testOptions);
|
||||
}, [provider, pendingChanges, pendingFields, mutationError, test]);
|
||||
test(updatedSettings);
|
||||
}, [provider, pendingChanges, pendingFields, test]);
|
||||
|
||||
const updateValue = useCallback(
|
||||
<K extends keyof T>(key: K, value: T[K]) => {
|
||||
|
||||
@@ -76,13 +76,13 @@ namespace Sonarr.Api.V5.Provider
|
||||
[RestPostById]
|
||||
[Consumes("application/json")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<TProviderResource> CreateProvider([FromBody] TProviderResource providerResource, [FromQuery] bool skipTesting = false, [FromQuery] SkipValidation skipValidation = SkipValidation.None)
|
||||
public ActionResult<TProviderResource> CreateProvider([FromBody] TProviderResource providerResource, [FromQuery] bool forceSave = false)
|
||||
{
|
||||
var providerDefinition = GetDefinition(providerResource, null, skipValidation, false);
|
||||
var providerDefinition = GetDefinition(providerResource, null, true, !forceSave, false);
|
||||
|
||||
if (providerDefinition.Enable && !skipTesting)
|
||||
if (providerDefinition.Enable)
|
||||
{
|
||||
Test(providerDefinition, skipValidation);
|
||||
Test(providerDefinition, !forceSave);
|
||||
}
|
||||
|
||||
providerDefinition = _providerFactory.Create(providerDefinition);
|
||||
@@ -93,7 +93,7 @@ namespace Sonarr.Api.V5.Provider
|
||||
[RestPutById]
|
||||
[Consumes("application/json")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<TProviderResource> UpdateProvider([FromRoute] int id, [FromBody] TProviderResource providerResource, [FromQuery] bool skipTesting = false, [FromQuery] SkipValidation skipValidation = SkipValidation.None)
|
||||
public ActionResult<TProviderResource> UpdateProvider([FromRoute] int id, [FromBody] TProviderResource providerResource, [FromQuery] bool forceSave = false)
|
||||
{
|
||||
// TODO: Remove fallback to Id from body in next API version bump
|
||||
var existingDefinition = _providerFactory.Find(id) ?? _providerFactory.Find(providerResource.Id);
|
||||
@@ -103,15 +103,15 @@ namespace Sonarr.Api.V5.Provider
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var providerDefinition = GetDefinition(providerResource, existingDefinition, skipValidation, false);
|
||||
var providerDefinition = GetDefinition(providerResource, existingDefinition, true, !forceSave, false);
|
||||
|
||||
// Compare settings separately because they are not serialized with the definition.
|
||||
var hasDefinitionChanged = !existingDefinition.Equals(providerDefinition) || !existingDefinition.Settings.Equals(providerDefinition.Settings);
|
||||
|
||||
// Only test existing definitions if it is enabled, skipTesting isn't set and the definition has changed.
|
||||
if (providerDefinition.Enable && !skipTesting && hasDefinitionChanged)
|
||||
// Only test existing definitions if it is enabled and forceSave isn't set and the definition has changed.
|
||||
if (providerDefinition.Enable && !forceSave && hasDefinitionChanged)
|
||||
{
|
||||
Test(providerDefinition, skipValidation);
|
||||
Test(providerDefinition, true);
|
||||
}
|
||||
|
||||
if (hasDefinitionChanged)
|
||||
@@ -163,13 +163,13 @@ namespace Sonarr.Api.V5.Provider
|
||||
return Accepted(_providerFactory.Update(definitionsToUpdate).Select(x => _resourceMapper.ToResource(x)));
|
||||
}
|
||||
|
||||
private TProviderDefinition GetDefinition(TProviderResource providerResource, TProviderDefinition? existingDefinition, SkipValidation skipValidation, bool forceValidate)
|
||||
private TProviderDefinition GetDefinition(TProviderResource providerResource, TProviderDefinition? existingDefinition, bool validate, bool includeWarnings, bool forceValidate)
|
||||
{
|
||||
var definition = _resourceMapper.ToModel(providerResource, existingDefinition);
|
||||
|
||||
if (skipValidation != SkipValidation.All && (definition.Enable || forceValidate))
|
||||
if (validate && (definition.Enable || forceValidate))
|
||||
{
|
||||
Validate(definition, skipValidation);
|
||||
Validate(definition, includeWarnings);
|
||||
}
|
||||
|
||||
return definition;
|
||||
@@ -218,12 +218,12 @@ namespace Sonarr.Api.V5.Provider
|
||||
[SkipValidation(true, false)]
|
||||
[HttpPost("test")]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult Test([FromBody] TProviderResource providerResource, [FromQuery] SkipValidation skipValidation = SkipValidation.None)
|
||||
public ActionResult Test([FromBody] TProviderResource providerResource, [FromQuery] bool forceTest = false)
|
||||
{
|
||||
var existingDefinition = providerResource.Id > 0 ? _providerFactory.Find(providerResource.Id) : null;
|
||||
var providerDefinition = GetDefinition(providerResource, existingDefinition, skipValidation, true);
|
||||
var providerDefinition = GetDefinition(providerResource, existingDefinition, true, !forceTest, true);
|
||||
|
||||
Test(providerDefinition, skipValidation);
|
||||
Test(providerDefinition, true);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@@ -261,7 +261,7 @@ namespace Sonarr.Api.V5.Provider
|
||||
public IActionResult RequestAction([FromRoute] string name, [FromBody] TProviderResource providerResource)
|
||||
{
|
||||
var existingDefinition = providerResource.Id > 0 ? _providerFactory.Find(providerResource.Id) : null;
|
||||
var providerDefinition = GetDefinition(providerResource, existingDefinition, SkipValidation.All, false);
|
||||
var providerDefinition = GetDefinition(providerResource, existingDefinition, false, false, false);
|
||||
|
||||
var query = Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString());
|
||||
|
||||
@@ -288,30 +288,30 @@ namespace Sonarr.Api.V5.Provider
|
||||
BroadcastResourceChange(ModelAction.Deleted, message.ProviderId);
|
||||
}
|
||||
|
||||
private void Validate(TProviderDefinition definition, SkipValidation skipValidation)
|
||||
private void Validate(TProviderDefinition definition, bool includeWarnings)
|
||||
{
|
||||
var validationResult = definition.Settings.Validate();
|
||||
|
||||
VerifyValidationResult(validationResult, skipValidation);
|
||||
VerifyValidationResult(validationResult, includeWarnings);
|
||||
}
|
||||
|
||||
protected virtual void Test(TProviderDefinition definition, SkipValidation skipValidation)
|
||||
protected virtual void Test(TProviderDefinition definition, bool includeWarnings)
|
||||
{
|
||||
var validationResult = _providerFactory.Test(definition);
|
||||
|
||||
VerifyValidationResult(validationResult, skipValidation);
|
||||
VerifyValidationResult(validationResult, includeWarnings);
|
||||
}
|
||||
|
||||
protected void VerifyValidationResult(ValidationResult validationResult, SkipValidation skipValidation)
|
||||
protected void VerifyValidationResult(ValidationResult validationResult, bool includeWarnings)
|
||||
{
|
||||
var result = validationResult as NzbDroneValidationResult ?? new NzbDroneValidationResult(validationResult.Errors);
|
||||
|
||||
if (skipValidation == SkipValidation.None && (!result.IsValid || result.HasWarnings))
|
||||
if (includeWarnings && (!result.IsValid || result.HasWarnings))
|
||||
{
|
||||
throw new ValidationException(result.Failures);
|
||||
}
|
||||
|
||||
if (skipValidation == SkipValidation.Warnings && !result.IsValid)
|
||||
if (!result.IsValid)
|
||||
{
|
||||
throw new ValidationException(result.Errors);
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Sonarr.Api.V5.Provider
|
||||
{
|
||||
public enum SkipValidation
|
||||
{
|
||||
None = 0,
|
||||
Warnings = 1,
|
||||
All = 2
|
||||
}
|
||||
}
|
||||
@@ -7518,7 +7518,8 @@
|
||||
"notQualityUpgrade",
|
||||
"notRevisionUpgrade",
|
||||
"notCustomFormatUpgrade",
|
||||
"notCustomFormatUpgradeAfterRename"
|
||||
"notCustomFormatUpgradeAfterRename",
|
||||
"multiSeason"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user