From 56184905a9ca0a9f6468cf7dffcc0d61be98080d Mon Sep 17 00:00:00 2001 From: Qstick Date: Tue, 18 Aug 2020 23:21:44 -0400 Subject: [PATCH] New: Allow Radarr List Sync by Source Tag Fixes #4826 --- .../Components/Form/DeviceInputConnector.js | 26 +++++++++++--- .../Components/Form/ProviderFieldFormGroup.js | 3 ++ .../Store/Actions/providerOptionActions.js | 9 +++-- .../Annotations/FieldDefinitionAttribute.cs | 1 + .../NetImport/Radarr/RadarrAPIResource.cs | 7 ++++ .../NetImport/Radarr/RadarrImport.cs | 36 +++++++++++++------ .../NetImport/Radarr/RadarrSettings.cs | 6 +++- .../NetImport/Radarr/RadarrV3Proxy.cs | 6 ++++ src/Radarr.Http/ClientSchema/Field.cs | 1 + src/Radarr.Http/ClientSchema/SchemaBuilder.cs | 3 +- 10 files changed, 79 insertions(+), 19 deletions(-) diff --git a/frontend/src/Components/Form/DeviceInputConnector.js b/frontend/src/Components/Form/DeviceInputConnector.js index 8fa9d41c37..2053d3a30f 100644 --- a/frontend/src/Components/Form/DeviceInputConnector.js +++ b/frontend/src/Components/Form/DeviceInputConnector.js @@ -8,16 +8,28 @@ import DeviceInput from './DeviceInput'; function createMapStateToProps() { return createSelector( (state, { value }) => value, + (state, { name }) => name, (state) => state.providerOptions, - (value, devices) => { + (value, name, devices) => { + const { + isFetching, + isPopulated, + error, + items + } = devices; return { - ...devices, + isFetching, + isPopulated, + error, + items: items[name] || [], selectedDevices: value.map((valueDevice) => { + const sectionItems = items[name] || []; + // Disable equality ESLint rule so we don't need to worry about // a type mismatch between the value items and the device ID. // eslint-disable-next-line eqeqeq - const device = devices.items.find((d) => d.id == valueDevice); + const device = sectionItems.find((d) => d.id == valueDevice); if (device) { return { @@ -61,11 +73,14 @@ class DeviceInputConnector extends Component { const { provider, providerData, - dispatchFetchOptions + dispatchFetchOptions, + requestAction, + name } = this.props; dispatchFetchOptions({ - action: 'getDevices', + action: requestAction, + itemSection: name, provider, providerData }); @@ -94,6 +109,7 @@ class DeviceInputConnector extends Component { DeviceInputConnector.propTypes = { provider: PropTypes.string.isRequired, providerData: PropTypes.object.isRequired, + requestAction: PropTypes.string.isRequired, name: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, dispatchFetchOptions: PropTypes.func.isRequired, diff --git a/frontend/src/Components/Form/ProviderFieldFormGroup.js b/frontend/src/Components/Form/ProviderFieldFormGroup.js index d0ea9916ef..e1263a096a 100644 --- a/frontend/src/Components/Form/ProviderFieldFormGroup.js +++ b/frontend/src/Components/Form/ProviderFieldFormGroup.js @@ -62,6 +62,7 @@ function ProviderFieldFormGroup(props) { value, type, advanced, + requestAction, hidden, pending, errors, @@ -98,6 +99,7 @@ function ProviderFieldFormGroup(props) { pending={pending} includeFiles={type === 'filePath' ? true : undefined} onChange={onChange} + requestAction={requestAction} {...otherProps} /> @@ -118,6 +120,7 @@ ProviderFieldFormGroup.propTypes = { value: PropTypes.any, type: PropTypes.string.isRequired, advanced: PropTypes.bool.isRequired, + requestAction: PropTypes.string, hidden: PropTypes.string, pending: PropTypes.bool.isRequired, errors: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/Store/Actions/providerOptionActions.js b/frontend/src/Store/Actions/providerOptionActions.js index 4a89561171..72a8b1745b 100644 --- a/frontend/src/Store/Actions/providerOptionActions.js +++ b/frontend/src/Store/Actions/providerOptionActions.js @@ -14,7 +14,7 @@ export const section = 'providerOptions'; // State export const defaultState = { - items: [], + items: {}, isFetching: false, isPopulated: false, error: false @@ -43,15 +43,20 @@ export const actionHandlers = handleThunks({ isFetching: true })); + const oldItems = getState().providerOptions.items; + const itemSection = payload.itemSection; + const promise = requestAction(payload); promise.done((data) => { + oldItems[itemSection] = data.options || []; + dispatch(set({ section, isFetching: false, isPopulated: true, error: null, - items: data.options || [] + items: oldItems })); }); diff --git a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs index 4785a382c7..ab0209c00b 100644 --- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs +++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs @@ -20,6 +20,7 @@ public FieldDefinitionAttribute(int order) public Type SelectOptions { get; set; } public string Section { get; set; } public HiddenType Hidden { get; set; } + public string RequestAction { get; set; } } public enum FieldType diff --git a/src/NzbDrone.Core/NetImport/Radarr/RadarrAPIResource.cs b/src/NzbDrone.Core/NetImport/Radarr/RadarrAPIResource.cs index 7778bdb66e..69b967098a 100644 --- a/src/NzbDrone.Core/NetImport/Radarr/RadarrAPIResource.cs +++ b/src/NzbDrone.Core/NetImport/Radarr/RadarrAPIResource.cs @@ -16,6 +16,7 @@ public class RadarrMovie public int Year { get; set; } public string TitleSlug { get; set; } public int QualityProfileId { get; set; } + public HashSet Tags { get; set; } } public class RadarrProfile @@ -23,4 +24,10 @@ public class RadarrProfile public string Name { get; set; } public int Id { get; set; } } + + public class RadarrTag + { + public string Label { get; set; } + public int Id { get; set; } + } } diff --git a/src/NzbDrone.Core/NetImport/Radarr/RadarrImport.cs b/src/NzbDrone.Core/NetImport/Radarr/RadarrImport.cs index d6666858ce..7b44577cdb 100644 --- a/src/NzbDrone.Core/NetImport/Radarr/RadarrImport.cs +++ b/src/NzbDrone.Core/NetImport/Radarr/RadarrImport.cs @@ -41,7 +41,8 @@ public override NetImportFetchResult Fetch() foreach (var remoteMovie in remoteMovies) { - if (!Settings.ProfileIds.Any() || Settings.ProfileIds.Contains(remoteMovie.QualityProfileId)) + if ((!Settings.ProfileIds.Any() || Settings.ProfileIds.Contains(remoteMovie.QualityProfileId)) && + (!Settings.TagIds.Any() || Settings.TagIds.Any(x => remoteMovie.Tags.Any(y => y == x)))) { movies.Add(new Movie { @@ -76,19 +77,19 @@ public override NetImportFetchResult Fetch() public override object RequestAction(string action, IDictionary query) { - if (action == "getDevices") + // Return early if there is not an API key + if (Settings.ApiKey.IsNullOrWhiteSpace()) { - // Return early if there is not an API key - if (Settings.ApiKey.IsNullOrWhiteSpace()) + return new { - return new - { - devices = new List() - }; - } + devices = new List() + }; + } - Settings.Validate().Filter("ApiKey").ThrowOnError(); + Settings.Validate().Filter("ApiKey").ThrowOnError(); + if (action == "getProfiles") + { var devices = _radarrV3Proxy.GetProfiles(Settings); return new @@ -102,6 +103,21 @@ public override object RequestAction(string action, IDictionary }; } + if (action == "getTags") + { + var devices = _radarrV3Proxy.GetTags(Settings); + + return new + { + options = devices.OrderBy(d => d.Label, StringComparer.InvariantCultureIgnoreCase) + .Select(d => new + { + id = d.Id, + name = d.Label + }) + }; + } + return new { }; } diff --git a/src/NzbDrone.Core/NetImport/Radarr/RadarrSettings.cs b/src/NzbDrone.Core/NetImport/Radarr/RadarrSettings.cs index 0b5cbdaa01..095ee1b4fb 100644 --- a/src/NzbDrone.Core/NetImport/Radarr/RadarrSettings.cs +++ b/src/NzbDrone.Core/NetImport/Radarr/RadarrSettings.cs @@ -24,6 +24,7 @@ public RadarrSettings() BaseUrl = ""; ApiKey = ""; ProfileIds = new int[] { }; + TagIds = new int[] { }; } [FieldDefinition(0, Label = "Full URL", HelpText = "URL, including port, of the Radarr V3 instance to import from")] @@ -32,9 +33,12 @@ public RadarrSettings() [FieldDefinition(1, Label = "API Key", HelpText = "Apikey of the Radarr V3 instance to import from")] public string ApiKey { get; set; } - [FieldDefinition(2, Type = FieldType.Device, Label = "Profiles", HelpText = "Profiles from the source instance to import from")] + [FieldDefinition(2, Type = FieldType.Device, RequestAction = "getProfiles", Label = "Profiles", HelpText = "Profiles from the source instance to import from")] public IEnumerable ProfileIds { get; set; } + [FieldDefinition(3, Type = FieldType.Device, RequestAction = "getTags", Label = "Tags", HelpText = "Tags from the source instance to import from")] + public IEnumerable TagIds { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/NetImport/Radarr/RadarrV3Proxy.cs b/src/NzbDrone.Core/NetImport/Radarr/RadarrV3Proxy.cs index 79e152c5f7..ce23e962c7 100644 --- a/src/NzbDrone.Core/NetImport/Radarr/RadarrV3Proxy.cs +++ b/src/NzbDrone.Core/NetImport/Radarr/RadarrV3Proxy.cs @@ -13,6 +13,7 @@ public interface IRadarrV3Proxy { List GetMovies(RadarrSettings settings); List GetProfiles(RadarrSettings settings); + List GetTags(RadarrSettings settings); ValidationFailure Test(RadarrSettings settings); } @@ -37,6 +38,11 @@ public List GetProfiles(RadarrSettings settings) return Execute("/api/v3/qualityprofile", settings); } + public List GetTags(RadarrSettings settings) + { + return Execute("/api/v3/tag", settings); + } + public ValidationFailure Test(RadarrSettings settings) { try diff --git a/src/Radarr.Http/ClientSchema/Field.cs b/src/Radarr.Http/ClientSchema/Field.cs index 560910c7eb..eb8fa0edd8 100644 --- a/src/Radarr.Http/ClientSchema/Field.cs +++ b/src/Radarr.Http/ClientSchema/Field.cs @@ -17,6 +17,7 @@ public class Field public List SelectOptions { get; set; } public string Section { get; set; } public string Hidden { get; set; } + public string RequestAction { get; set; } public Field Clone() { diff --git a/src/Radarr.Http/ClientSchema/SchemaBuilder.cs b/src/Radarr.Http/ClientSchema/SchemaBuilder.cs index eb8efc7dab..6805d12b9b 100644 --- a/src/Radarr.Http/ClientSchema/SchemaBuilder.cs +++ b/src/Radarr.Http/ClientSchema/SchemaBuilder.cs @@ -100,7 +100,8 @@ private static FieldMapping[] GetFieldMapping(Type type, string prefix, Func