diff --git a/frontend/src/Search/SearchIndex.js b/frontend/src/Search/SearchIndex.js
index 3775b46fa..6133ea202 100644
--- a/frontend/src/Search/SearchIndex.js
+++ b/frontend/src/Search/SearchIndex.js
@@ -146,8 +146,8 @@ class SearchIndex extends Component {
this.setState({ jumpToCharacter });
}
- onSearchPress = (query, indexerIds) => {
- this.props.onSearchPress({ query, indexerIds });
+ onSearchPress = (query, indexerIds, categories) => {
+ this.props.onSearchPress({ query, indexerIds, categories });
}
onKeyUp = (event) => {
diff --git a/frontend/src/Store/Actions/Settings/indexerCategories.js b/frontend/src/Store/Actions/Settings/indexerCategories.js
new file mode 100644
index 000000000..2b07a4406
--- /dev/null
+++ b/frontend/src/Store/Actions/Settings/indexerCategories.js
@@ -0,0 +1,48 @@
+import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
+import { createThunk } from 'Store/thunks';
+
+//
+// Variables
+
+const section = 'settings.indexerCategories';
+
+//
+// Actions Types
+
+export const FETCH_INDEXER_CATEGORIES = 'settings/indexerFlags/fetchIndexerCategories';
+
+//
+// Action Creators
+
+export const fetchIndexerCategories = createThunk(FETCH_INDEXER_CATEGORIES);
+
+//
+// Details
+
+export default {
+
+ //
+ // State
+
+ defaultState: {
+ isFetching: false,
+ isPopulated: false,
+ error: null,
+ items: []
+ },
+
+ //
+ // Action Handlers
+
+ actionHandlers: {
+ [FETCH_INDEXER_CATEGORIES]: createFetchHandler(section, '/indexer/categories')
+ },
+
+ //
+ // Reducers
+
+ reducers: {
+
+ }
+
+};
diff --git a/frontend/src/Store/Actions/providerOptionActions.js b/frontend/src/Store/Actions/providerOptionActions.js
index 72a8b1745..4dc38a98f 100644
--- a/frontend/src/Store/Actions/providerOptionActions.js
+++ b/frontend/src/Store/Actions/providerOptionActions.js
@@ -1,3 +1,4 @@
+import _ from 'lodash';
import { createAction } from 'redux-actions';
import { createThunk, handleThunks } from 'Store/thunks';
import requestAction from 'Utilities/requestAction';
@@ -10,11 +11,14 @@ import createHandleActions from './Creators/createHandleActions';
export const section = 'providerOptions';
+const lastActions = {};
+let lastActionId = 0;
+
//
// State
export const defaultState = {
- items: {},
+ items: [],
isFetching: false,
isPopulated: false,
error: false
@@ -23,8 +27,8 @@ export const defaultState = {
//
// Actions Types
-export const FETCH_OPTIONS = 'devices/fetchOptions';
-export const CLEAR_OPTIONS = 'devices/clearOptions';
+export const FETCH_OPTIONS = 'providers/fetchOptions';
+export const CLEAR_OPTIONS = 'providers/clearOptions';
//
// Action Creators
@@ -38,35 +42,55 @@ export const clearOptions = createAction(CLEAR_OPTIONS);
export const actionHandlers = handleThunks({
[FETCH_OPTIONS]: function(getState, payload, dispatch) {
+ const subsection = `${section}.${payload.section}`;
+
+ if (lastActions[payload.section] && _.isEqual(payload, lastActions[payload.section].payload)) {
+ return;
+ }
+
+ const actionId = ++lastActionId;
+
+ lastActions[payload.section] = {
+ actionId,
+ payload
+ };
+
dispatch(set({
- section,
+ section: subsection,
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: oldItems
- }));
+ if (lastActions[payload.section]) {
+ if (lastActions[payload.section].actionId === actionId) {
+ lastActions[payload.section] = null;
+ }
+
+ dispatch(set({
+ section: subsection,
+ isFetching: false,
+ isPopulated: true,
+ error: null,
+ items: data.options || []
+ }));
+ }
});
promise.fail((xhr) => {
- dispatch(set({
- section,
- isFetching: false,
- isPopulated: false,
- error: xhr
- }));
+ if (lastActions[payload.section]) {
+ if (lastActions[payload.section].actionId === actionId) {
+ lastActions[payload.section] = null;
+ }
+
+ dispatch(set({
+ section: subsection,
+ isFetching: false,
+ isPopulated: false,
+ error: xhr
+ }));
+ }
});
}
});
@@ -76,8 +100,12 @@ export const actionHandlers = handleThunks({
export const reducers = createHandleActions({
- [CLEAR_OPTIONS]: function(state) {
- return updateSectionState(state, section, defaultState);
+ [CLEAR_OPTIONS]: function(state, { payload }) {
+ const subsection = `${section}.${payload.section}`;
+
+ lastActions[payload.section] = null;
+
+ return updateSectionState(state, subsection, defaultState);
}
-}, defaultState, section);
+}, {}, section);
diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js
index 4a35d5d48..47c3f573b 100644
--- a/frontend/src/Store/Actions/settingsActions.js
+++ b/frontend/src/Store/Actions/settingsActions.js
@@ -3,6 +3,7 @@ import { handleThunks } from 'Store/thunks';
import createHandleActions from './Creators/createHandleActions';
import applications from './Settings/applications';
import general from './Settings/general';
+import indexerCategories from './Settings/indexerCategories';
import indexerFlags from './Settings/indexerFlags';
import indexerOptions from './Settings/indexerOptions';
import languages from './Settings/languages';
@@ -10,6 +11,7 @@ import notifications from './Settings/notifications';
import ui from './Settings/ui';
export * from './Settings/general';
+export * from './Settings/indexerCategories';
export * from './Settings/indexerFlags';
export * from './Settings/indexerOptions';
export * from './Settings/languages';
@@ -29,6 +31,7 @@ export const defaultState = {
advancedSettings: false,
general: general.defaultState,
+ indexerCategories: indexerCategories.defaultState,
indexerFlags: indexerFlags.defaultState,
indexerOptions: indexerOptions.defaultState,
languages: languages.defaultState,
@@ -56,6 +59,7 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS);
export const actionHandlers = handleThunks({
...general.actionHandlers,
+ ...indexerCategories.actionHandlers,
...indexerFlags.actionHandlers,
...indexerOptions.actionHandlers,
...languages.actionHandlers,
@@ -74,6 +78,7 @@ export const reducers = createHandleActions({
},
...general.reducers,
+ ...indexerCategories.reducers,
...indexerFlags.reducers,
...indexerOptions.reducers,
...languages.reducers,
diff --git a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs
index b887551b4..e68db5a2c 100644
--- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs
+++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs
@@ -19,10 +19,10 @@ namespace NzbDrone.Core.Annotations
public FieldType Type { get; set; }
public bool Advanced { get; set; }
public Type SelectOptions { get; set; }
+ public string SelectOptionsProviderAction { get; set; }
public string Section { get; set; }
public HiddenType Hidden { get; set; }
public PrivacyLevel Privacy { get; set; }
- public string RequestAction { get; set; }
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
@@ -39,6 +39,15 @@ namespace NzbDrone.Core.Annotations
public string Hint { get; set; }
}
+ public class FieldSelectOption
+ {
+ public int Value { get; set; }
+ public string Name { get; set; }
+ public int Order { get; set; }
+ public string Hint { get; set; }
+ public int? ParentValue { get; set; }
+ }
+
public enum FieldType
{
Textbox,
diff --git a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilitiesProvider.cs b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilitiesProvider.cs
index a94ff72ff..5b12bc76e 100644
--- a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilitiesProvider.cs
+++ b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilitiesProvider.cs
@@ -48,6 +48,7 @@ namespace NzbDrone.Core.Indexers.Newznab
}
var request = new HttpRequest(url, HttpAccept.Rss);
+ request.AllowAutoRedirect = true;
HttpResponse response;
diff --git a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRequestGenerator.cs
index 1ca43027a..f0de463aa 100644
--- a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRequestGenerator.cs
+++ b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRequestGenerator.cs
@@ -133,7 +133,7 @@ namespace NzbDrone.Core.Indexers.Newznab
if (categories != null && categories.Any())
{
var categoriesQuery = string.Join(",", categories.Distinct());
- baseUrl += string.Format("&cats={0}", categoriesQuery);
+ baseUrl += string.Format("&cat={0}", categoriesQuery);
}
if (Settings.ApiKey.IsNotNullOrWhiteSpace())
@@ -151,14 +151,7 @@ namespace NzbDrone.Core.Indexers.Newznab
parameters += string.Format("&offset={0}", searchCriteria.Offset);
}
- if (PageSize == 0)
- {
- yield return new IndexerRequest(string.Format("{0}{1}", baseUrl, parameters), HttpAccept.Rss);
- }
- else
- {
- yield return new IndexerRequest(string.Format("{0}&offset={1}&limit={2}{3}", baseUrl, searchCriteria.Offset, searchCriteria.Limit, parameters), HttpAccept.Rss);
- }
+ yield return new IndexerRequest(string.Format("{0}{1}", baseUrl, parameters), HttpAccept.Rss);
}
private static string NewsnabifyTitle(string title)
diff --git a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs
index b9d6ed555..318d0e589 100644
--- a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs
+++ b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs
@@ -358,7 +358,14 @@ namespace NzbDrone.Core.Indexers
_indexerStatusService.UpdateCookies(Definition.Id, cookies, expiration);
};
var generator = GetRequestGenerator();
- var releases = FetchPage(generator.GetSearchRequests(new MovieSearchCriteria()).GetAllTiers().First().First(), parser);
+ var firstRequest = generator.GetSearchRequests(new BasicSearchCriteria()).GetAllTiers().FirstOrDefault()?.FirstOrDefault();
+
+ if (firstRequest == null)
+ {
+ return new ValidationFailure(string.Empty, "No rss feed query available. This may be an issue with the indexer or your indexer category settings.");
+ }
+
+ var releases = FetchPage(firstRequest, parser);
if (releases.Empty())
{
diff --git a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletSettings.cs b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletSettings.cs
index 7d3167987..d4e9d0661 100644
--- a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletSettings.cs
+++ b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletSettings.cs
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Notifications.PushBullet
[FieldDefinition(0, Label = "Access Token", Privacy = PrivacyLevel.ApiKey, HelpLink = "https://www.pushbullet.com/#settings/account")]
public string ApiKey { get; set; }
- [FieldDefinition(1, Label = "Device IDs", HelpText = "List of device IDs (leave blank to send to all devices)", Type = FieldType.Device, RequestAction = "getDevices")]
+ [FieldDefinition(1, Label = "Device IDs", HelpText = "List of device IDs (leave blank to send to all devices)", Type = FieldType.Device)]
public IEnumerable DeviceIds { get; set; }
[FieldDefinition(2, Label = "Channel Tags", HelpText = "List of Channel Tags to send notifications to", Type = FieldType.Tag)]
diff --git a/src/Prowlarr.Api.V1/Indexers/IndexerDefaultCategoriesModule.cs b/src/Prowlarr.Api.V1/Indexers/IndexerDefaultCategoriesModule.cs
new file mode 100644
index 000000000..3420e5c17
--- /dev/null
+++ b/src/Prowlarr.Api.V1/Indexers/IndexerDefaultCategoriesModule.cs
@@ -0,0 +1,19 @@
+using NzbDrone.Core.Indexers;
+using Prowlarr.Api.V1;
+
+namespace NzbDrone.Api.V1.Indexers
+{
+ public class IndexerDefaultCategoriesModule : ProwlarrV1Module
+ {
+ public IndexerDefaultCategoriesModule()
+ : base("/indexer/categories")
+ {
+ Get("/", movie => GetAll());
+ }
+
+ private IndexerCategory[] GetAll()
+ {
+ return NewznabStandardCategory.ParentCats;
+ }
+ }
+}
diff --git a/src/Prowlarr.Api.V1/ProviderModuleBase.cs b/src/Prowlarr.Api.V1/ProviderModuleBase.cs
index 3c49ebf7f..d93045ef7 100644
--- a/src/Prowlarr.Api.V1/ProviderModuleBase.cs
+++ b/src/Prowlarr.Api.V1/ProviderModuleBase.cs
@@ -28,7 +28,7 @@ namespace Prowlarr.Api.V1
Get("schema", x => GetTemplates());
Post("test", x => Test(ReadResourceFromRequest(true)));
Post("testall", x => TestAll());
- Post("action/{action}", x => RequestAction(x.action, ReadResourceFromRequest(true)));
+ Post("action/{action}", x => RequestAction(x.action, ReadResourceFromRequest(true, true)));
GetResourceAll = GetAll;
GetResourceById = GetProviderById;
diff --git a/src/Prowlarr.Api.V1/Search/SearchModule.cs b/src/Prowlarr.Api.V1/Search/SearchModule.cs
index 8208c21d6..766c85404 100644
--- a/src/Prowlarr.Api.V1/Search/SearchModule.cs
+++ b/src/Prowlarr.Api.V1/Search/SearchModule.cs
@@ -35,22 +35,22 @@ namespace Prowlarr.Api.V1.Search
if (indexerIds.Count > 0)
{
- return GetSearchReleases(request.Query, indexerIds);
+ return GetSearchReleases(request.Query, indexerIds, request.Categories);
}
else
{
- return GetSearchReleases(request.Query, null);
+ return GetSearchReleases(request.Query, null, request.Categories);
}
}
return new List();
}
- private List GetSearchReleases(string query, List indexerIds)
+ private List GetSearchReleases(string query, List indexerIds, int[] categories)
{
try
{
- var decisions = _nzbSearhService.Search(new NewznabRequest { q = query, t = "search" }, indexerIds, true).Releases;
+ var decisions = _nzbSearhService.Search(new NewznabRequest { q = query, t = "search", cat = string.Join(",", categories) }, indexerIds, true).Releases;
return MapDecisions(decisions);
}
diff --git a/src/Prowlarr.Http/ClientSchema/Field.cs b/src/Prowlarr.Http/ClientSchema/Field.cs
index ba3d465fd..61db3cfc2 100644
--- a/src/Prowlarr.Http/ClientSchema/Field.cs
+++ b/src/Prowlarr.Http/ClientSchema/Field.cs
@@ -15,9 +15,10 @@ namespace Prowlarr.Http.ClientSchema
public string Type { get; set; }
public bool Advanced { get; set; }
public List SelectOptions { get; set; }
+
+ public string SelectOptionsProviderAction { get; set; }
public string Section { get; set; }
public string Hidden { get; set; }
- public string RequestAction { get; set; }
public Field Clone()
{
diff --git a/src/Prowlarr.Http/ClientSchema/SchemaBuilder.cs b/src/Prowlarr.Http/ClientSchema/SchemaBuilder.cs
index 4febd4be3..0b37bbdbd 100644
--- a/src/Prowlarr.Http/ClientSchema/SchemaBuilder.cs
+++ b/src/Prowlarr.Http/ClientSchema/SchemaBuilder.cs
@@ -100,13 +100,19 @@ namespace Prowlarr.Http.ClientSchema
Order = fieldAttribute.Order,
Advanced = fieldAttribute.Advanced,
Type = fieldAttribute.Type.ToString().FirstCharToLower(),
- Section = fieldAttribute.Section,
- RequestAction = fieldAttribute.RequestAction
+ Section = fieldAttribute.Section
};
if (fieldAttribute.Type == FieldType.Select || fieldAttribute.Type == FieldType.TagSelect)
{
- field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions);
+ if (fieldAttribute.SelectOptionsProviderAction.IsNotNullOrWhiteSpace())
+ {
+ field.SelectOptionsProviderAction = fieldAttribute.SelectOptionsProviderAction;
+ }
+ else
+ {
+ field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions);
+ }
}
if (fieldAttribute.Hidden != HiddenType.Visible)
diff --git a/src/Prowlarr.Http/REST/RestModule.cs b/src/Prowlarr.Http/REST/RestModule.cs
index 89722a699..82bf9c30d 100644
--- a/src/Prowlarr.Http/REST/RestModule.cs
+++ b/src/Prowlarr.Http/REST/RestModule.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
+using FluentValidation.Results;
using Nancy;
using Nancy.Responses.Negotiation;
using Newtonsoft.Json;
@@ -224,7 +225,7 @@ namespace Prowlarr.Http.REST
return Negotiate.WithModel(model).WithStatusCode(statusCode);
}
- protected TResource ReadResourceFromRequest(bool skipValidate = false)
+ protected TResource ReadResourceFromRequest(bool skipValidate = false, bool skipSharedValidate = false)
{
TResource resource;
@@ -242,7 +243,12 @@ namespace Prowlarr.Http.REST
throw new BadRequestException("Request body can't be empty");
}
- var errors = SharedValidator.Validate(resource).Errors.ToList();
+ var errors = new List();
+
+ if (!skipSharedValidate)
+ {
+ errors.AddRange(SharedValidator.Validate(resource).Errors);
+ }
if (Request.Method.Equals("POST", StringComparison.InvariantCultureIgnoreCase) && !skipValidate && !Request.Url.Path.EndsWith("/test", StringComparison.InvariantCultureIgnoreCase))
{