1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-10 15:10:09 -04:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Mark McDowall
07fe813d70 Use react-query for Indexers Flags 2026-03-08 10:46:55 -07:00
Mark McDowall
0256c91782 Use react-query for Indexers Options 2026-03-08 10:22:58 -07:00
Mark McDowall
1fd8f55838 Add v5 Indexer options endpoints 2026-03-08 10:20:48 -07:00
15 changed files with 154 additions and 227 deletions

View File

@@ -14,9 +14,7 @@ import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
import ImportListExclusion from 'typings/ImportListExclusion';
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
import IndexerFlag from 'typings/IndexerFlag';
import DownloadClientOptions from 'typings/Settings/DownloadClientOptions';
import IndexerOptions from 'typings/Settings/IndexerOptions';
type Presets<T> = T & {
presets: T[];
@@ -58,10 +56,6 @@ export interface ImportListAppState
isTestingAll: boolean;
}
export interface IndexerOptionsAppState
extends AppSectionItemState<IndexerOptions>,
AppSectionSaveState {}
export interface CustomFormatAppState
extends AppSectionState<CustomFormat>,
AppSectionDeleteState,
@@ -85,8 +79,6 @@ export interface ImportListExclusionsSettingsAppState
pendingChanges: Partial<ImportListExclusion>;
}
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
interface SettingsAppState {
autoTaggings: AutoTaggingAppState;
autoTaggingSpecifications: AutoTaggingSpecificationAppState;
@@ -98,8 +90,6 @@ interface SettingsAppState {
importListExclusions: ImportListExclusionsSettingsAppState;
importListOptions: ImportListOptionsSettingsAppState;
importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexerOptions: IndexerOptionsAppState;
}
export default SettingsAppState;

View File

@@ -1,35 +1,8 @@
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import React, { useCallback, useMemo } from 'react';
import useIndexerFlags from 'Settings/Indexers/useIndexerFlags';
import { EnhancedSelectInputChanged } from 'typings/inputs';
import EnhancedSelectInput from './EnhancedSelectInput';
const selectIndexerFlagsValues = (selectedFlags: number) =>
createSelector(
(state: AppState) => state.settings.indexerFlags,
(indexerFlags) => {
const value = indexerFlags.items.reduce((acc: number[], { id }) => {
// eslint-disable-next-line no-bitwise
if ((selectedFlags & id) === id) {
acc.push(id);
}
return acc;
}, []);
const values = indexerFlags.items.map(({ id, name }) => ({
key: id,
value: name,
}));
return {
value,
values,
};
}
);
export interface IndexerFlagsSelectInputProps {
name: string;
indexerFlags: number;
@@ -42,7 +15,29 @@ function IndexerFlagsSelectInput({
onChange,
...otherProps
}: IndexerFlagsSelectInputProps) {
const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags));
const { data: allIndexerFlags } = useIndexerFlags();
const value = useMemo(
() =>
allIndexerFlags.reduce((acc: number[], { id }) => {
// eslint-disable-next-line no-bitwise
if ((indexerFlags & id) === id) {
acc.push(id);
}
return acc;
}, []),
[allIndexerFlags, indexerFlags]
);
const values = useMemo(
() =>
allIndexerFlags.map(({ id, name }) => ({
key: id,
value: name,
})),
[allIndexerFlags]
);
const handleChange = useCallback(
(change: EnhancedSelectInputChanged<number[]>) => {

View File

@@ -1,15 +1,14 @@
import React from 'react';
import { useSelector } from 'react-redux';
import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector';
import useIndexerFlags from 'Settings/Indexers/useIndexerFlags';
interface IndexerFlagsProps {
indexerFlags: number;
}
function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) {
const allIndexerFlags = useSelector(createIndexerFlagsSelector);
const { data: allIndexerFlags } = useIndexerFlags();
const flags = allIndexerFlags.items.filter(
const flags = allIndexerFlags.filter(
// eslint-disable-next-line no-bitwise
(item) => (indexerFlags & item.id) === item.id
);

View File

@@ -8,19 +8,18 @@ import useCustomFilters from 'Filters/useCustomFilters';
import { useInitializeLanguage } from 'Language/useLanguageName';
import { useLanguages } from 'Language/useLanguages';
import useSeries from 'Series/useSeries';
import useIndexerFlags from 'Settings/Indexers/useIndexerFlags';
import { useQualityProfiles } from 'Settings/Profiles/Quality/useQualityProfiles';
import { useUiSettings } from 'Settings/UI/useUiSettings';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import {
fetchImportLists,
fetchIndexerFlags,
} from 'Store/Actions/settingsActions';
import { fetchImportLists } from 'Store/Actions/settingsActions';
import useSystemStatus from 'System/Status/useSystemStatus';
import useTags from 'Tags/useTags';
import { ApiError } from 'Utilities/Fetch/fetchJson';
const createErrorsSelector = ({
customFiltersError,
indexerFlagsError,
systemStatusError,
tagsError,
translationsError,
@@ -30,6 +29,7 @@ const createErrorsSelector = ({
languagesError,
}: {
customFiltersError: ApiError | null;
indexerFlagsError: ApiError | null;
systemStatusError: ApiError | null;
tagsError: ApiError | null;
translationsError: ApiError | null;
@@ -40,8 +40,7 @@ const createErrorsSelector = ({
}) =>
createSelector(
(state: AppState) => state.settings.importLists.error,
(state: AppState) => state.settings.indexerFlags.error,
(importListsError, indexerFlagsError) => {
(importListsError) => {
const hasError = !!(
customFiltersError ||
seriesError ||
@@ -102,15 +101,17 @@ const useAppPage = () => {
const { isFetched: isLanguagesFetched, error: languagesError } =
useLanguages();
const { isFetched: isIndexerFlagsFetched, error: indexerFlagsError } =
useIndexerFlags();
const isAppStatePopulated = useSelector(
(state: AppState) =>
state.settings.importLists.isPopulated &&
state.settings.indexerFlags.isPopulated
(state: AppState) => state.settings.importLists.isPopulated
);
const isPopulated =
isAppStatePopulated &&
isCustomFiltersFetched &&
isIndexerFlagsFetched &&
isSeriesFetched &&
isSystemStatusFetched &&
isTagsFetched &&
@@ -122,6 +123,7 @@ const useAppPage = () => {
const { hasError, errors } = useSelector(
createErrorsSelector({
customFiltersError,
indexerFlagsError,
seriesError,
systemStatusError,
tagsError,
@@ -148,7 +150,6 @@ const useAppPage = () => {
useEffect(() => {
dispatch(fetchCustomFilters());
dispatch(fetchImportLists());
dispatch(fetchIndexerFlags());
}, [dispatch]);
return useMemo(() => {

View File

@@ -1,5 +1,4 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
@@ -9,21 +8,13 @@ import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { inputTypes, kinds } from 'Helpers/Props';
import { useShowAdvancedSettings } from 'Settings/advancedSettingsStore';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import {
fetchIndexerOptions,
saveIndexerOptions,
setIndexerOptionsValue,
} from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import { InputChanged } from 'typings/inputs';
import {
OnChildStateChange,
SetChildSave,
} from 'typings/Settings/SettingsState';
import translate from 'Utilities/String/translate';
const SECTION = 'indexerOptions';
import { useManageIndexerSettings } from './useIndexerSettings';
interface IndexerOptionsProps {
setChildSave: SetChildSave;
@@ -34,31 +25,31 @@ function IndexerOptions({
setChildSave,
onChildStateChange,
}: IndexerOptionsProps) {
const dispatch = useDispatch();
const {
isFetching,
isPopulated,
isFetched,
isSaving,
error,
settings,
hasSettings,
hasPendingChanges,
} = useSelector(createSettingsSectionSelector(SECTION));
saveSettings,
updateSetting,
} = useManageIndexerSettings();
const showAdvancedSettings = useShowAdvancedSettings();
const handleInputChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions aren't typed
dispatch(setIndexerOptionsValue(change));
({ name, value }: InputChanged) => {
// @ts-expect-error - InputChanged name/value are not typed as keyof IndexerSettingsModel
updateSetting(name, value);
},
[dispatch]
[updateSetting]
);
useEffect(() => {
dispatch(fetchIndexerOptions());
setChildSave(() => dispatch(saveIndexerOptions()));
}, [dispatch, setChildSave]);
setChildSave(saveSettings);
}, [saveSettings, setChildSave]);
useEffect(() => {
onChildStateChange({
@@ -67,12 +58,6 @@ function IndexerOptions({
});
}, [hasPendingChanges, isSaving, onChildStateChange]);
useEffect(() => {
return () => {
dispatch(clearPendingChanges({ section: `settings.${SECTION}` }));
};
}, [dispatch]);
return (
<FieldSet legend={translate('Options')}>
{isFetching ? <LoadingIndicator /> : null}
@@ -83,7 +68,7 @@ function IndexerOptions({
</Alert>
) : null}
{hasSettings && isPopulated && !error ? (
{hasSettings && isFetched && !error ? (
<Form>
<FormGroup>
<FormLabel>{translate('MinimumAge')}</FormLabel>

View File

@@ -0,0 +1,18 @@
import { useManageSettings, useSettings } from 'Settings/useSettings';
export interface IndexerSettingsModel {
minimumAge: number;
retention: number;
maximumSize: number;
rssSyncInterval: number;
}
const PATH = '/settings/indexer';
export const useIndexerSettings = () => {
return useSettings<IndexerSettingsModel>(PATH);
};
export const useManageIndexerSettings = () => {
return useManageSettings<IndexerSettingsModel>(PATH);
};

View File

@@ -0,0 +1,25 @@
import useApiQuery from 'Helpers/Hooks/useApiQuery';
export interface IndexerFlag {
id: number;
name: string;
}
const DEFAULT_INDEXER_FLAGS: IndexerFlag[] = [];
const useIndexerFlags = () => {
const result = useApiQuery<IndexerFlag[]>({
path: '/indexerFlag',
queryOptions: {
gcTime: Infinity,
staleTime: Infinity,
},
});
return {
...result,
data: result.data ?? DEFAULT_INDEXER_FLAGS,
};
};
export default useIndexerFlags;

View File

@@ -1,48 +0,0 @@
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import { createThunk } from 'Store/thunks';
//
// Variables
const section = 'settings.indexerFlags';
//
// Actions Types
export const FETCH_INDEXER_FLAGS = 'settings/indexerFlags/fetchIndexerFlags';
//
// Action Creators
export const fetchIndexerFlags = createThunk(FETCH_INDEXER_FLAGS);
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
items: []
},
//
// Action Handlers
actionHandlers: {
[FETCH_INDEXER_FLAGS]: createFetchHandler(section, '/indexerFlag')
},
//
// Reducers
reducers: {
}
};

View File

@@ -1,64 +0,0 @@
import { createAction } from 'redux-actions';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createSaveHandler from 'Store/Actions/Creators/createSaveHandler';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
//
// Variables
const section = 'settings.indexerOptions';
//
// Actions Types
export const FETCH_INDEXER_OPTIONS = 'settings/indexerOptions/fetchIndexerOptions';
export const SAVE_INDEXER_OPTIONS = 'settings/indexerOptions/saveIndexerOptions';
export const SET_INDEXER_OPTIONS_VALUE = 'settings/indexerOptions/setIndexerOptionsValue';
//
// Action Creators
export const fetchIndexerOptions = createThunk(FETCH_INDEXER_OPTIONS);
export const saveIndexerOptions = createThunk(SAVE_INDEXER_OPTIONS);
export const setIndexerOptionsValue = createAction(SET_INDEXER_OPTIONS_VALUE, (payload) => {
return {
section,
...payload
};
});
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
pendingChanges: {},
isSaving: false,
saveError: null,
item: {}
},
//
// Action Handlers
actionHandlers: {
[FETCH_INDEXER_OPTIONS]: createFetchHandler(section, '/config/indexer'),
[SAVE_INDEXER_OPTIONS]: createSaveHandler(section, '/config/indexer')
},
//
// Reducers
reducers: {
[SET_INDEXER_OPTIONS_VALUE]: createSetSettingValueReducer(section)
}
};

View File

@@ -10,8 +10,6 @@ import downloadClients from './Settings/downloadClients';
import importListExclusions from './Settings/importListExclusions';
import importListOptions from './Settings/importListOptions';
import importLists from './Settings/importLists';
import indexerFlags from './Settings/indexerFlags';
import indexerOptions from './Settings/indexerOptions';
export * from './Settings/autoTaggingSpecifications';
export * from './Settings/autoTaggings';
@@ -23,8 +21,6 @@ export * from './Settings/downloadClientOptions';
export * from './Settings/importListOptions';
export * from './Settings/importLists';
export * from './Settings/importListExclusions';
export * from './Settings/indexerFlags';
export * from './Settings/indexerOptions';
//
// Variables
@@ -45,9 +41,7 @@ export const defaultState = {
downloadClientOptions: downloadClientOptions.defaultState,
importLists: importLists.defaultState,
importListExclusions: importListExclusions.defaultState,
importListOptions: importListOptions.defaultState,
indexerFlags: indexerFlags.defaultState,
indexerOptions: indexerOptions.defaultState
importListOptions: importListOptions.defaultState
};
export const persistState = [
@@ -67,9 +61,7 @@ export const actionHandlers = handleThunks({
...downloadClientOptions.actionHandlers,
...importLists.actionHandlers,
...importListExclusions.actionHandlers,
...importListOptions.actionHandlers,
...indexerFlags.actionHandlers,
...indexerOptions.actionHandlers
...importListOptions.actionHandlers
});
//
@@ -85,8 +77,6 @@ export const reducers = createHandleActions({
...downloadClientOptions.reducers,
...importLists.reducers,
...importListExclusions.reducers,
...importListOptions.reducers,
...indexerFlags.reducers,
...indexerOptions.reducers
...importListOptions.reducers
}, defaultState, section);

View File

@@ -1,9 +0,0 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
const createIndexerFlagsSelector = createSelector(
(state: AppState) => state.settings.indexerFlags,
(indexerFlags) => indexerFlags
);
export default createIndexerFlagsSelector;

View File

@@ -1,6 +0,0 @@
interface IndexerFlag {
id: number;
name: string;
}
export default IndexerFlag;

View File

@@ -1,6 +0,0 @@
export default interface IndexerOptions {
minimumAge: number;
retention: number;
maximumSize: number;
rssSyncInterval: number;
}

View File

@@ -0,0 +1,30 @@
using FluentValidation;
using NzbDrone.Core.Configuration;
using Sonarr.Http;
using Sonarr.Http.Validation;
namespace Sonarr.Api.V5.Settings
{
[V5ApiController("settings/indexer")]
public class IndexerSettingsController : SettingsController<IndexerSettingsResource>
{
public IndexerSettingsController(IConfigFileProvider configFileProvider,
IConfigService configService)
: base(configFileProvider, configService)
{
SharedValidator.RuleFor(c => c.MinimumAge)
.GreaterThanOrEqualTo(0);
SharedValidator.RuleFor(c => c.Retention)
.GreaterThanOrEqualTo(0);
SharedValidator.RuleFor(c => c.RssSyncInterval)
.IsValidRssSyncInterval();
}
protected override IndexerSettingsResource ToResource(IConfigFileProvider configFile, IConfigService model)
{
return IndexerConfigResourceMapper.ToResource(model);
}
}
}

View File

@@ -0,0 +1,27 @@
using NzbDrone.Core.Configuration;
using Sonarr.Http.REST;
namespace Sonarr.Api.V5.Settings
{
public class IndexerSettingsResource : RestResource
{
public int MinimumAge { get; set; }
public int Retention { get; set; }
public int MaximumSize { get; set; }
public int RssSyncInterval { get; set; }
}
public static class IndexerConfigResourceMapper
{
public static IndexerSettingsResource ToResource(IConfigService model)
{
return new IndexerSettingsResource
{
MinimumAge = model.MinimumAge,
Retention = model.Retention,
MaximumSize = model.MaximumSize,
RssSyncInterval = model.RssSyncInterval
};
}
}
}