diff --git a/frontend/src/Components/Form/IndexersSelectInputConnector.js b/frontend/src/Components/Form/IndexersSelectInputConnector.js new file mode 100644 index 000000000..032b303de --- /dev/null +++ b/frontend/src/Components/Form/IndexersSelectInputConnector.js @@ -0,0 +1,55 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import EnhancedSelectInput from './EnhancedSelectInput'; + +function createMapStateToProps() { + return createSelector( + (state, { value }) => value, + (state) => state.indexers, + (value, indexers) => { + const values = indexers.items.map(({ id, name }) => { + return { + key: id, + value: name + }; + }); + + return { + value, + values + }; + } + ); +} + +class IndexersSelectInputConnector extends Component { + + onChange = ({ name, value }) => { + this.props.onChange({ name, value }); + } + + // + // Render + + render() { + + return ( + + ); + } +} + +IndexersSelectInputConnector.propTypes = { + name: PropTypes.string.isRequired, + indexerIds: PropTypes.number.isRequired, + value: PropTypes.arrayOf(PropTypes.number).isRequired, + values: PropTypes.arrayOf(PropTypes.object).isRequired, + onChange: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps)(IndexersSelectInputConnector); diff --git a/frontend/src/Indexer/Index/Menus/MovieIndexSortMenu.js b/frontend/src/Indexer/Index/Menus/MovieIndexSortMenu.js index fe006f414..ed65bd980 100644 --- a/frontend/src/Indexer/Index/Menus/MovieIndexSortMenu.js +++ b/frontend/src/Indexer/Index/Menus/MovieIndexSortMenu.js @@ -26,34 +26,16 @@ function MovieIndexSortMenu(props) { sortDirection={sortDirection} onPress={onSortSelect} > - Monitored/Status + Status - {translate('Title')} - - - - {translate('Studio')} - - - - {translate('QualityProfile')} + {translate('Name')} - {translate('Year')} + {'Protocol'} - {translate('InCinemas')} - - - - {translate('PhysicalRelease')} - - - - {translate('DigitalRelease')} - - - - {translate('Path')} - - - - {translate('SizeOnDisk')} - - - - {translate('Certification')} + {'Privacy'} diff --git a/frontend/src/Indexer/Index/Table/CapabilitiesLabel.js b/frontend/src/Indexer/Index/Table/CapabilitiesLabel.js index ae25ed058..144b9e6ad 100644 --- a/frontend/src/Indexer/Index/Table/CapabilitiesLabel.js +++ b/frontend/src/Indexer/Index/Table/CapabilitiesLabel.js @@ -4,16 +4,16 @@ import Label from 'Components/Label'; function CapabilitiesLabel(props) { const { - supportsBooks, - supportsMovies, - supportsMusic, - supportsTv - } = props; + movieSearchAvailable, + tvSearchAvailable, + musicSearchAvailable, + bookSearchAvailable + } = props.capabilities; return ( { - supportsBooks ? + bookSearchAvailable ? : @@ -21,7 +21,7 @@ function CapabilitiesLabel(props) { } { - supportsMovies ? + movieSearchAvailable ? : @@ -29,7 +29,7 @@ function CapabilitiesLabel(props) { } { - supportsMusic ? + musicSearchAvailable ? : @@ -37,7 +37,7 @@ function CapabilitiesLabel(props) { } { - supportsTv ? + tvSearchAvailable ? : @@ -45,7 +45,7 @@ function CapabilitiesLabel(props) { } { - !supportsTv && !supportsMusic && !supportsMovies && !supportsBooks ? + !tvSearchAvailable && !musicSearchAvailable && !movieSearchAvailable && !bookSearchAvailable ? : @@ -56,10 +56,7 @@ function CapabilitiesLabel(props) { } CapabilitiesLabel.propTypes = { - supportsTv: PropTypes.bool.isRequired, - supportsBooks: PropTypes.bool.isRequired, - supportsMusic: PropTypes.bool.isRequired, - supportsMovies: PropTypes.bool.isRequired + capabilities: PropTypes.object.isRequired }; export default CapabilitiesLabel; diff --git a/frontend/src/Indexer/Index/Table/MovieIndexRow.js b/frontend/src/Indexer/Index/Table/MovieIndexRow.js index 0d60a7f21..65e6e6db0 100644 --- a/frontend/src/Indexer/Index/Table/MovieIndexRow.js +++ b/frontend/src/Indexer/Index/Table/MovieIndexRow.js @@ -64,10 +64,7 @@ class MovieIndexRow extends Component { protocol, privacy, added, - supportsTv, - supportsBooks, - supportsMusic, - supportsMovies, + capabilities, columns, isMovieEditorActive, isSelected, @@ -175,10 +172,7 @@ class MovieIndexRow extends Component { className={styles[column.name]} > ); @@ -243,10 +237,7 @@ MovieIndexRow.propTypes = { enableRss: PropTypes.bool.isRequired, enableAutomaticSearch: PropTypes.bool.isRequired, enableInteractiveSearch: PropTypes.bool.isRequired, - supportsTv: PropTypes.bool.isRequired, - supportsBooks: PropTypes.bool.isRequired, - supportsMusic: PropTypes.bool.isRequired, - supportsMovies: PropTypes.bool.isRequired, + capabilities: PropTypes.object.isRequired, added: PropTypes.string.isRequired, tags: PropTypes.arrayOf(PropTypes.number).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/Search/NoSearchResults.css b/frontend/src/Search/NoSearchResults.css new file mode 100644 index 000000000..eff6272f7 --- /dev/null +++ b/frontend/src/Search/NoSearchResults.css @@ -0,0 +1,6 @@ +.message { + margin-top: 10px; + margin-bottom: 30px; + text-align: center; + font-size: 20px; +} diff --git a/frontend/src/Search/NoSearchResults.js b/frontend/src/Search/NoSearchResults.js new file mode 100644 index 000000000..51204959d --- /dev/null +++ b/frontend/src/Search/NoSearchResults.js @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import translate from 'Utilities/String/translate'; +import styles from './NoSearchResults.css'; + +function NoSearchResults(props) { + const { totalItems } = props; + + if (totalItems > 0) { + return ( +
+
+ {translate('AllIndexersHiddenDueToFilter')} +
+
+ ); + } + + return ( +
+
+ No search results found, try performing a new search below. +
+
+ ); +} + +NoSearchResults.propTypes = { + totalItems: PropTypes.number.isRequired +}; + +export default NoSearchResults; diff --git a/frontend/src/Search/SearchFooter.css b/frontend/src/Search/SearchFooter.css index 59049c184..0dbb98cb3 100644 --- a/frontend/src/Search/SearchFooter.css +++ b/frontend/src/Search/SearchFooter.css @@ -3,6 +3,11 @@ min-width: 150px; } +.indexerContainer { + margin-right: 20px; + min-width: 250px; +} + .buttonContainer { display: flex; justify-content: flex-end; diff --git a/frontend/src/Search/SearchFooter.js b/frontend/src/Search/SearchFooter.js index f1131c494..01d8b260d 100644 --- a/frontend/src/Search/SearchFooter.js +++ b/frontend/src/Search/SearchFooter.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import IndexersSelectInputConnector from 'Components/Form/IndexersSelectInputConnector'; import TextInput from 'Components/Form/TextInput'; import SpinnerButton from 'Components/Link/SpinnerButton'; import PageContentFooter from 'Components/Page/PageContentFooter'; @@ -15,7 +16,8 @@ class SearchFooter extends Component { this.state = { searchingReleases: false, - searchQuery: '' + searchQuery: '', + indexerIds: [] }; } @@ -40,7 +42,7 @@ class SearchFooter extends Component { } onSearchPress = () => { - this.props.onSearchPress(this.state.searchQuery); + this.props.onSearchPress(this.state.searchQuery, this.state.indexerIds); } // @@ -52,7 +54,8 @@ class SearchFooter extends Component { } = this.props; const { - searchQuery + searchQuery, + indexerIds } = this.state; return ( @@ -67,6 +70,16 @@ class SearchFooter extends Component { /> +
+ +
+
diff --git a/frontend/src/Search/SearchIndex.js b/frontend/src/Search/SearchIndex.js index 21bf3d36b..a4a4c5ef4 100644 --- a/frontend/src/Search/SearchIndex.js +++ b/frontend/src/Search/SearchIndex.js @@ -11,13 +11,13 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import { align, icons, sortDirections } from 'Helpers/Props'; -import NoIndexer from 'Indexer/NoIndexer'; import * as keyCodes from 'Utilities/Constants/keyCodes'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import translate from 'Utilities/String/translate'; import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu'; import SearchIndexSortMenu from './Menus/SearchIndexSortMenu'; +import NoSearchResults from './NoSearchResults'; import SearchFooter from './SearchFooter.js'; import SearchIndexTableConnector from './Table/SearchIndexTableConnector'; import styles from './SearchIndex.css'; @@ -126,9 +126,8 @@ class SearchIndex extends Component { this.setState({ jumpToCharacter }); } - onSearchPress = (query) => { - console.log('index', query); - this.props.onSearchPress({ query }); + onSearchPress = (query, indexerIds) => { + this.props.onSearchPress({ query, indexerIds }); } onKeyUp = (event) => { @@ -175,6 +174,8 @@ class SearchIndex extends Component { const isLoaded = !!(!error && isPopulated && items.length && scroller); const hasNoIndexer = !totalItems; + console.log(hasNoIndexer); + return ( @@ -246,8 +247,8 @@ class SearchIndex extends Component { } { - !error && isPopulated && !items.length && - + !error && !isFetching && !items.length && + } diff --git a/frontend/src/Store/Actions/indexerIndexActions.js b/frontend/src/Store/Actions/indexerIndexActions.js index 291970dec..0f3a7bd2e 100644 --- a/frontend/src/Store/Actions/indexerIndexActions.js +++ b/frontend/src/Store/Actions/indexerIndexActions.js @@ -95,25 +95,7 @@ export const defaultState = { ], sortPredicates: { - ...sortPredicates, - - studio: function(item) { - const studio = item.studio; - - return studio ? studio.toLowerCase() : ''; - }, - - collection: function(item) { - const { collection ={} } = item; - - return collection.name; - }, - - ratings: function(item) { - const { ratings = {} } = item; - - return ratings.value; - } + ...sortPredicates }, selectedFilterKey: 'all', @@ -122,12 +104,6 @@ export const defaultState = { filterPredicates, filterBuilderProps: [ - { - name: 'monitored', - label: translate('Monitored'), - type: filterBuilderTypes.EXACT, - valueType: filterBuilderValueTypes.BOOL - }, { name: 'title', label: 'Indexer Name', diff --git a/frontend/src/Store/Actions/movieActions.js b/frontend/src/Store/Actions/movieActions.js index 4aa7d55fc..5be6399da 100644 --- a/frontend/src/Store/Actions/movieActions.js +++ b/frontend/src/Store/Actions/movieActions.js @@ -1,11 +1,10 @@ import _ from 'lodash'; import { createAction } from 'redux-actions'; -import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props'; +import { sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; // import { batchActions } from 'redux-batched-actions'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate'; -import padNumber from 'Utilities/Number/padNumber'; import translate from 'Utilities/String/translate'; import { updateItem } from './baseActions'; import createFetchHandler from './Creators/createFetchHandler'; @@ -24,123 +23,12 @@ export const filters = [ key: 'all', label: translate('All'), filters: [] - }, - { - key: 'monitored', - label: translate('MonitoredOnly'), - filters: [ - { - key: 'monitored', - value: true, - type: filterTypes.EQUAL - } - ] - }, - { - key: 'unmonitored', - label: translate('Unmonitored'), - filters: [ - { - key: 'monitored', - value: false, - type: filterTypes.EQUAL - } - ] - }, - { - key: 'missing', - label: translate('Missing'), - filters: [ - { - key: 'monitored', - value: true, - type: filterTypes.EQUAL - }, - { - key: 'hasFile', - value: false, - type: filterTypes.EQUAL - } - ] - }, - { - key: 'wanted', - label: translate('Wanted'), - filters: [ - { - key: 'monitored', - value: true, - type: filterTypes.EQUAL - }, - { - key: 'hasFile', - value: false, - type: filterTypes.EQUAL - }, - { - key: 'isAvailable', - value: true, - type: filterTypes.EQUAL - } - ] - }, - { - key: 'cutoffunmet', - label: translate('CutoffUnmet'), - filters: [ - { - key: 'monitored', - value: true, - type: filterTypes.EQUAL - }, - { - key: 'hasFile', - value: true, - type: filterTypes.EQUAL - }, - { - key: 'qualityCutoffNotMet', - value: true, - type: filterTypes.EQUAL - } - ] } ]; export const filterPredicates = { added: function(item, filterValue, type) { return dateFilterPredicate(item.added, filterValue, type); - }, - - collection: function(item, filterValue, type) { - const predicate = filterTypePredicates[type]; - const { collection } = item; - - return predicate(collection ? collection.name : '', filterValue); - }, - - inCinemas: function(item, filterValue, type) { - return dateFilterPredicate(item.inCinemas, filterValue, type); - }, - - physicalRelease: function(item, filterValue, type) { - return dateFilterPredicate(item.physicalRelease, filterValue, type); - }, - - digitalRelease: function(item, filterValue, type) { - return dateFilterPredicate(item.digitalRelease, filterValue, type); - }, - - ratings: function(item, filterValue, type) { - const predicate = filterTypePredicates[type]; - - return predicate(item.ratings.value * 10, filterValue); - }, - - qualityCutoffNotMet: function(item) { - const { movieFile = {} } = item; - - return movieFile.qualityCutoffNotMet; } }; @@ -165,33 +53,6 @@ export const sortPredicates = { } return result; - }, - - movieStatus: function(item) { - let result = 0; - let qualityName = ''; - - const hasMovieFile = !!item.movieFile; - - if (item.isAvailable) { - result++; - } - - if (item.monitored) { - result += 2; - } - - if (hasMovieFile) { - // TODO: Consider Quality Weight for Sorting within status of hasMovie - if (item.movieFile.qualityCutoffNotMet) { - result += 4; - } else { - result += 8; - } - qualityName = item.movieFile.quality.quality.name; - } - - return padNumber(result.toString(), 2) + qualityName; } }; @@ -205,7 +66,7 @@ export const defaultState = { isSaving: false, saveError: null, items: [], - sortKey: 'sortTitle', + sortKey: 'name', sortDirection: sortDirections.ASCENDING, pendingChanges: {} }; diff --git a/frontend/src/Store/Actions/movieTitlesActions.js b/frontend/src/Store/Actions/movieTitlesActions.js deleted file mode 100644 index 8a53ae87b..000000000 --- a/frontend/src/Store/Actions/movieTitlesActions.js +++ /dev/null @@ -1,74 +0,0 @@ -import { batchActions } from 'redux-batched-actions'; -import { createThunk, handleThunks } from 'Store/thunks'; -import createAjaxRequest from 'Utilities/createAjaxRequest'; -import { set, update } from './baseActions'; -import createHandleActions from './Creators/createHandleActions'; - -// -// Variables - -export const section = 'movieTitles'; - -// -// State - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - items: [] -}; - -// -// Actions Types - -export const FETCH_MOVIE_TITLES = 'movieTitles/fetchMovieTitles'; - -// -// Action Creators - -export const fetchMovieTitles = createThunk(FETCH_MOVIE_TITLES); - -// -// Action Handlers - -export const actionHandlers = handleThunks({ - - [FETCH_MOVIE_TITLES]: function(getState, payload, dispatch) { - dispatch(set({ section, isFetching: true })); - - const promise = createAjaxRequest({ - url: '/alttitle', - data: payload - }).request; - - promise.done((data) => { - dispatch(batchActions([ - update({ section, data }), - - set({ - section, - isFetching: false, - isPopulated: true, - error: null - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isFetching: false, - isPopulated: false, - error: xhr - })); - }); - } -}); - -// -// Reducers - -export const reducers = createHandleActions({ - -}, defaultState, section); diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabCapabilitiesProviderFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabCapabilitiesProviderFixture.cs index ff9731ac4..bd9d316bb 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabCapabilitiesProviderFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabCapabilitiesProviderFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Xml; using FluentAssertions; using Moq; @@ -6,7 +6,6 @@ using NUnit.Framework; using NzbDrone.Common.Http; using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.IndexerTests.NewznabTests { @@ -53,8 +52,8 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests var caps = Subject.GetCapabilities(_settings); - caps.DefaultPageSize.Should().Be(25); - caps.MaxPageSize.Should().Be(60); + caps.LimitsDefault.Value.Should().Be(25); + caps.LimitsMax.Value.Should().Be(60); } [Test] @@ -64,8 +63,8 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests var caps = Subject.GetCapabilities(_settings); - caps.DefaultPageSize.Should().Be(100); - caps.MaxPageSize.Should().Be(100); + caps.LimitsDefault.Value.Should().Be(100); + caps.LimitsMax.Value.Should().Be(100); } [Test] diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs index e31bfc35b..71dafecfc 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using FluentAssertions; using Moq; @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests [TestFixture] public class NewznabFixture : CoreTest { - private NewznabCapabilities _caps; + private IndexerCapabilities _caps; [SetUp] public void Setup() @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests } }; - _caps = new NewznabCapabilities(); + _caps = new IndexerCapabilities(); Mocker.GetMock() .Setup(v => v.GetCapabilities(It.IsAny())) .Returns(_caps); @@ -64,8 +64,8 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests [Test] public void should_use_pagesize_reported_by_caps() { - _caps.MaxPageSize = 30; - _caps.DefaultPageSize = 25; + _caps.LimitsMax = 30; + _caps.LimitsDefault = 25; Subject.PageSize.Should().Be(25); } diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs index 14c99d4f2..89b86cef4 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs @@ -3,6 +3,7 @@ using System.Linq; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Test.Framework; @@ -12,7 +13,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests public class NewznabRequestGeneratorFixture : CoreTest { private MovieSearchCriteria _movieSearchCriteria; - private NewznabCapabilities _capabilities; + private IndexerCapabilities _capabilities; [SetUp] public void SetUp() @@ -29,7 +30,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests SceneTitles = new List { "Star Wars" } }; - _capabilities = new NewznabCapabilities(); + _capabilities = new IndexerCapabilities(); Mocker.GetMock() .Setup(v => v.GetCapabilities(It.IsAny())) @@ -91,7 +92,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests [Test] public void should_not_search_by_imdbid_if_not_supported() { - _capabilities.SupportedMovieSearchParameters = new[] { "q" }; + _capabilities.MovieSearchParams = new List { MovieSearchParam.Q }; var results = Subject.GetSearchRequests(_movieSearchCriteria); @@ -106,7 +107,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests [Test] public void should_search_by_imdbid_if_supported() { - _capabilities.SupportedMovieSearchParameters = new[] { "q", "imdbid" }; + _capabilities.MovieSearchParams = new List { MovieSearchParam.Q, MovieSearchParam.ImdbId }; var results = Subject.GetSearchRequests(_movieSearchCriteria); results.GetTier(0).Should().HaveCount(1); @@ -119,7 +120,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests [Test] public void should_search_by_tmdbid_if_supported() { - _capabilities.SupportedMovieSearchParameters = new[] { "q", "tmdbid" }; + _capabilities.MovieSearchParams = new List { MovieSearchParam.Q, MovieSearchParam.TmdbId }; var results = Subject.GetSearchRequests(_movieSearchCriteria); results.GetTier(0).Should().HaveCount(1); @@ -132,7 +133,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests [Test] public void should_prefer_search_by_tmdbid_if_rid_supported() { - _capabilities.SupportedMovieSearchParameters = new[] { "q", "tmdbid", "imdbid" }; + _capabilities.MovieSearchParams = new List { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId }; var results = Subject.GetSearchRequests(_movieSearchCriteria); results.GetTier(0).Should().HaveCount(1); @@ -146,8 +147,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests [Test] public void should_use_aggregrated_id_search_if_supported() { - _capabilities.SupportedMovieSearchParameters = new[] { "q", "tmdbid", "imdbid" }; - _capabilities.SupportsAggregateIdSearch = true; + _capabilities.MovieSearchParams = new List { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId }; var results = Subject.GetSearchRequests(_movieSearchCriteria); results.GetTier(0).Should().HaveCount(1); @@ -161,8 +161,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests [Test] public void should_not_use_aggregrated_id_search_if_no_ids_supported() { - _capabilities.SupportedMovieSearchParameters = new[] { "q" }; - _capabilities.SupportsAggregateIdSearch = true; // Turns true if indexer supplies supportedParams. + _capabilities.MovieSearchParams = new List { MovieSearchParam.Q }; var results = Subject.GetSearchRequests(_movieSearchCriteria); results.Tiers.Should().Be(1); @@ -176,8 +175,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests [Test] public void should_not_use_aggregrated_id_search_if_no_ids_are_known() { - _capabilities.SupportedMovieSearchParameters = new[] { "q", "imdbid" }; - _capabilities.SupportsAggregateIdSearch = true; // Turns true if indexer supplies supportedParams. + _capabilities.MovieSearchParams = new List { MovieSearchParam.Q, MovieSearchParam.ImdbId }; _movieSearchCriteria.ImdbId = null; @@ -191,8 +189,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests [Test] public void should_fallback_to_q() { - _capabilities.SupportedMovieSearchParameters = new[] { "q", "tmdbid", "imdbid" }; - _capabilities.SupportsAggregateIdSearch = true; + _capabilities.MovieSearchParams = new List { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId }; var results = Subject.GetSearchRequests(_movieSearchCriteria); results.Tiers.Should().Be(2); diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabSettingFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabSettingFixture.cs index 51e2227d4..33eee6e54 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabSettingFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabSettingFixture.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.Test.Framework; diff --git a/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs index d65c92660..f4dd499d1 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using FluentAssertions; using Moq; @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests [TestFixture] public class TorznabFixture : CoreTest { - private NewznabCapabilities _caps; + private IndexerCapabilities _caps; [SetUp] public void Setup() @@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests } }; - _caps = new NewznabCapabilities(); + _caps = new IndexerCapabilities(); Mocker.GetMock() .Setup(v => v.GetCapabilities(It.IsAny())) .Returns(_caps); @@ -129,8 +129,8 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests [Test] public void should_use_pagesize_reported_by_caps() { - _caps.MaxPageSize = 30; - _caps.DefaultPageSize = 25; + _caps.LimitsMax = 30; + _caps.LimitsDefault = 25; Subject.PageSize.Should().Be(25); } diff --git a/src/NzbDrone.Core.Test/Prowlarr.Core.Test.csproj b/src/NzbDrone.Core.Test/Prowlarr.Core.Test.csproj index f18d88820..929199a29 100644 --- a/src/NzbDrone.Core.Test/Prowlarr.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/Prowlarr.Core.Test.csproj @@ -10,6 +10,7 @@ + diff --git a/src/NzbDrone.Core.Test/ThingiProviderTests/ProviderBaseFixture.cs b/src/NzbDrone.Core.Test/ThingiProviderTests/ProviderBaseFixture.cs index 1faf6d750..600206a24 100644 --- a/src/NzbDrone.Core.Test/ThingiProviderTests/ProviderBaseFixture.cs +++ b/src/NzbDrone.Core.Test/ThingiProviderTests/ProviderBaseFixture.cs @@ -1,4 +1,4 @@ -using FizzWare.NBuilder; +using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Indexers; diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 94f96d974..ef766fbca 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -7,7 +7,6 @@ using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; using NzbDrone.Core.CustomFilters; using NzbDrone.Core.Datastore.Converters; -using NzbDrone.Core.History; using NzbDrone.Core.Indexers; using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Jobs; @@ -82,7 +81,6 @@ namespace NzbDrone.Core.Datastore SqlMapper.AddTypeHandler(new DapperLanguageIntConverter()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>(new LanguageIntConverter())); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); - SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter(new LanguageIntConverter())); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); SqlMapper.AddTypeHandler(new OsPathConverter()); diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs index 2a8ea0f5a..0836089b7 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs @@ -13,11 +13,11 @@ namespace NzbDrone.Core.IndexerSearch.Definitions private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled); public List SceneTitles { get; set; } - public virtual bool MonitoredEpisodesOnly { get; set; } public virtual bool UserInvokedSearch { get; set; } public virtual bool InteractiveSearch { get; set; } public string ImdbId { get; set; } public int TmdbId { get; set; } + public List IndexerIds { get; set; } public List QueryTitles => SceneTitles.Select(GetQueryTitle).ToList(); diff --git a/src/NzbDrone.Core/IndexerSearch/MoviesSearchCommand.cs b/src/NzbDrone.Core/IndexerSearch/MoviesSearchCommand.cs deleted file mode 100644 index 7c3b00e5e..000000000 --- a/src/NzbDrone.Core/IndexerSearch/MoviesSearchCommand.cs +++ /dev/null @@ -1,11 +0,0 @@ -using NzbDrone.Core.Messaging.Commands; - -namespace NzbDrone.Core.IndexerSearch -{ - public class MoviesSearchCommand : Command - { - public string SearchTerm { get; set; } - - public override bool SendUpdatesToClient => true; - } -} diff --git a/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs b/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs deleted file mode 100644 index 7d9b52b47..000000000 --- a/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs +++ /dev/null @@ -1,26 +0,0 @@ -using NLog; -using NzbDrone.Common.Instrumentation.Extensions; -using NzbDrone.Core.Messaging.Commands; - -namespace NzbDrone.Core.IndexerSearch -{ - public class MovieSearchService : IExecute - { - private readonly ISearchForNzb _nzbSearchService; - private readonly Logger _logger; - - public MovieSearchService(ISearchForNzb nzbSearchService, - Logger logger) - { - _nzbSearchService = nzbSearchService; - _logger = logger; - } - - public void Execute(MoviesSearchCommand message) - { - var decisions = _nzbSearchService.MovieSearch(message.SearchTerm, false, false); - - _logger.ProgressInfo("Movie search completed. {0} reports downloaded.", decisions.Count); - } - } -} diff --git a/src/NzbDrone.Core/IndexerSearch/NewznabRequest.cs b/src/NzbDrone.Core/IndexerSearch/NewznabRequest.cs new file mode 100644 index 000000000..81f900959 --- /dev/null +++ b/src/NzbDrone.Core/IndexerSearch/NewznabRequest.cs @@ -0,0 +1,28 @@ +namespace NzbDrone.Core.IndexerSearch +{ + public class NewznabRequest + { + public int id { get; set; } + public string t { get; set; } + public string q { get; set; } + public string cat { get; set; } + public string imdbid { get; set; } + public string tmdbid { get; set; } + public string extended { get; set; } + public string limit { get; set; } + public string offset { get; set; } + public string rid { get; set; } + public string tvdbid { get; set; } + public string season { get; set; } + public string ep { get; set; } + public string album { get; set; } + public string artist { get; set; } + public string label { get; set; } + public string track { get; set; } + public string year { get; set; } + public string genre { get; set; } + public string author { get; set; } + public string title { get; set; } + public string configured { get; set; } + } +} diff --git a/src/NzbDrone.Core/IndexerSearch/NewznabResults.cs b/src/NzbDrone.Core/IndexerSearch/NewznabResults.cs new file mode 100644 index 000000000..58493d90d --- /dev/null +++ b/src/NzbDrone.Core/IndexerSearch/NewznabResults.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Xml.Linq; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.IndexerSearch +{ + public class NewznabResults + { + private static readonly XNamespace _AtomNs = "http://www.w3.org/2005/Atom"; + private static readonly XNamespace _TorznabNs = "http://torznab.com/schemas/2015/feed"; + + // filters control characters but allows only properly-formed surrogate sequences + // https://stackoverflow.com/a/961504 + private static readonly Regex _InvalidXmlChars = new Regex( + @"(? Releases; + + private static string RemoveInvalidXMLChars(string text) + { + if (text == null) + { + return null; + } + + return _InvalidXmlChars.Replace(text, ""); + } + + private static string XmlDateFormat(DateTime dt) + { + Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); + + //Sat, 14 Mar 2015 17:10:42 -0400 + return $"{dt:ddd, dd MMM yyyy HH:mm:ss} " + $"{dt:zzz}".Replace(":", ""); + } + + private static XElement GetTorznabElement(string name, object value) + { + if (value == null) + { + return null; + } + + return new XElement(_TorznabNs + "attr", new XAttribute("name", name), new XAttribute("value", value)); + } + + public string ToXml() + { + // IMPORTANT: We can't use Uri.ToString(), because it generates URLs without URL encode (links with unicode + // characters are broken). We must use Uri.AbsoluteUri instead that handles encoding correctly + var xdoc = new XDocument( + new XDeclaration("1.0", "UTF-8", null), + new XElement("rss", + new XAttribute("version", "1.0"), + new XAttribute(XNamespace.Xmlns + "atom", _AtomNs.NamespaceName), + new XAttribute(XNamespace.Xmlns + "torznab", _TorznabNs.NamespaceName), + new XElement("channel", + new XElement(_AtomNs + "link", + new XAttribute("rel", "self"), + new XAttribute("type", "application/rss+xml")), + new XElement("title", "Prowlarr"), + from r in Releases + let t = (r as TorrentInfo) ?? new TorrentInfo() + select new XElement("item", + new XElement("title", RemoveInvalidXMLChars(r.Title)), + new XElement("guid", r.Guid), // GUID and (Link or Magnet) are mandatory + new XElement("prowlarrindexer", new XAttribute("id", r.IndexerId), r.Indexer), + r.PublishDate == DateTime.MinValue ? new XElement("pubDate", XmlDateFormat(DateTime.Now)) : new XElement("pubDate", XmlDateFormat(r.PublishDate)), + new XElement("size", r.Size), + new XElement( + "enclosure", + new XAttribute("length", r.Size), + new XAttribute("type", "application/x-bittorrent")), + GetTorznabElement("rageid", r.TvRageId), + GetTorznabElement("thetvdb", r.TvdbId), + GetTorznabElement("imdb", r.ImdbId.ToString("D7")), + GetTorznabElement("tmdb", r.TmdbId), + GetTorznabElement("seeders", t.Seeders), + GetTorznabElement("peers", t.Peers), + GetTorznabElement("infohash", RemoveInvalidXMLChars(r.Guid)))))); + + return xdoc.Declaration + Environment.NewLine + xdoc; + } + } +} diff --git a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs index b723c7322..df0e64102 100644 --- a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using NLog; using NzbDrone.Common.Extensions; @@ -13,7 +14,8 @@ namespace NzbDrone.Core.IndexerSearch { public interface ISearchForNzb { - List MovieSearch(string movieId, bool userInvokedSearch, bool interactiveSearch); + List Search(string query, List indexerIds, bool userInvokedSearch, bool interactiveSearch); + NewznabResults Search(NewznabRequest request, List indexerIds, bool userInvokedSearch, bool interactiveSearch); } public class NzbSearchService : ISearchForNzb @@ -28,14 +30,21 @@ namespace NzbDrone.Core.IndexerSearch _logger = logger; } - public List MovieSearch(string movie, bool userInvokedSearch, bool interactiveSearch) + public List Search(string query, List indexerIds, bool userInvokedSearch, bool interactiveSearch) { - var searchSpec = Get(movie, userInvokedSearch, interactiveSearch); + var searchSpec = Get(query, indexerIds, userInvokedSearch, interactiveSearch); return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); } - private TSpec Get(string movie, bool userInvokedSearch, bool interactiveSearch) + public NewznabResults Search(NewznabRequest request, List indexerIds, bool userInvokedSearch, bool interactiveSearch) + { + var searchSpec = Get(request.q, indexerIds, userInvokedSearch, interactiveSearch); + + return new NewznabResults { Releases = Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) }; + } + + private TSpec Get(string query, List indexerIds, bool userInvokedSearch, bool interactiveSearch) where TSpec : SearchCriteriaBase, new() { var spec = new TSpec() @@ -44,7 +53,8 @@ namespace NzbDrone.Core.IndexerSearch InteractiveSearch = interactiveSearch }; - spec.SceneTitles = new List { movie }; + spec.SceneTitles = new List { query }; + spec.IndexerIds = indexerIds; return spec; } @@ -55,6 +65,11 @@ namespace NzbDrone.Core.IndexerSearch _indexerFactory.InteractiveSearchEnabled() : _indexerFactory.AutomaticSearchEnabled(); + if (criteriaBase.IndexerIds != null && criteriaBase.IndexerIds.Count > 0) + { + indexers = indexers.Where(i => criteriaBase.IndexerIds.Contains(i.Definition.Id)).ToList(); + } + var reports = new List(); _logger.ProgressInfo("Searching {0} indexers for {1}", indexers.Count, criteriaBase.QueryTitles.Join(", ")); diff --git a/src/NzbDrone.Core/Indexers/Definitions/Newznab/Newznab.cs b/src/NzbDrone.Core/Indexers/Definitions/Newznab/Newznab.cs index 3cf28551a..055f6d2fc 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Newznab/Newznab.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Newznab/Newznab.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Indexers.Newznab public override DownloadProtocol Protocol => DownloadProtocol.Usenet; public override IndexerPrivacy Privacy => IndexerPrivacy.Private; - public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings).DefaultPageSize; + public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings).LimitsDefault.Value; public override IIndexerRequestGenerator GetRequestGenerator() { @@ -36,6 +36,14 @@ namespace NzbDrone.Core.Indexers.Newznab return new NewznabRssParser(Settings); } + public override IndexerCapabilities GetCapabilities() + { + // TODO: This uses indexer capabilities when called so we don't have to keep up with all of them + // however, this is not pulled on a all pull from UI, doing so will kill the UI load if an indexer is down + // should we just purge and manage + return _capabilitiesProvider.GetCapabilities(Settings); + } + public override IEnumerable DefaultDefinitions { get @@ -75,7 +83,8 @@ namespace NzbDrone.Core.Indexers.Newznab Protocol = DownloadProtocol.Usenet, Privacy = IndexerPrivacy.Private, SupportsRss = SupportsRss, - SupportsSearch = SupportsSearch + SupportsSearch = SupportsSearch, + Capabilities = Capabilities }; } @@ -107,15 +116,15 @@ namespace NzbDrone.Core.Indexers.Newznab failures.AddIfNotNull(TestCapabilities()); } - protected static List CategoryIds(List categories) + protected static List CategoryIds(List categories) { var l = categories.Select(c => c.Id).ToList(); foreach (var category in categories) { - if (category.Subcategories != null) + if (category.SubCategories != null) { - l.AddRange(CategoryIds(category.Subcategories)); + l.AddRange(CategoryIds(category.SubCategories)); } } @@ -139,14 +148,14 @@ namespace NzbDrone.Core.Indexers.Newznab } } - if (capabilities.SupportedSearchParameters != null && capabilities.SupportedSearchParameters.Contains("q")) + if (capabilities.SearchParams != null && capabilities.SearchParams.Contains(SearchParam.Q)) { return null; } - if (capabilities.SupportedMovieSearchParameters != null && - new[] { "q", "imdbid" }.Any(v => capabilities.SupportedMovieSearchParameters.Contains(v)) && - new[] { "imdbtitle", "imdbyear" }.All(v => capabilities.SupportedMovieSearchParameters.Contains(v))) + if (capabilities.MovieSearchParams != null && + new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId }.Any(v => capabilities.MovieSearchParams.Contains(v)) && + new[] { MovieSearchParam.ImdbTitle, MovieSearchParam.ImdbYear }.All(v => capabilities.MovieSearchParams.Contains(v))) { return null; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilities.cs b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilities.cs deleted file mode 100644 index 7961b0b2e..000000000 --- a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilities.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.Indexers.Newznab -{ - public class NewznabCapabilities - { - public int DefaultPageSize { get; set; } - public int MaxPageSize { get; set; } - public string[] SupportedSearchParameters { get; set; } - public string[] SupportedMovieSearchParameters { get; set; } - public bool SupportsAggregateIdSearch { get; set; } - public List Categories { get; set; } - - public NewznabCapabilities() - { - DefaultPageSize = 100; - MaxPageSize = 100; - SupportedSearchParameters = new[] { "q" }; - SupportedMovieSearchParameters = new[] { "q", "imdbid", "imdbtitle", "imdbyear" }; - SupportsAggregateIdSearch = false; - Categories = new List(); - } - } - - public class NewznabCategory - { - public int Id { get; set; } - public string Name { get; set; } - public string Description { get; set; } - - public List Subcategories { get; set; } - } -} diff --git a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilitiesProvider.cs b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilitiesProvider.cs index deae3ea5d..6042ecb65 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilitiesProvider.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilitiesProvider.cs @@ -12,23 +12,23 @@ namespace NzbDrone.Core.Indexers.Newznab { public interface INewznabCapabilitiesProvider { - NewznabCapabilities GetCapabilities(NewznabSettings settings); + IndexerCapabilities GetCapabilities(NewznabSettings settings); } public class NewznabCapabilitiesProvider : INewznabCapabilitiesProvider { - private readonly ICached _capabilitiesCache; + private readonly ICached _capabilitiesCache; private readonly IHttpClient _httpClient; private readonly Logger _logger; public NewznabCapabilitiesProvider(ICacheManager cacheManager, IHttpClient httpClient, Logger logger) { - _capabilitiesCache = cacheManager.GetCache(GetType()); + _capabilitiesCache = cacheManager.GetCache(GetType()); _httpClient = httpClient; _logger = logger; } - public NewznabCapabilities GetCapabilities(NewznabSettings indexerSettings) + public IndexerCapabilities GetCapabilities(NewznabSettings indexerSettings) { var key = indexerSettings.ToJson(); var capabilities = _capabilitiesCache.Get(key, () => FetchCapabilities(indexerSettings), TimeSpan.FromDays(7)); @@ -36,9 +36,9 @@ namespace NzbDrone.Core.Indexers.Newznab return capabilities; } - private NewznabCapabilities FetchCapabilities(NewznabSettings indexerSettings) + private IndexerCapabilities FetchCapabilities(NewznabSettings indexerSettings) { - var capabilities = new NewznabCapabilities(); + var capabilities = new IndexerCapabilities(); var url = string.Format("{0}{1}?t=caps", indexerSettings.BaseUrl.TrimEnd('/'), indexerSettings.ApiPath.TrimEnd('/')); @@ -81,9 +81,9 @@ namespace NzbDrone.Core.Indexers.Newznab return capabilities; } - private NewznabCapabilities ParseCapabilities(HttpResponse response) + private IndexerCapabilities ParseCapabilities(HttpResponse response) { - var capabilities = new NewznabCapabilities(); + var capabilities = new IndexerCapabilities(); var xDoc = XDocument.Parse(response.Content); @@ -104,8 +104,8 @@ namespace NzbDrone.Core.Indexers.Newznab var xmlLimits = xmlRoot.Element("limits"); if (xmlLimits != null) { - capabilities.DefaultPageSize = int.Parse(xmlLimits.Attribute("default").Value); - capabilities.MaxPageSize = int.Parse(xmlLimits.Attribute("max").Value); + capabilities.LimitsDefault = int.Parse(xmlLimits.Attribute("default").Value); + capabilities.LimitsMax = int.Parse(xmlLimits.Attribute("max").Value); } var xmlSearching = xmlRoot.Element("searching"); @@ -114,22 +114,81 @@ namespace NzbDrone.Core.Indexers.Newznab var xmlBasicSearch = xmlSearching.Element("search"); if (xmlBasicSearch == null || xmlBasicSearch.Attribute("available").Value != "yes") { - capabilities.SupportedSearchParameters = null; + capabilities.SearchParams = new List(); } else if (xmlBasicSearch.Attribute("supportedParams") != null) { - capabilities.SupportedSearchParameters = xmlBasicSearch.Attribute("supportedParams").Value.Split(','); + foreach (var param in xmlBasicSearch.Attribute("supportedParams").Value.Split(',')) + { + if (Enum.TryParse(param, true, out SearchParam searchParam)) + { + capabilities.SearchParams.AddIfNotNull(searchParam); + } + } } var xmlMovieSearch = xmlSearching.Element("movie-search"); if (xmlMovieSearch == null || xmlMovieSearch.Attribute("available").Value != "yes") { - capabilities.SupportedMovieSearchParameters = null; + capabilities.MovieSearchParams = new List(); } else if (xmlMovieSearch.Attribute("supportedParams") != null) { - capabilities.SupportedMovieSearchParameters = xmlMovieSearch.Attribute("supportedParams").Value.Split(','); - capabilities.SupportsAggregateIdSearch = true; + foreach (var param in xmlMovieSearch.Attribute("supportedParams").Value.Split(',')) + { + if (Enum.TryParse(param, true, out MovieSearchParam searchParam)) + { + capabilities.MovieSearchParams.AddIfNotNull(searchParam); + } + } + } + + var xmlTvSearch = xmlSearching.Element("tv-search"); + if (xmlTvSearch == null || xmlTvSearch.Attribute("available").Value != "yes") + { + capabilities.TvSearchParams = new List(); + } + else if (xmlTvSearch.Attribute("supportedParams") != null) + { + foreach (var param in xmlTvSearch.Attribute("supportedParams").Value.Split(',')) + { + if (Enum.TryParse(param, true, out TvSearchParam searchParam)) + { + capabilities.TvSearchParams.AddIfNotNull(searchParam); + } + } + } + + var xmlAudioSearch = xmlSearching.Element("audio-search"); + if (xmlAudioSearch == null || xmlAudioSearch.Attribute("available").Value != "yes") + { + capabilities.MusicSearchParams = new List(); + } + else if (xmlAudioSearch.Attribute("supportedParams") != null) + { + foreach (var param in xmlAudioSearch.Attribute("supportedParams").Value.Split(',')) + { + if (Enum.TryParse(param, true, out MusicSearchParam searchParam)) + { + capabilities.MusicSearchParams.AddIfNotNull(searchParam); + } + } + } + + var xmlBookSearch = xmlSearching.Element("book-search"); + if (xmlBookSearch == null || xmlBookSearch.Attribute("available").Value != "yes") + { + capabilities.BookSearchParams = new List(); + } + else if (xmlBookSearch.Attribute("supportedParams") != null) + { + foreach (var param in xmlBookSearch.Attribute("supportedParams").Value.Split(',')) + { + if (Enum.TryParse(param, true, out BookSearchParam searchParam)) + { + capabilities.BookSearchParams.AddIfNotNull(searchParam); + } + } } } @@ -138,17 +197,16 @@ namespace NzbDrone.Core.Indexers.Newznab { foreach (var xmlCategory in xmlCategories.Elements("category")) { - var cat = new NewznabCategory + var cat = new IndexerCategory { Id = int.Parse(xmlCategory.Attribute("id").Value), Name = xmlCategory.Attribute("name").Value, - Description = xmlCategory.Attribute("description") != null ? xmlCategory.Attribute("description").Value : string.Empty, - Subcategories = new List() + Description = xmlCategory.Attribute("description") != null ? xmlCategory.Attribute("description").Value : string.Empty }; foreach (var xmlSubcat in xmlCategory.Elements("subcat")) { - cat.Subcategories.Add(new NewznabCategory + cat.SubCategories.Add(new IndexerCategory { Id = int.Parse(xmlSubcat.Attribute("id").Value), Name = xmlSubcat.Attribute("name").Value, diff --git a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRequestGenerator.cs index a4aef34ec..b8c26fca0 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRequestGenerator.cs @@ -28,8 +28,8 @@ namespace NzbDrone.Core.Indexers.Newznab { var capabilities = _capabilitiesProvider.GetCapabilities(Settings); - return capabilities.SupportedSearchParameters != null && - capabilities.SupportedSearchParameters.Contains("q"); + return capabilities.SearchParams != null && + capabilities.SearchParams.Contains(SearchParam.Q); } } @@ -39,8 +39,8 @@ namespace NzbDrone.Core.Indexers.Newznab { var capabilities = _capabilitiesProvider.GetCapabilities(Settings); - return capabilities.SupportedMovieSearchParameters != null && - capabilities.SupportedMovieSearchParameters.Contains("imdbid"); + return capabilities.MovieSearchParams != null && + capabilities.MovieSearchParams.Contains(MovieSearchParam.ImdbId); } } @@ -50,8 +50,8 @@ namespace NzbDrone.Core.Indexers.Newznab { var capabilities = _capabilitiesProvider.GetCapabilities(Settings); - return capabilities.SupportedMovieSearchParameters != null && - capabilities.SupportedMovieSearchParameters.Contains("tmdbid"); + return capabilities.MovieSearchParams != null && + capabilities.MovieSearchParams.Contains(MovieSearchParam.TmdbId); } } @@ -61,7 +61,8 @@ namespace NzbDrone.Core.Indexers.Newznab { var capabilities = _capabilitiesProvider.GetCapabilities(Settings); - return capabilities.SupportsAggregateIdSearch; + // TODO: Fix this, return capabilities.SupportsAggregateIdSearch; + return true; } } @@ -72,11 +73,11 @@ namespace NzbDrone.Core.Indexers.Newznab var capabilities = _capabilitiesProvider.GetCapabilities(Settings); // Some indexers might forget to enable movie search, but normal search still works fine. Thus we force a normal search. - if (capabilities.SupportedMovieSearchParameters != null) + if (capabilities.MovieSearchParams != null) { pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "movie", "")); } - else if (capabilities.SupportedSearchParameters != null) + else if (capabilities.SearchParams != null) { pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", "")); } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRssParser.cs b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRssParser.cs index 460974fb5..e6f6e0f3b 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRssParser.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Xml.Linq; using NzbDrone.Common.Extensions; @@ -148,19 +147,6 @@ namespace NzbDrone.Core.Indexers.Newznab return 0; } - protected virtual string GetImdbTitle(XElement item) - { - var imdbTitle = TryGetNewznabAttribute(item, "imdbtitle"); - if (!imdbTitle.IsNullOrWhiteSpace()) - { - return CultureInfo.CurrentCulture.TextInfo.ToTitleCase( - Parser.Parser.ReplaceGermanUmlauts( - Parser.Parser.NormalizeTitle(imdbTitle).Replace(" ", "."))); - } - - return string.Empty; - } - protected virtual int GetImdbYear(XElement item) { var imdbYearString = TryGetNewznabAttribute(item, "imdbyear"); diff --git a/src/NzbDrone.Core/Indexers/Definitions/Torznab/Torznab.cs b/src/NzbDrone.Core/Indexers/Definitions/Torznab/Torznab.cs index 0f265daed..764c08a67 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Torznab/Torznab.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Torznab/Torznab.cs @@ -20,7 +20,8 @@ namespace NzbDrone.Core.Indexers.Torznab public override DownloadProtocol Protocol => DownloadProtocol.Torrent; public override IndexerPrivacy Privacy => IndexerPrivacy.Private; - public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings).DefaultPageSize; + + public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings).LimitsDefault.Value; public override IIndexerRequestGenerator GetRequestGenerator() { @@ -63,7 +64,8 @@ namespace NzbDrone.Core.Indexers.Torznab Settings = settings, Protocol = DownloadProtocol.Usenet, SupportsRss = SupportsRss, - SupportsSearch = SupportsSearch + SupportsSearch = SupportsSearch, + Capabilities = new IndexerCapabilities() }; } @@ -95,15 +97,15 @@ namespace NzbDrone.Core.Indexers.Torznab failures.AddIfNotNull(TestCapabilities()); } - protected static List CategoryIds(List categories) + protected static List CategoryIds(List categories) { var l = categories.Select(c => c.Id).ToList(); foreach (var category in categories) { - if (category.Subcategories != null) + if (category.SubCategories != null) { - l.AddRange(CategoryIds(category.Subcategories)); + l.AddRange(CategoryIds(category.SubCategories)); } } @@ -127,14 +129,14 @@ namespace NzbDrone.Core.Indexers.Torznab } } - if (capabilities.SupportedSearchParameters != null && capabilities.SupportedSearchParameters.Contains("q")) + if (capabilities.SearchParams != null && capabilities.SearchParams.Contains(SearchParam.Q)) { return null; } - if (capabilities.SupportedMovieSearchParameters != null && - new[] { "q", "imdbid" }.Any(v => capabilities.SupportedMovieSearchParameters.Contains(v)) && - new[] { "imdbtitle", "imdbyear" }.All(v => capabilities.SupportedMovieSearchParameters.Contains(v))) + if (capabilities.MovieSearchParams != null && + new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId }.Any(v => capabilities.MovieSearchParams.Contains(v)) && + new[] { MovieSearchParam.ImdbTitle, MovieSearchParam.ImdbYear }.All(v => capabilities.MovieSearchParams.Contains(v))) { return null; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Torznab/TorznabSettings.cs b/src/NzbDrone.Core/Indexers/Definitions/Torznab/TorznabSettings.cs index 75ec82e92..7e2a979b7 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Torznab/TorznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Torznab/TorznabSettings.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Annotations; using NzbDrone.Core.Indexers.Newznab; -using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Torznab @@ -47,22 +44,14 @@ namespace NzbDrone.Core.Indexers.Torznab } } - public class TorznabSettings : NewznabSettings, ITorrentIndexerSettings + public class TorznabSettings : NewznabSettings { private static readonly TorznabSettingsValidator Validator = new TorznabSettingsValidator(); public TorznabSettings() { - MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; - RequiredFlags = new List(); } - [FieldDefinition(8, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] - public int MinimumSeeders { get; set; } - - [FieldDefinition(9, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://github.com/Prowlarr/Prowlarr/wiki/Indexer-Flags#1-required-flags", Advanced = true)] - public IEnumerable RequiredFlags { get; set; } - public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs index 2a6a10932..7dc2ea842 100644 --- a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs @@ -86,6 +86,11 @@ namespace NzbDrone.Core.Indexers return requests; } + public override IndexerCapabilities GetCapabilities() + { + return Capabilities; + } + protected virtual IList FetchReleases(Func pageableRequestChainSelector, bool isRecent = false) { var releases = new List(); diff --git a/src/NzbDrone.Core/Indexers/IIndexer.cs b/src/NzbDrone.Core/Indexers/IIndexer.cs index a05e42362..d13506844 100644 --- a/src/NzbDrone.Core/Indexers/IIndexer.cs +++ b/src/NzbDrone.Core/Indexers/IIndexer.cs @@ -16,5 +16,7 @@ namespace NzbDrone.Core.Indexers IList FetchRecent(); IList Fetch(MovieSearchCriteria searchCriteria); + + IndexerCapabilities GetCapabilities(); } } diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index 160ec444a..3f0b984e1 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -68,6 +68,8 @@ namespace NzbDrone.Core.Indexers public abstract IList FetchRecent(); public abstract IList Fetch(MovieSearchCriteria searchCriteria); + public abstract IndexerCapabilities GetCapabilities(); + protected virtual IList CleanupReleases(IEnumerable releases) { var result = releases.DistinctBy(v => v.Guid).ToList(); diff --git a/src/NzbDrone.Core/Indexers/IndexerCapabilities.cs b/src/NzbDrone.Core/Indexers/IndexerCapabilities.cs index f7f5144db..e7c8546f1 100644 --- a/src/NzbDrone.Core/Indexers/IndexerCapabilities.cs +++ b/src/NzbDrone.Core/Indexers/IndexerCapabilities.cs @@ -13,13 +13,16 @@ namespace NzbDrone.Core.Indexers ImdbId, TvdbId, RId, + TvMazeId } public enum MovieSearchParam { Q, ImdbId, - TmdbId + TmdbId, + ImdbTitle, + ImdbYear } public enum MusicSearchParam @@ -31,12 +34,23 @@ namespace NzbDrone.Core.Indexers Year } + public enum SearchParam + { + Q + } + + public enum BookSearchParam + { + Q + } + public class IndexerCapabilities { public int? LimitsMax { get; set; } public int? LimitsDefault { get; set; } - public bool SearchAvailable { get; set; } + public List SearchParams; + public bool SearchAvailable => SearchParams.Count > 0; public List TvSearchParams; public bool TvSearchAvailable => TvSearchParams.Count > 0; @@ -45,6 +59,7 @@ namespace NzbDrone.Core.Indexers public bool TvSearchImdbAvailable => TvSearchParams.Contains(TvSearchParam.ImdbId); public bool TvSearchTvdbAvailable => TvSearchParams.Contains(TvSearchParam.TvdbId); public bool TvSearchTvRageAvailable => TvSearchParams.Contains(TvSearchParam.RId); + public bool TvSearchTvMazeAvailable => TvSearchParams.Contains(TvSearchParam.TvMazeId); public List MovieSearchParams; public bool MovieSearchAvailable => MovieSearchParams.Count > 0; @@ -58,17 +73,18 @@ namespace NzbDrone.Core.Indexers public bool MusicSearchLabelAvailable => MusicSearchParams.Contains(MusicSearchParam.Label); public bool MusicSearchYearAvailable => MusicSearchParams.Contains(MusicSearchParam.Year); - public bool BookSearchAvailable { get; set; } + public List BookSearchParams; + public bool BookSearchAvailable => BookSearchParams.Count > 0; public List Categories { get; private set; } public IndexerCapabilities() { - SearchAvailable = true; + SearchParams = new List(); TvSearchParams = new List(); MovieSearchParams = new List(); MusicSearchParams = new List(); - BookSearchAvailable = false; + BookSearchParams = new List(); Categories = new List(); } @@ -181,6 +197,11 @@ namespace NzbDrone.Core.Indexers parameters.Add("rid"); } + if (TvSearchTvMazeAvailable) + { + parameters.Add("tvmazeid"); + } + return string.Join(",", parameters); } @@ -244,7 +265,7 @@ namespace NzbDrone.Core.Indexers { var subCategories = Categories.SelectMany(c => c.SubCategories); var allCategories = Categories.Concat(subCategories); - var supportsCategory = allCategories.Any(i => categories.Any(c => c == i.ID)); + var supportsCategory = allCategories.Any(i => categories.Any(c => c == i.Id)); return supportsCategory; } @@ -280,13 +301,13 @@ namespace NzbDrone.Core.Indexers new XAttribute("available", BookSearchAvailable ? "yes" : "no"), new XAttribute("supportedParams", SupportedBookSearchParams))), new XElement("categories", - from c in Categories.OrderBy(x => x.ID < 100000 ? "z" + x.ID.ToString() : x.Name) + from c in Categories.OrderBy(x => x.Id < 100000 ? "z" + x.Id.ToString() : x.Name) select new XElement("category", - new XAttribute("id", c.ID), + new XAttribute("id", c.Id), new XAttribute("name", c.Name), from sc in c.SubCategories select new XElement("subcat", - new XAttribute("id", sc.ID), + new XAttribute("id", sc.Id), new XAttribute("name", sc.Name)))))); return xdoc; } @@ -296,12 +317,12 @@ namespace NzbDrone.Core.Indexers public static IndexerCapabilities Concat(IndexerCapabilities left, IndexerCapabilities right) { - left.SearchAvailable = left.SearchAvailable || right.SearchAvailable; + left.SearchParams = left.SearchParams.Union(right.SearchParams).ToList(); left.TvSearchParams = left.TvSearchParams.Union(right.TvSearchParams).ToList(); left.MovieSearchParams = left.MovieSearchParams.Union(right.MovieSearchParams).ToList(); left.MusicSearchParams = left.MusicSearchParams.Union(right.MusicSearchParams).ToList(); - left.BookSearchAvailable = left.BookSearchAvailable || right.BookSearchAvailable; - left.Categories.AddRange(right.Categories.Where(x => x.ID < 100000).Except(left.Categories)); // exclude indexer specific categories (>= 100000) + left.BookSearchParams = left.BookSearchParams.Union(right.BookSearchParams).ToList(); + left.Categories.AddRange(right.Categories.Where(x => x.Id < 100000).Except(left.Categories)); // exclude indexer specific categories (>= 100000) return left; } } diff --git a/src/NzbDrone.Core/Indexers/IndexerCategory.cs b/src/NzbDrone.Core/Indexers/IndexerCategory.cs index 2d4489ab0..3ac8d29eb 100644 --- a/src/NzbDrone.Core/Indexers/IndexerCategory.cs +++ b/src/NzbDrone.Core/Indexers/IndexerCategory.cs @@ -5,8 +5,9 @@ namespace NzbDrone.Core.Indexers { public class IndexerCategory { - public int ID { get; set; } + public int Id { get; set; } public string Name { get; set; } + public string Description { get; set; } public List SubCategories { get; private set; } @@ -14,7 +15,7 @@ namespace NzbDrone.Core.Indexers public IndexerCategory(int id, string name) { - ID = id; + Id = id; Name = name; SubCategories = new List(); } @@ -25,14 +26,14 @@ namespace NzbDrone.Core.Indexers public JToken ToJson() => new JObject { - ["ID"] = ID, + ["ID"] = Id, ["Name"] = Name }; - public override bool Equals(object obj) => (obj as IndexerCategory)?.ID == ID; + public override bool Equals(object obj) => (obj as IndexerCategory)?.Id == Id; // Get Hash code should be calculated off read only properties. // ID is not readonly - public override int GetHashCode() => ID; + public override int GetHashCode() => Id; } } diff --git a/src/NzbDrone.Core/Parser/Model/ParsedMovieInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedMovieInfo.cs deleted file mode 100644 index 7a8c2d9ed..000000000 --- a/src/NzbDrone.Core/Parser/Model/ParsedMovieInfo.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using NzbDrone.Core.Languages; - -namespace NzbDrone.Core.Parser.Model -{ - public class ParsedMovieInfo - { - public string MovieTitle { get; set; } - public string OriginalTitle { get; set; } - public string ReleaseTitle { get; set; } - public string SimpleReleaseTitle { get; set; } - public List Languages { get; set; } = new List(); - public string ReleaseGroup { get; set; } - public string ReleaseHash { get; set; } - public string Edition { get; set; } - public int Year { get; set; } - public string ImdbId { get; set; } - [JsonIgnore] - public Dictionary ExtraInfo { get; set; } = new Dictionary(); - - public override string ToString() - { - return string.Format("{0} - {1}", MovieTitle, Year); - } - } -} diff --git a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs index c9f6069f5..3f72c93e5 100644 --- a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text; using NzbDrone.Core.Indexers; @@ -19,6 +19,7 @@ namespace NzbDrone.Core.Parser.Model public int TvdbId { get; set; } public int TvRageId { get; set; } public int ImdbId { get; set; } + public int TmdbId { get; set; } public DateTime PublishDate { get; set; } public string Origin { get; set; } diff --git a/src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs b/src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs deleted file mode 100644 index e9befbf39..000000000 --- a/src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NzbDrone.Core.Parser.Model -{ - public class SeriesTitleInfo - { - public string Title { get; set; } - public string TitleWithoutYear { get; set; } - public int Year { get; set; } - } -} diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs deleted file mode 100644 index 8a3361a2d..000000000 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ /dev/null @@ -1,584 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Instrumentation; -using NzbDrone.Core.Parser.Model; -#if !LIBRARY -#endif - -namespace NzbDrone.Core.Parser -{ - public static class Parser - { - private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(Parser)); - - private static readonly Regex ReportYearRegex = new Regex(@"^.*(?(19|20)\d{2}).*$", RegexOptions.Compiled); - - private static readonly Regex EditionRegex = new Regex(@"\(?\b(?(((Recut.|Extended.|Ultimate.)?(Director.?s|Collector.?s|Theatrical|Ultimate|Final(?=(.(Cut|Edition|Version)))|Extended|Rogue|Special|Despecialized|\d{2,3}(th)?.Anniversary)(.(Cut|Edition|Version))?(.(Extended|Uncensored|Remastered|Unrated|Uncut|IMAX|Fan.?Edit))?|((Uncensored|Remastered|Unrated|Uncut|IMAX|Fan.?Edit|Edition|Restored|((2|3|4)in1))))))\b\)?", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex ReportEditionRegex = new Regex(@"^.+?" + EditionRegex, RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex[] ReportMovieTitleRegex = new[] - { - //Some german or french tracker formats (missing year, ...) (Only applies to german and French/TrueFrench releases) - see ParserFixture for examples and tests - new Regex(@"^(?(?![(\[]).+?)((\W|_))(" + EditionRegex + @".{1,3})?(?:(?<!(19|20)\d{2}.*?)(German|French|TrueFrench))(.+?)(?=((19|20)\d{2}|$))(?<year>(19|20)\d{2}(?!p|i|\d+|\]|\W\d+))?(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //Special, Despecialized, etc. Edition Movies, e.g: Mission.Impossible.3.Special.Edition.2011 - new Regex(@"^(?<title>(?![(\[]).+?)?(?:(?:[-_\W](?<![)\[!]))*" + EditionRegex + @".{1,3}(?<year>(1(8|9)|20)\d{2}(?!p|i|\d+|\]|\W\d+)))+(\W+|_|$)(?!\\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //Special, Despecialized, etc. Edition Movies, e.g: Mission.Impossible.3.2011.Special.Edition //TODO: Seems to slow down parsing heavily! - /*new Regex(@"^(?<title>(?![(\[]).+?)?(?:(?:[-_\W](?<![)\[!]))*(?<year>(19|20)\d{2}(?!p|i|(19|20)\d{2}|\]|\W(19|20)\d{2})))+(\W+|_|$)(?!\\)\(?(?<edition>(((Extended.|Ultimate.)?(Director.?s|Collector.?s|Theatrical|Ultimate|Final(?=(.(Cut|Edition|Version)))|Extended|Rogue|Special|Despecialized|\d{2,3}(th)?.Anniversary)(.(Cut|Edition|Version))?(.(Extended|Uncensored|Remastered|Unrated|Uncut|IMAX|Fan.?Edit))?|((Uncensored|Remastered|Unrated|Uncut|IMAX|Fan.?Edit|Edition|Restored|((2|3|4)in1))))))\)?", - RegexOptions.IgnoreCase | RegexOptions.Compiled),*/ - - //Normal movie format, e.g: Mission.Impossible.3.2011 - new Regex(@"^(?<title>(?![(\[]).+?)?(?:(?:[-_\W](?<![)\[!]))*(?<year>(1(8|9)|20)\d{2}(?!p|i|(1(8|9)|20)\d{2}|\]|\W(1(8|9)|20)\d{2})))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //PassThePopcorn Torrent names: Star.Wars[PassThePopcorn] - new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()\[!]))*(?<year>(\[\w *\])))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //That did not work? Maybe some tool uses [] for years. Who would do that? - new Regex(@"^(?<title>(?![(\[]).+?)?(?:(?:[-_\W](?<![)!]))*(?<year>(1(8|9)|20)\d{2}(?!p|i|\d+|\W\d+)))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //As a last resort for movies that have ( or [ in their title. - new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![)\[!]))*(?<year>(1(8|9)|20)\d{2}(?!p|i|\d+|\]|\W\d+)))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), - }; - - private static readonly Regex[] ReportMovieTitleFolderRegex = new[] - { - //When year comes first. - new Regex(@"^(?:(?:[-_\W](?<![)!]))*(?<year>(19|20)\d{2}(?!p|i|\d+|\W\d+)))+(\W+|_|$)(?<title>.+?)?$") - }; - - private static readonly Regex[] RejectHashedReleasesRegex = new Regex[] - { - // Generic match for md5 and mixed-case hashes. - new Regex(@"^[0-9a-zA-Z]{32}", RegexOptions.Compiled), - - // Generic match for shorter lower-case hashes. - new Regex(@"^[a-z0-9]{24}$", RegexOptions.Compiled), - - // Format seen on some NZBGeek releases - // Be very strict with these coz they are very close to the valid 101 ep numbering. - new Regex(@"^[A-Z]{11}\d{3}$", RegexOptions.Compiled), - new Regex(@"^[a-z]{12}\d{3}$", RegexOptions.Compiled), - - //Backup filename (Unknown origins) - new Regex(@"^Backup_\d{5,}S\d{2}-\d{2}$", RegexOptions.Compiled), - - //123 - Started appearing December 2014 - new Regex(@"^123$", RegexOptions.Compiled), - - //abc - Started appearing January 2015 - new Regex(@"^abc$", RegexOptions.Compiled | RegexOptions.IgnoreCase), - - //abc - Started appearing 2020 - new Regex(@"^abc[-_. ]xyz", RegexOptions.Compiled | RegexOptions.IgnoreCase), - - //b00bs - Started appearing January 2015 - new Regex(@"^b00bs$", RegexOptions.Compiled | RegexOptions.IgnoreCase) - }; - - //Regex to detect whether the title was reversed. - private static readonly Regex ReversedTitleRegex = new Regex(@"(?:^|[-._ ])(p027|p0801)[-._ ]", RegexOptions.Compiled); - - private static readonly Regex NormalizeRegex = new Regex(@"((?:\b|_)(?<!^|[^a-zA-Z0-9_']\w[^a-zA-Z0-9_'])(a(?!$|[^a-zA-Z0-9_']\w[^a-zA-Z0-9_'])|an|the|and|or|of)(?:\b|_))|\W|_", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly Regex FileExtensionRegex = new Regex(@"\.[a-z0-9]{2,4}$", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly Regex ReportImdbId = new Regex(@"(?<imdbid>tt\d{7,8})", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly RegexReplace SimpleTitleRegex = new RegexReplace(@"\s*(?:480[ip]|576[ip]|720[ip]|1080[ip]|2160[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*:|]|848x480|1280x720|1920x1080|(8|10)b(it)?)", - string.Empty, - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly Regex SimpleReleaseTitleRegex = new Regex(@"\s*(?:[<>?*:|])", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly RegexReplace WebsitePrefixRegex = new RegexReplace(@"^\[\s*[-a-z]+(\.[a-z]+)+\s*\][- ]*|^www\.[a-z]+\.(?:com|net|org)[ -]*", - string.Empty, - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly RegexReplace WebsitePostfixRegex = new RegexReplace(@"\[\s*[-a-z]+(\.[a-z0-9]+)+\s*\]$", - string.Empty, - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly RegexReplace CleanReleaseGroupRegex = new RegexReplace(@"(-(RP|1|NZBGeek|Obfuscated|Obfuscation|Scrambled|sample|Pre|postbot|xpost|Rakuv[a-z0-9]*|WhiteRev|BUYMORE|AsRequested|AlternativeToRequested|GEROV|Z0iDS3N|Chamele0n|4P|4Planet|AlteZachen))+$", - string.Empty, - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly RegexReplace CleanTorrentSuffixRegex = new RegexReplace(@"\[(?:ettv|rartv|rarbg|cttv)\]$", - string.Empty, - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly Regex CleanQualityBracketsRegex = new Regex(@"\[[a-z0-9 ._-]+\]$", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly Regex ReleaseGroupRegex = new Regex(@"-(?<releasegroup>[a-z0-9]+(?!.+?(?:480p|720p|1080p|2160p)))(?<!.*?WEB-DL|Blu-Ray|480p|720p|1080p|2160p|DTS-HD|DTS-X|DTS-MA|DTS-ES)(?:\b|[-._ ]|$)|[-._ ]\[(?<releasegroup>[a-z0-9]+)\]$", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly Regex AnimeReleaseGroupRegex = new Regex(@"^(?:\[(?<subgroup>(?!\s).+?(?<!\s))\](?:_|-|\s|\.)?)", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly Regex YearInTitleRegex = new Regex(@"^(?<title>.+?)(?:\W|_)?(?<year>\d{4})", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly Regex WordDelimiterRegex = new Regex(@"(\s|\.|,|_|-|=|'|\|)+", RegexOptions.Compiled); - private static readonly Regex SpecialCharRegex = new Regex(@"(\&|\:|\\|\/)+", RegexOptions.Compiled); - private static readonly Regex PunctuationRegex = new Regex(@"[^\w\s]", RegexOptions.Compiled); - private static readonly Regex CommonWordRegex = new Regex(@"\b(a|an|the|and|or|of)\b\s?", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex SpecialEpisodeWordRegex = new Regex(@"\b(part|special|edition|christmas)\b\s?", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex DuplicateSpacesRegex = new Regex(@"\s{2,}", RegexOptions.Compiled); - - private static readonly Regex RequestInfoRegex = new Regex(@"^(?:\[.+?\])+", RegexOptions.Compiled); - - private static readonly string[] Numbers = new[] { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" }; - private static Dictionary<string, string> _umlautMappings = new Dictionary<string, string> - { - { "ö", "oe" }, - { "ä", "ae" }, - { "ü", "ue" }, - }; - - public static ParsedMovieInfo ParseMoviePath(string path) - { - var fileInfo = new FileInfo(path); - - var result = ParseMovieTitle(fileInfo.Name, true); - - if (result == null) - { - Logger.Debug("Attempting to parse movie info using directory and file names. {0}", fileInfo.Directory.Name); - result = ParseMovieTitle(fileInfo.Directory.Name + " " + fileInfo.Name); - } - - if (result == null) - { - Logger.Debug("Attempting to parse movie info using directory name. {0}", fileInfo.Directory.Name); - result = ParseMovieTitle(fileInfo.Directory.Name + fileInfo.Extension); - } - - return result; - } - - public static ParsedMovieInfo ParseMovieTitle(string title, bool isDir = false) - { - var originalTitle = title; - try - { - if (!ValidateBeforeParsing(title)) - { - return null; - } - - Logger.Debug("Parsing string '{0}'", title); - - if (ReversedTitleRegex.IsMatch(title)) - { - var titleWithoutExtension = RemoveFileExtension(title).ToCharArray(); - Array.Reverse(titleWithoutExtension); - - title = new string(titleWithoutExtension) + title.Substring(titleWithoutExtension.Length); - - Logger.Debug("Reversed name detected. Converted to '{0}'", title); - } - - var releaseTitle = RemoveFileExtension(title); - - //Trim dashes from end - releaseTitle = releaseTitle.Trim('-', '_'); - - releaseTitle = releaseTitle.Replace("【", "[").Replace("】", "]"); - - var simpleTitle = SimpleTitleRegex.Replace(releaseTitle); - - // TODO: Quick fix stripping [url] - prefixes. - simpleTitle = WebsitePrefixRegex.Replace(simpleTitle); - simpleTitle = WebsitePostfixRegex.Replace(simpleTitle); - - simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle); - - var allRegexes = ReportMovieTitleRegex.ToList(); - - if (isDir) - { - allRegexes.AddRange(ReportMovieTitleFolderRegex); - } - - foreach (var regex in allRegexes) - { - var match = regex.Matches(simpleTitle); - - if (match.Count != 0) - { - Logger.Trace(regex); - try - { - var result = ParseMovieMatchCollection(match); - - if (result != null) - { - //TODO: Add tests for this! - var simpleReleaseTitle = SimpleReleaseTitleRegex.Replace(releaseTitle, string.Empty); - - var simpleTitleReplaceString = match[0].Groups["title"].Success ? match[0].Groups["title"].Value : result.MovieTitle; - - if (simpleTitleReplaceString.IsNotNullOrWhiteSpace()) - { - simpleReleaseTitle = simpleReleaseTitle.Replace(simpleTitleReplaceString, simpleTitleReplaceString.Contains(".") ? "A.Movie" : "A Movie"); - } - - result.ReleaseGroup = ParseReleaseGroup(simpleReleaseTitle); - - var subGroup = GetSubGroup(match); - if (!subGroup.IsNullOrWhiteSpace()) - { - result.ReleaseGroup = subGroup; - } - - Logger.Debug("Release Group parsed: {0}", result.ReleaseGroup); - - result.Languages = LanguageParser.ParseLanguages(result.ReleaseGroup.IsNotNullOrWhiteSpace() ? simpleReleaseTitle.Replace(result.ReleaseGroup, "RlsGrp") : simpleReleaseTitle); - Logger.Debug("Languages parsed: {0}", string.Join(", ", result.Languages)); - - if (result.Edition.IsNullOrWhiteSpace()) - { - result.Edition = ParseEdition(simpleReleaseTitle); - Logger.Debug("Edition parsed: {0}", result.Edition); - } - - result.ReleaseHash = GetReleaseHash(match); - if (!result.ReleaseHash.IsNullOrWhiteSpace()) - { - Logger.Debug("Release Hash parsed: {0}", result.ReleaseHash); - } - - result.OriginalTitle = originalTitle; - result.ReleaseTitle = releaseTitle; - result.SimpleReleaseTitle = simpleReleaseTitle; - - result.ImdbId = ParseImdbId(simpleReleaseTitle); - - return result; - } - } - catch (InvalidDateException ex) - { - Logger.Debug(ex, ex.Message); - break; - } - } - } - } - catch (Exception e) - { - if (!title.ToLower().Contains("password") && !title.ToLower().Contains("yenc")) - { - Logger.Error(e, "An error has occurred while trying to parse {0}", title); - } - } - - Logger.Debug("Unable to parse {0}", title); - return null; - } - - public static string ParseImdbId(string title) - { - var match = ReportImdbId.Match(title); - if (match.Success) - { - if (match.Groups["imdbid"].Value != null) - { - if (match.Groups["imdbid"].Length == 9 || match.Groups["imdbid"].Length == 10) - { - return match.Groups["imdbid"].Value; - } - } - } - - return ""; - } - - public static string ParseEdition(string languageTitle) - { - var editionMatch = ReportEditionRegex.Match(languageTitle); - - if (editionMatch.Success && editionMatch.Groups["edition"].Value != null && - editionMatch.Groups["edition"].Value.IsNotNullOrWhiteSpace()) - { - return editionMatch.Groups["edition"].Value.Replace(".", " "); - } - - return ""; - } - - public static string ReplaceGermanUmlauts(string s) - { - var t = s; - t = t.Replace("ä", "ae"); - t = t.Replace("ö", "oe"); - t = t.Replace("ü", "ue"); - t = t.Replace("Ä", "Ae"); - t = t.Replace("Ö", "Oe"); - t = t.Replace("Ü", "Ue"); - t = t.Replace("ß", "ss"); - return t; - } - - public static string NormalizeImdbId(string imdbId) - { - if (imdbId.Length > 2) - { - imdbId = imdbId.Replace("tt", "").PadLeft(7, '0'); - return $"tt{imdbId}"; - } - - return null; - } - - public static string ToUrlSlug(string value) - { - //First to lower case - value = value.ToLowerInvariant(); - - //Remove all accents - value = value.RemoveAccent(); - - //Replace spaces - value = Regex.Replace(value, @"\s", "-", RegexOptions.Compiled); - - //Remove invalid chars - value = Regex.Replace(value, @"[^a-z0-9\s-_]", "", RegexOptions.Compiled); - - //Trim dashes from end - value = value.Trim('-', '_'); - - //Replace double occurences of - or _ - value = Regex.Replace(value, @"([-_]){2,}", "$1", RegexOptions.Compiled); - - return value; - } - - public static string CleanMovieTitle(this string title) - { - long number = 0; - - //If Title only contains numbers return it as is. - if (long.TryParse(title, out number)) - { - return title; - } - - return ReplaceGermanUmlauts(NormalizeRegex.Replace(title, string.Empty).ToLower()).RemoveAccent(); - } - - public static string NormalizeEpisodeTitle(this string title) - { - title = SpecialEpisodeWordRegex.Replace(title, string.Empty); - title = PunctuationRegex.Replace(title, " "); - title = DuplicateSpacesRegex.Replace(title, " "); - - return title.Trim() - .ToLower(); - } - - public static string NormalizeTitle(this string title) - { - title = WordDelimiterRegex.Replace(title, " "); - title = PunctuationRegex.Replace(title, string.Empty); - title = CommonWordRegex.Replace(title, string.Empty); - title = DuplicateSpacesRegex.Replace(title, " "); - title = SpecialCharRegex.Replace(title, string.Empty); - - return title.Trim().ToLower(); - } - - public static string SimplifyReleaseTitle(this string title) - { - return SimpleReleaseTitleRegex.Replace(title, string.Empty); - } - - public static string ParseReleaseGroup(string title) - { - title = title.Trim(); - title = RemoveFileExtension(title); - title = WebsitePrefixRegex.Replace(title); - - var animeMatch = AnimeReleaseGroupRegex.Match(title); - - if (animeMatch.Success) - { - return animeMatch.Groups["subgroup"].Value; - } - - title = CleanReleaseGroupRegex.Replace(title); - - var matches = ReleaseGroupRegex.Matches(title); - - if (matches.Count != 0) - { - var group = matches.OfType<Match>().Last().Groups["releasegroup"].Value; - int groupIsNumeric; - - if (int.TryParse(group, out groupIsNumeric)) - { - return null; - } - - return group; - } - - return null; - } - - public static string RemoveFileExtension(string title) - { - title = FileExtensionRegex.Replace(title, m => - { - var extension = m.Value.ToLower(); - - return m.Value; - }); - - return title; - } - - private static ParsedMovieInfo ParseMovieMatchCollection(MatchCollection matchCollection) - { - if (!matchCollection[0].Groups["title"].Success || matchCollection[0].Groups["title"].Value == "(") - { - return null; - } - - var movieName = matchCollection[0].Groups["title"].Value.Replace('_', ' '); - movieName = RequestInfoRegex.Replace(movieName, "").Trim(' '); - - var parts = movieName.Split('.'); - movieName = ""; - int n = 0; - bool previousAcronym = false; - string nextPart = ""; - foreach (var part in parts) - { - if (parts.Length >= n + 2) - { - nextPart = parts[n + 1]; - } - else - { - nextPart = ""; - } - - if (part.Length == 1 && part.ToLower() != "a" && !int.TryParse(part, out _) && - (previousAcronym || n < parts.Length - 1) && - (previousAcronym || nextPart.Length != 1 || !int.TryParse(nextPart, out _))) - { - movieName += part + "."; - previousAcronym = true; - } - else if (part.ToLower() == "a" && (previousAcronym || nextPart.Length == 1)) - { - movieName += part + "."; - previousAcronym = true; - } - else if (part.ToLower() == "dr") - { - movieName += part + "."; - previousAcronym = true; - } - else - { - if (previousAcronym) - { - movieName += " "; - previousAcronym = false; - } - - movieName += part + " "; - } - - n++; - } - - movieName = movieName.Trim(' '); - - int airYear; - int.TryParse(matchCollection[0].Groups["year"].Value, out airYear); - - ParsedMovieInfo result; - - result = new ParsedMovieInfo { Year = airYear }; - - if (matchCollection[0].Groups["edition"].Success) - { - result.Edition = matchCollection[0].Groups["edition"].Value.Replace(".", " "); - } - - result.MovieTitle = movieName; - - Logger.Debug("Movie Parsed. {0}", result); - - return result; - } - - private static bool ValidateBeforeParsing(string title) - { - if (title.ToLower().Contains("password") && title.ToLower().Contains("yenc")) - { - Logger.Debug(""); - return false; - } - - if (!title.Any(char.IsLetterOrDigit)) - { - return false; - } - - var titleWithoutExtension = RemoveFileExtension(title); - - if (RejectHashedReleasesRegex.Any(v => v.IsMatch(titleWithoutExtension))) - { - Logger.Debug("Rejected Hashed Release Title: " + title); - return false; - } - - return true; - } - - private static string GetSubGroup(MatchCollection matchCollection) - { - var subGroup = matchCollection[0].Groups["subgroup"]; - - if (subGroup.Success) - { - return subGroup.Value; - } - - return string.Empty; - } - - private static string GetReleaseHash(MatchCollection matchCollection) - { - var hash = matchCollection[0].Groups["hash"]; - - if (hash.Success) - { - var hashValue = hash.Value.Trim('[', ']'); - - if (hashValue.Equals("1280x720")) - { - return string.Empty; - } - - return hashValue; - } - - return string.Empty; - } - } -} diff --git a/src/NzbDrone.Core/Parser/SceneChecker.cs b/src/NzbDrone.Core/Parser/SceneChecker.cs deleted file mode 100644 index 459145c12..000000000 --- a/src/NzbDrone.Core/Parser/SceneChecker.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace NzbDrone.Core.Parser -{ - public static class SceneChecker - { - //This method should prefer false negatives over false positives. - //It's better not to use a title that might be scene than to use one that isn't scene - public static string GetSceneTitle(string title) - { - if (title == null) - { - return null; - } - - if (!title.Contains(".")) - { - return null; - } - - if (title.Contains(" ")) - { - return null; - } - - var parsedTitle = Parser.ParseMovieTitle(title); - - if (parsedTitle == null || - parsedTitle.ReleaseGroup == null || - string.IsNullOrWhiteSpace(parsedTitle.MovieTitle) || - string.IsNullOrWhiteSpace(parsedTitle.ReleaseTitle)) - { - return null; - } - - return parsedTitle.ReleaseTitle; - } - - public static bool IsSceneTitle(string title) - { - return GetSceneTitle(title) != null; - } - } -} diff --git a/src/Prowlarr.Api.V1/Indexers/IndexerModule.cs b/src/Prowlarr.Api.V1/Indexers/IndexerModule.cs index ddabb8367..c9d9b59ec 100644 --- a/src/Prowlarr.Api.V1/Indexers/IndexerModule.cs +++ b/src/Prowlarr.Api.V1/Indexers/IndexerModule.cs @@ -1,5 +1,9 @@ +using System.Collections.Generic; using Nancy; +using Nancy.ModelBinding; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; +using NzbDrone.Core.IndexerSearch; using Prowlarr.Http.REST; namespace Prowlarr.Api.V1.Indexers @@ -9,13 +13,19 @@ namespace Prowlarr.Api.V1.Indexers public static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper(); private IIndexerFactory _indexerFactory { get; set; } + private ISearchForNzb _nzbSearchService { get; set; } - public IndexerModule(IndexerFactory indexerFactory) + public IndexerModule(IndexerFactory indexerFactory, ISearchForNzb nzbSearchService) : base(indexerFactory, "indexer", ResourceMapper) { _indexerFactory = indexerFactory; + _nzbSearchService = nzbSearchService; - Get("{id}/newznab", x => GetNewznabResponse(x.id)); + Get("{id}/newznab", x => + { + var request = this.Bind<NewznabRequest>(); + return GetNewznabResponse(request); + }); } protected override void Validate(IndexerDefinition definition, bool includeWarnings) @@ -28,25 +38,36 @@ namespace Prowlarr.Api.V1.Indexers base.Validate(definition, includeWarnings); } - private object GetNewznabResponse(int id) + private object GetNewznabResponse(NewznabRequest request) { - var requestType = Request.Query.t; + var requestType = request.t; - if (!requestType.HasValue) + if (requestType.IsNullOrWhiteSpace()) { throw new BadRequestException("Missing Function Parameter"); } - if (requestType.Value == "caps") + var indexer = _indexerFactory.Get(request.id); + + if (indexer == null) { - var indexer = _indexerFactory.GetInstance(_indexerFactory.Get(id)); - Response response = indexer.Capabilities.ToXml(); - response.ContentType = "application/rss+xml"; - return response; + throw new NotFoundException("Indexer Not Found"); } - else + + var indexerInstance = _indexerFactory.GetInstance(indexer); + + switch (requestType) { - throw new BadRequestException("Function Not Available"); + case "caps": + Response response = indexerInstance.GetCapabilities().ToXml(); + response.ContentType = "application/rss+xml"; + return response; + case "movie": + Response movieResponse = _nzbSearchService.Search(request, new List<int> { indexer.Id }, true, false).ToXml(); + movieResponse.ContentType = "application/rss+xml"; + return movieResponse; + default: + throw new BadRequestException("Function Not Available"); } } } diff --git a/src/Prowlarr.Api.V1/Indexers/IndexerResource.cs b/src/Prowlarr.Api.V1/Indexers/IndexerResource.cs index 3601a0917..d53f7c81a 100644 --- a/src/Prowlarr.Api.V1/Indexers/IndexerResource.cs +++ b/src/Prowlarr.Api.V1/Indexers/IndexerResource.cs @@ -10,10 +10,6 @@ namespace Prowlarr.Api.V1.Indexers public bool EnableInteractiveSearch { get; set; } public bool SupportsRss { get; set; } public bool SupportsSearch { get; set; } - public bool SupportsMusic { get; set; } - public bool SupportsTv { get; set; } - public bool SupportsMovies { get; set; } - public bool SupportsBooks { get; set; } public DownloadProtocol Protocol { get; set; } public IndexerPrivacy Privacy { get; set; } public IndexerCapabilities Capabilities { get; set; } diff --git a/src/Prowlarr.Api.V1/Search/SearchModule.cs b/src/Prowlarr.Api.V1/Search/SearchModule.cs index 1e9bba7dd..95321035d 100644 --- a/src/Prowlarr.Api.V1/Search/SearchModule.cs +++ b/src/Prowlarr.Api.V1/Search/SearchModule.cs @@ -26,17 +26,26 @@ namespace Prowlarr.Api.V1.Search { if (Request.Query.query.HasValue) { - return GetSearchReleases(Request.Query.query); + var indexerIds = Request.Query.indexerIds.HasValue ? (List<int>)Request.Query.indexerIds.split(',') : new List<int>(); + + if (indexerIds.Count > 0) + { + return GetSearchReleases(Request.Query.query, indexerIds); + } + else + { + return GetSearchReleases(Request.Query.query, null); + } } return new List<SearchResource>(); } - private List<SearchResource> GetSearchReleases(string query) + private List<SearchResource> GetSearchReleases(string query, List<int> indexerIds) { try { - var decisions = _nzbSearhService.MovieSearch(query, true, true); + var decisions = _nzbSearhService.Search(query, indexerIds, true, true); return MapDecisions(decisions); }