diff --git a/frontend/src/Components/Form/CardigannCaptchaInput.js b/frontend/src/Components/Form/CardigannCaptchaInput.js new file mode 100644 index 000000000..73aef732f --- /dev/null +++ b/frontend/src/Components/Form/CardigannCaptchaInput.js @@ -0,0 +1,84 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import Icon from 'Components/Icon'; +import { icons } from 'Helpers/Props'; +import FormInputButton from './FormInputButton'; +import TextInput from './TextInput'; +import styles from './CaptchaInput.css'; + +function CardigannCaptchaInput(props) { + const { + className, + name, + value, + hasError, + hasWarning, + type, + refreshing, + contentType, + imageData, + onChange, + onRefreshPress + } = props; + + const img = `data:${contentType};base64,${imageData}`; + + return ( +
+
+ + + + + +
+ + { + type === 'image' && +
+ +
+ } +
+ ); +} + +CardigannCaptchaInput.propTypes = { + className: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + hasError: PropTypes.bool, + hasWarning: PropTypes.bool, + type: PropTypes.string, + refreshing: PropTypes.bool.isRequired, + contentType: PropTypes.string.isRequired, + imageData: PropTypes.string, + onChange: PropTypes.func.isRequired, + onRefreshPress: PropTypes.func.isRequired, + onCaptchaChange: PropTypes.func.isRequired +}; + +CardigannCaptchaInput.defaultProps = { + className: styles.input, + value: '' +}; + +export default CardigannCaptchaInput; diff --git a/frontend/src/Components/Form/CardigannCaptchaInputConnector.js b/frontend/src/Components/Form/CardigannCaptchaInputConnector.js new file mode 100644 index 000000000..728755bd9 --- /dev/null +++ b/frontend/src/Components/Form/CardigannCaptchaInputConnector.js @@ -0,0 +1,77 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { getCaptchaCookie, refreshCaptcha, resetCaptcha } from 'Store/Actions/captchaActions'; +import CardigannCaptchaInput from './CardigannCaptchaInput'; + +function createMapStateToProps() { + return createSelector( + (state) => state.captcha, + (captcha) => { + return captcha; + } + ); +} + +const mapDispatchToProps = { + refreshCaptcha, + getCaptchaCookie, + resetCaptcha +}; + +class CardigannCaptchaInputConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.onRefreshPress(); + } + + componentWillUnmount = () => { + this.props.resetCaptcha(); + } + + // + // Listeners + + onRefreshPress = () => { + const { + provider, + providerData, + name, + onChange + } = this.props; + + onChange({ name, value: '' }); + this.props.resetCaptcha(); + this.props.refreshCaptcha({ provider, providerData }); + + } + + // + // Render + + render() { + return ( + + ); + } +} + +CardigannCaptchaInputConnector.propTypes = { + provider: PropTypes.string.isRequired, + providerData: PropTypes.object.isRequired, + name: PropTypes.string.isRequired, + token: PropTypes.string, + onChange: PropTypes.func.isRequired, + refreshCaptcha: PropTypes.func.isRequired, + getCaptchaCookie: PropTypes.func.isRequired, + resetCaptcha: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(CardigannCaptchaInputConnector); diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index 068e831f2..97775c2a7 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -6,6 +6,7 @@ import translate from 'Utilities/String/translate'; import AutoCompleteInput from './AutoCompleteInput'; import AvailabilitySelectInput from './AvailabilitySelectInput'; import CaptchaInputConnector from './CaptchaInputConnector'; +import CardigannCaptchaInputConnector from './CardigannCaptchaInputConnector'; import CheckInput from './CheckInput'; import DeviceInputConnector from './DeviceInputConnector'; import EnhancedSelectInput from './EnhancedSelectInput'; @@ -37,6 +38,9 @@ function getComponent(type) { case inputTypes.CAPTCHA: return CaptchaInputConnector; + case inputTypes.CARDIGANNCAPTCHA: + return CardigannCaptchaInputConnector; + case inputTypes.CHECK: return CheckInput; diff --git a/frontend/src/Components/Form/ProviderFieldFormGroup.js b/frontend/src/Components/Form/ProviderFieldFormGroup.js index 3095bfb7f..3a9a2f5a5 100644 --- a/frontend/src/Components/Form/ProviderFieldFormGroup.js +++ b/frontend/src/Components/Form/ProviderFieldFormGroup.js @@ -10,6 +10,8 @@ function getType({ type, selectOptionsProviderAction }) { switch (type) { case 'captcha': return inputTypes.CAPTCHA; + case 'cardigannCaptcha': + return inputTypes.CARDIGANNCAPTCHA; case 'checkbox': return inputTypes.CHECK; case 'device': diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js index c6b94a87f..ef4af8252 100644 --- a/frontend/src/Helpers/Props/inputTypes.js +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -1,6 +1,7 @@ export const AUTO_COMPLETE = 'autoComplete'; export const AVAILABILITY_SELECT = 'availabilitySelect'; export const CAPTCHA = 'captcha'; +export const CARDIGANNCAPTCHA = 'cardigannCaptcha'; export const CHECK = 'check'; export const DEVICE = 'device'; export const INFO = 'info'; @@ -21,6 +22,7 @@ export const all = [ AUTO_COMPLETE, AVAILABILITY_SELECT, CAPTCHA, + CARDIGANNCAPTCHA, CHECK, DEVICE, INFO, diff --git a/frontend/src/Store/Actions/captchaActions.js b/frontend/src/Store/Actions/captchaActions.js index c83d231b7..a1fa1df2b 100644 --- a/frontend/src/Store/Actions/captchaActions.js +++ b/frontend/src/Store/Actions/captchaActions.js @@ -20,7 +20,10 @@ export const defaultState = { secretToken: null, ray: null, stoken: null, - responseUrl: null + responseUrl: null, + type: null, + contentType: null, + imageData: null }; // diff --git a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/Captcha.cs b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/Captcha.cs new file mode 100644 index 000000000..bed95162d --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/Captcha.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Indexers.Definitions.Cardigann +{ + public class Captcha + { + public string Type { get; set; } = "image"; + public string ContentType { get; set; } + public byte[] ImageData { get; set; } + } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/Cardigann.cs b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/Cardigann.cs index 7e421b8b3..f714fe866 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/Cardigann.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/Cardigann.cs @@ -1,7 +1,10 @@ +using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using FluentValidation.Results; using NLog; +using NzbDrone.Common.Cache; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.IndexerVersions; @@ -13,6 +16,7 @@ namespace NzbDrone.Core.Indexers.Cardigann public class Cardigann : HttpIndexerBase { private readonly IIndexerDefinitionUpdateService _definitionService; + private readonly ICached _generatorCache; public override string Name => "Cardigann"; public override string BaseUrl => ""; @@ -23,21 +27,24 @@ namespace NzbDrone.Core.Indexers.Cardigann public override IIndexerRequestGenerator GetRequestGenerator() { - return new CardigannRequestGenerator(_configService, - _definitionService.GetDefinition(Settings.DefinitionFile), - Settings, - _logger) - { - HttpClient = _httpClient - }; + return _generatorCache.Get(Settings.DefinitionFile, () => + new CardigannRequestGenerator(_configService, + _definitionService.GetDefinition(Settings.DefinitionFile), + _logger) + { + HttpClient = _httpClient, + Settings = Settings + }); } public override IParseIndexerResponse GetParser() { return new CardigannParser(_configService, - _definitionService.GetDefinition(Settings.DefinitionFile), - Settings, - _logger); + _definitionService.GetDefinition(Settings.DefinitionFile), + _logger) + { + Settings = Settings + }; } public override IEnumerable DefaultDefinitions @@ -55,10 +62,12 @@ namespace NzbDrone.Core.Indexers.Cardigann IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, + ICacheManager cacheManager, Logger logger) : base(httpClient, indexerStatusService, configService, logger) { _definitionService = definitionService; + _generatorCache = cacheManager.GetRollingCache(GetType(), "CardigannGeneratorCache", TimeSpan.FromMinutes(5)); } private IndexerDefinition GetDefinition(CardigannMetaDefinition definition) @@ -71,6 +80,16 @@ namespace NzbDrone.Core.Indexers.Cardigann var settings = definition.Settings ?? defaultSettings; + if (definition.Login?.Captcha != null) + { + settings.Add(new SettingsField + { + Name = "cardigannCaptcha", + Type = "cardigannCaptcha", + Label = "CAPTCHA" + }); + } + return new IndexerDefinition { Enable = true, @@ -93,6 +112,8 @@ namespace NzbDrone.Core.Indexers.Cardigann SetCookieFunctions(generator); + generator.Settings = Settings; + return generator.CheckIfLoginIsNeeded(httpResponse); } @@ -102,6 +123,8 @@ namespace NzbDrone.Core.Indexers.Cardigann SetCookieFunctions(generator); + generator.Settings = Settings; + await generator.DoLogin(); } @@ -113,5 +136,21 @@ namespace NzbDrone.Core.Indexers.Cardigann return; } } + + public override object RequestAction(string action, IDictionary query) + { + if (action == "checkCaptcha") + { + var generator = (CardigannRequestGenerator)GetRequestGenerator(); + + var result = generator.GetConfigurationForSetup(false).GetAwaiter().GetResult(); + return new + { + captchaRequest = result + }; + } + + return null; + } } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannBase.cs b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannBase.cs index 93902b9e9..e04a17aa3 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannBase.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannBase.cs @@ -18,7 +18,6 @@ namespace NzbDrone.Core.Indexers.Cardigann public class CardigannBase { protected readonly CardigannDefinition _definition; - protected readonly CardigannSettings _settings; protected readonly Logger _logger; protected readonly Encoding _encoding; protected readonly IConfigService _configService; @@ -48,14 +47,14 @@ namespace NzbDrone.Core.Indexers.Cardigann protected static readonly Regex _LogicFunctionRegex = new Regex( $@"\b({string.Join("|", _SupportedLogicFunctions.Select(Regex.Escape))})(?:\s+(\(?\.[^\)\s]+\)?|""[^""]+"")){{2,}}"); + public CardigannSettings Settings { get; set; } + public CardigannBase(IConfigService configService, CardigannDefinition definition, - CardigannSettings settings, Logger logger) { _configService = configService; _definition = definition; - _settings = settings; _encoding = Encoding.GetEncoding(definition.Encoding); _logger = logger; @@ -224,7 +223,7 @@ namespace NzbDrone.Core.Indexers.Cardigann foreach (var setting in _definition.Settings) { var name = ".Config." + setting.Name; - var value = _settings.ExtraFieldData.GetValueOrDefault(setting.Name, setting.Default); + var value = Settings.ExtraFieldData.GetValueOrDefault(setting.Name, setting.Default); if (setting.Type != "password" && indexerLogging) { @@ -260,6 +259,9 @@ namespace NzbDrone.Core.Indexers.Cardigann { variables[name] = value; } + else if (setting.Type == "cardigannCaptcha") + { + } else { throw new NotSupportedException(); diff --git a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannMetaDef.cs b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannMetaDef.cs index 6fb1645be..18d6ac4e3 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannMetaDef.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannMetaDef.cs @@ -15,5 +15,6 @@ namespace NzbDrone.Core.Indexers.Cardigann public List Legacylinks { get; set; } public List Settings { get; set; } public string Sha { get; set; } + public LoginBlock Login { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannParser.cs b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannParser.cs index 9ed3d296d..d4e81902b 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannParser.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannParser.cs @@ -20,9 +20,8 @@ namespace NzbDrone.Core.Indexers.Cardigann public CardigannParser(IConfigService configService, CardigannDefinition definition, - CardigannSettings settings, Logger logger) - : base(configService, definition, settings, logger) + : base(configService, definition, logger) { } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannRequestGenerator.cs index b74433d88..9f791d74a 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannRequestGenerator.cs @@ -25,9 +25,8 @@ namespace NzbDrone.Core.Indexers.Cardigann public CardigannRequestGenerator(IConfigService configService, CardigannDefinition definition, - CardigannSettings settings, Logger logger) - : base(configService, definition, settings, logger) + : base(configService, definition, logger) { } @@ -181,7 +180,7 @@ namespace NzbDrone.Core.Indexers.Cardigann LogResponseContent = true, Method = HttpMethod.POST, AllowAutoRedirect = true, - SuppressHttpError = true + SuppressHttpError = true, }; foreach (var pair in pairs) @@ -339,46 +338,22 @@ namespace NzbDrone.Core.Indexers.Cardigann if (login.Captcha != null) { var captcha = login.Captcha; - if (captcha.Type == "image") + Settings.ExtraFieldData.TryGetValue("CAPTCHA", out var captchaText); + if (captchaText != null) { - _settings.ExtraFieldData.TryGetValue("CaptchaText", out var captchaText); - if (captchaText != null) + var input = captcha.Input; + if (login.Selectors) { - var input = captcha.Input; - if (login.Selectors) + var inputElement = landingResultDocument.QuerySelector(captcha.Input); + if (inputElement == null) { - var inputElement = landingResultDocument.QuerySelector(captcha.Input); - if (inputElement == null) - { - throw new CardigannConfigException(_definition, string.Format("Login failed: No captcha input found using {0}", captcha.Input)); - } - - input = inputElement.GetAttribute("name"); + throw new CardigannConfigException(_definition, string.Format("Login failed: No captcha input found using {0}", captcha.Input)); } - pairs[input] = (string)captchaText; + input = inputElement.GetAttribute("name"); } - } - if (captcha.Type == "text") - { - _settings.ExtraFieldData.TryGetValue("CaptchaAnswer", out var captchaAnswer); - if (captchaAnswer != null) - { - var input = captcha.Input; - if (login.Selectors) - { - var inputElement = landingResultDocument.QuerySelector(captcha.Input); - if (inputElement == null) - { - throw new CardigannConfigException(_definition, string.Format("Login failed: No captcha input found using {0}", captcha.Input)); - } - - input = inputElement.GetAttribute("name"); - } - - pairs[input] = (string)captchaAnswer; - } + pairs[input] = (string)captchaText; } } @@ -462,7 +437,7 @@ namespace NzbDrone.Core.Indexers.Cardigann else if (login.Method == "cookie") { CookiesUpdater(null, null); - _settings.ExtraFieldData.TryGetValue("cookie", out var cookies); + Settings.ExtraFieldData.TryGetValue("cookie", out var cookies); CookiesUpdater(CookieUtil.CookieHeaderToDictionary((string)cookies), DateTime.Now + TimeSpan.FromDays(30)); } else if (login.Method == "get") @@ -557,13 +532,13 @@ namespace NzbDrone.Core.Indexers.Cardigann return true; } - public async Task GetConfigurationForSetup(bool automaticlogin) + public async Task GetConfigurationForSetup(bool automaticlogin) { var login = _definition.Login; if (login == null || login.Method != "form") { - return; + return null; } var loginUrl = ResolvePath(login.Path); @@ -588,7 +563,9 @@ namespace NzbDrone.Core.Indexers.Cardigann requestBuilder.SetCookies(Cookies); } - landingResult = await HttpClient.ExecuteAsync(requestBuilder.Build()); + var request = requestBuilder.Build(); + + landingResult = await HttpClient.ExecuteAsync(request); Cookies = landingResult.GetCookies(); @@ -597,122 +574,60 @@ namespace NzbDrone.Core.Indexers.Cardigann //{ // await FollowIfRedirect(landingResult, loginUrl.AbsoluteUri, overrideCookies: landingResult.Cookies, accumulateCookies: true); //} - var hasCaptcha = false; var htmlParser = new HtmlParser(); landingResultDocument = htmlParser.ParseDocument(landingResult.Content); + Captcha captcha = null; + if (login.Captcha != null) { - var captcha = login.Captcha; - if (captcha.Type == "image") + captcha = await GetCaptcha(login); + } + + if (captcha != null && automaticlogin) + { + _logger.Error(string.Format("CardigannIndexer ({0}): Found captcha during automatic login, aborting", _definition.Id)); + } + + return captcha; + } + + private async Task GetCaptcha(LoginBlock login) + { + var captcha = login.Captcha; + + if (captcha.Type == "image") + { + var captchaElement = landingResultDocument.QuerySelector(captcha.Selector); + if (captchaElement != null) { - var captchaElement = landingResultDocument.QuerySelector(captcha.Selector); - if (captchaElement != null) - { - hasCaptcha = true; + var loginUrl = ResolvePath(login.Path); + var captchaUrl = ResolvePath(captchaElement.GetAttribute("src"), loginUrl); - //TODO Bubble this to UI when we get a captcha so that user can action it - //Jackett does this by inserting image or question into the extrasettings which then show up in the add modal - // - //var captchaUrl = ResolvePath(captchaElement.GetAttribute("src"), loginUrl); - //var captchaImageData = RequestWithCookiesAsync(captchaUrl.ToString(), landingResult.GetCookies, referer: loginUrl.AbsoluteUri); - // var CaptchaImage = new ImageItem { Name = "Captcha Image" }; - //var CaptchaText = new StringItem { Name = "Captcha Text" }; - //CaptchaImage.Value = captchaImageData.ContentBytes; - //configData.AddDynamic("CaptchaImage", CaptchaImage); - //configData.AddDynamic("CaptchaText", CaptchaText); - } - else - { - _logger.Debug(string.Format("CardigannIndexer ({0}): No captcha image found", _definition.Id)); - } - } - else if (captcha.Type == "text") - { - var captchaElement = landingResultDocument.QuerySelector(captcha.Selector); - if (captchaElement != null) - { - hasCaptcha = true; + var request = new HttpRequestBuilder(captchaUrl.ToString()) + .SetCookies(landingResult.GetCookies()) + .SetHeader("Referrer", loginUrl.AbsoluteUri) + .Build(); - //var captchaChallenge = new DisplayItem(captchaElement.TextContent) { Name = "Captcha Challenge" }; - //var captchaAnswer = new StringItem { Name = "Captcha Answer" }; + var response = await HttpClient.ExecuteAsync(request); - //configData.AddDynamic("CaptchaChallenge", captchaChallenge); - //configData.AddDynamic("CaptchaAnswer", captchaAnswer); - } - else + return new Captcha { - _logger.Debug(string.Format("CardigannIndexer ({0}): No captcha image found", _definition.Id)); - } + ContentType = response.Headers.ContentType, + ImageData = response.ResponseData + }; } else { - throw new NotImplementedException(string.Format("Captcha type \"{0}\" is not implemented", captcha.Type)); + _logger.Debug(string.Format("CardigannIndexer ({0}): No captcha image found", _definition.Id)); } } - - if (hasCaptcha && automaticlogin) + else { - _logger.Error(string.Format("CardigannIndexer ({0}): Found captcha during automatic login, aborting", _definition.Id)); - return; + throw new NotImplementedException(string.Format("Captcha type \"{0}\" is not implemented", captcha.Type)); } - return; - } - - protected async Task TestLogin() - { - var login = _definition.Login; - - if (login == null || login.Test == null) - { - return false; - } - - // test if login was successful - var loginTestUrl = ResolvePath(login.Test.Path).ToString(); - - // var headers = ParseCustomHeaders(_definition.Search?.Headers, GetBaseTemplateVariables()); - var requestBuilder = new HttpRequestBuilder(loginTestUrl) - { - LogResponseContent = true, - Method = HttpMethod.GET, - SuppressHttpError = true - }; - - if (Cookies != null) - { - requestBuilder.SetCookies(Cookies); - } - - var testResult = await HttpClient.ExecuteAsync(requestBuilder.Build()); - - if (testResult.HasHttpRedirect) - { - var errormessage = "Login Failed, got redirected."; - var domainHint = GetRedirectDomainHint(testResult); - if (domainHint != null) - { - errormessage += " Try changing the indexer URL to " + domainHint + "."; - } - - _logger.Debug(errormessage); - return false; - } - - if (login.Test.Selector != null) - { - var testResultParser = new HtmlParser(); - var testResultDocument = testResultParser.ParseDocument(testResult.Content); - var selection = testResultDocument.QuerySelectorAll(login.Test.Selector); - if (selection.Length == 0) - { - _logger.Debug(string.Format("Login failed: Selector \"{0}\" didn't match", login.Test.Selector)); - return false; - } - } - - return true; + return null; } protected string GetRedirectDomainHint(string requestUrl, string redirectUrl) diff --git a/src/NzbDrone.Core/Indexers/IndexerFactory.cs b/src/NzbDrone.Core/Indexers/IndexerFactory.cs index 5b1ab6d75..ecec57ff0 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFactory.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFactory.cs @@ -78,6 +78,17 @@ namespace NzbDrone.Core.Indexers var settings = (CardigannSettings)definition.Settings; var defFile = _definitionService.GetDefinition(settings.DefinitionFile); definition.ExtraFields = defFile.Settings; + + if (defFile.Login?.Captcha != null && !definition.ExtraFields.Any(x => x.Type == "cardigannCaptcha")) + { + definition.ExtraFields.Add(new SettingsField + { + Name = "cardigannCaptcha", + Type = "cardigannCaptcha", + Label = "CAPTCHA" + }); + } + definition.BaseUrl = defFile.Links.First(); definition.Privacy = defFile.Type == "private" ? IndexerPrivacy.Private : IndexerPrivacy.Public; definition.Capabilities = new IndexerCapabilities(); diff --git a/src/Prowlarr.Api.V1/Indexers/IndexerResource.cs b/src/Prowlarr.Api.V1/Indexers/IndexerResource.cs index ebd93c6ce..c18632a35 100644 --- a/src/Prowlarr.Api.V1/Indexers/IndexerResource.cs +++ b/src/Prowlarr.Api.V1/Indexers/IndexerResource.cs @@ -97,8 +97,15 @@ namespace Prowlarr.Api.V1.Indexers { if (!standardFields.Contains(field.Name)) { - var cardigannSetting = cardigannDefinition.Settings.FirstOrDefault(x => x.Name == field.Name); - settings.ExtraFieldData[field.Name] = MapValue(cardigannSetting, field.Value); + if (field.Name == "cardigannCaptcha") + { + settings.ExtraFieldData["CAPTCHA"] = field.Value?.ToString() ?? string.Empty; + } + else + { + var cardigannSetting = cardigannDefinition.Settings.FirstOrDefault(x => x.Name == field.Name); + settings.ExtraFieldData[field.Name] = MapValue(cardigannSetting, field.Value); + } } } } @@ -132,7 +139,7 @@ namespace Prowlarr.Api.V1.Indexers } else { - return value.ToString(); + return value?.ToString() ?? string.Empty; } } diff --git a/src/Prowlarr.Api.V1/ProviderControllerBase.cs b/src/Prowlarr.Api.V1/ProviderControllerBase.cs index 69056edbf..16d187d09 100644 --- a/src/Prowlarr.Api.V1/ProviderControllerBase.cs +++ b/src/Prowlarr.Api.V1/ProviderControllerBase.cs @@ -174,7 +174,7 @@ namespace Prowlarr.Api.V1 var data = _providerFactory.RequestAction(providerDefinition, name, query); - return Content(data.ToJson(), "application/json"); + return Json(data); } protected virtual void Validate(TProviderDefinition definition, bool includeWarnings)