Newznab Responses for Caps and Movie Search (rough)

This commit is contained in:
Qstick
2020-10-21 01:47:20 -04:00
parent 84cbfe870f
commit cfb1a80c58
46 changed files with 526 additions and 1226 deletions

View File

@@ -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 (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
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);

View File

@@ -26,34 +26,16 @@ function MovieIndexSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
Monitored/Status Status
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
name="sortTitle" name="name"
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
{translate('Title')} {translate('Name')}
</SortMenuItem>
<SortMenuItem
name="studio"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Studio')}
</SortMenuItem>
<SortMenuItem
name="qualityProfileId"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('QualityProfile')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
@@ -66,66 +48,21 @@ function MovieIndexSortMenu(props) {
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
name="year" name="protocol"
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
{translate('Year')} {'Protocol'}
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
name="inCinemas" name="privacy"
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
{translate('InCinemas')} {'Privacy'}
</SortMenuItem>
<SortMenuItem
name="physicalRelease"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('PhysicalRelease')}
</SortMenuItem>
<SortMenuItem
name="digitalRelease"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('DigitalRelease')}
</SortMenuItem>
<SortMenuItem
name="path"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Path')}
</SortMenuItem>
<SortMenuItem
name="sizeOnDisk"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('SizeOnDisk')}
</SortMenuItem>
<SortMenuItem
name="certification"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Certification')}
</SortMenuItem> </SortMenuItem>
</MenuContent> </MenuContent>
</SortMenu> </SortMenu>

View File

@@ -4,16 +4,16 @@ import Label from 'Components/Label';
function CapabilitiesLabel(props) { function CapabilitiesLabel(props) {
const { const {
supportsBooks, movieSearchAvailable,
supportsMovies, tvSearchAvailable,
supportsMusic, musicSearchAvailable,
supportsTv bookSearchAvailable
} = props; } = props.capabilities;
return ( return (
<span> <span>
{ {
supportsBooks ? bookSearchAvailable ?
<Label> <Label>
{'Books'} {'Books'}
</Label> : </Label> :
@@ -21,7 +21,7 @@ function CapabilitiesLabel(props) {
} }
{ {
supportsMovies ? movieSearchAvailable ?
<Label> <Label>
{'Movies'} {'Movies'}
</Label> : </Label> :
@@ -29,7 +29,7 @@ function CapabilitiesLabel(props) {
} }
{ {
supportsMusic ? musicSearchAvailable ?
<Label> <Label>
{'Music'} {'Music'}
</Label> : </Label> :
@@ -37,7 +37,7 @@ function CapabilitiesLabel(props) {
} }
{ {
supportsTv ? tvSearchAvailable ?
<Label> <Label>
{'TV'} {'TV'}
</Label> : </Label> :
@@ -45,7 +45,7 @@ function CapabilitiesLabel(props) {
} }
{ {
!supportsTv && !supportsMusic && !supportsMovies && !supportsBooks ? !tvSearchAvailable && !musicSearchAvailable && !movieSearchAvailable && !bookSearchAvailable ?
<Label> <Label>
{'None'} {'None'}
</Label> : </Label> :
@@ -56,10 +56,7 @@ function CapabilitiesLabel(props) {
} }
CapabilitiesLabel.propTypes = { CapabilitiesLabel.propTypes = {
supportsTv: PropTypes.bool.isRequired, capabilities: PropTypes.object.isRequired
supportsBooks: PropTypes.bool.isRequired,
supportsMusic: PropTypes.bool.isRequired,
supportsMovies: PropTypes.bool.isRequired
}; };
export default CapabilitiesLabel; export default CapabilitiesLabel;

View File

@@ -64,10 +64,7 @@ class MovieIndexRow extends Component {
protocol, protocol,
privacy, privacy,
added, added,
supportsTv, capabilities,
supportsBooks,
supportsMusic,
supportsMovies,
columns, columns,
isMovieEditorActive, isMovieEditorActive,
isSelected, isSelected,
@@ -175,10 +172,7 @@ class MovieIndexRow extends Component {
className={styles[column.name]} className={styles[column.name]}
> >
<CapabilitiesLabel <CapabilitiesLabel
supportsBooks={supportsBooks} capabilities={capabilities}
supportsMovies={supportsMovies}
supportsMusic={supportsMusic}
supportsTv={supportsTv}
/> />
</VirtualTableRowCell> </VirtualTableRowCell>
); );
@@ -243,10 +237,7 @@ MovieIndexRow.propTypes = {
enableRss: PropTypes.bool.isRequired, enableRss: PropTypes.bool.isRequired,
enableAutomaticSearch: PropTypes.bool.isRequired, enableAutomaticSearch: PropTypes.bool.isRequired,
enableInteractiveSearch: PropTypes.bool.isRequired, enableInteractiveSearch: PropTypes.bool.isRequired,
supportsTv: PropTypes.bool.isRequired, capabilities: PropTypes.object.isRequired,
supportsBooks: PropTypes.bool.isRequired,
supportsMusic: PropTypes.bool.isRequired,
supportsMovies: PropTypes.bool.isRequired,
added: PropTypes.string.isRequired, added: PropTypes.string.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired, tags: PropTypes.arrayOf(PropTypes.number).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,

View File

@@ -0,0 +1,6 @@
.message {
margin-top: 10px;
margin-bottom: 30px;
text-align: center;
font-size: 20px;
}

View File

@@ -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 (
<div>
<div className={styles.message}>
{translate('AllIndexersHiddenDueToFilter')}
</div>
</div>
);
}
return (
<div>
<div className={styles.message}>
No search results found, try performing a new search below.
</div>
</div>
);
}
NoSearchResults.propTypes = {
totalItems: PropTypes.number.isRequired
};
export default NoSearchResults;

View File

@@ -3,6 +3,11 @@
min-width: 150px; min-width: 150px;
} }
.indexerContainer {
margin-right: 20px;
min-width: 250px;
}
.buttonContainer { .buttonContainer {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import IndexersSelectInputConnector from 'Components/Form/IndexersSelectInputConnector';
import TextInput from 'Components/Form/TextInput'; import TextInput from 'Components/Form/TextInput';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter'; import PageContentFooter from 'Components/Page/PageContentFooter';
@@ -15,7 +16,8 @@ class SearchFooter extends Component {
this.state = { this.state = {
searchingReleases: false, searchingReleases: false,
searchQuery: '' searchQuery: '',
indexerIds: []
}; };
} }
@@ -40,7 +42,7 @@ class SearchFooter extends Component {
} }
onSearchPress = () => { 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; } = this.props;
const { const {
searchQuery searchQuery,
indexerIds
} = this.state; } = this.state;
return ( return (
@@ -67,6 +70,16 @@ class SearchFooter extends Component {
/> />
</div> </div>
<div className={styles.indexerContainer}>
<IndexersSelectInputConnector
name='indexerIds'
placeholder='Indexers'
value={indexerIds}
isDisabled={isFetching}
onChange={this.onInputChange}
/>
</div>
<div className={styles.buttonContainer}> <div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}> <div className={styles.buttonContainerContent}>
<div className={styles.buttons}> <div className={styles.buttons}>

View File

@@ -11,13 +11,13 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, sortDirections } from 'Helpers/Props'; import { align, icons, sortDirections } from 'Helpers/Props';
import NoIndexer from 'Indexer/NoIndexer';
import * as keyCodes from 'Utilities/Constants/keyCodes'; import * as keyCodes from 'Utilities/Constants/keyCodes';
import getErrorMessage from 'Utilities/Object/getErrorMessage'; import getErrorMessage from 'Utilities/Object/getErrorMessage';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu'; import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu';
import SearchIndexSortMenu from './Menus/SearchIndexSortMenu'; import SearchIndexSortMenu from './Menus/SearchIndexSortMenu';
import NoSearchResults from './NoSearchResults';
import SearchFooter from './SearchFooter.js'; import SearchFooter from './SearchFooter.js';
import SearchIndexTableConnector from './Table/SearchIndexTableConnector'; import SearchIndexTableConnector from './Table/SearchIndexTableConnector';
import styles from './SearchIndex.css'; import styles from './SearchIndex.css';
@@ -126,9 +126,8 @@ class SearchIndex extends Component {
this.setState({ jumpToCharacter }); this.setState({ jumpToCharacter });
} }
onSearchPress = (query) => { onSearchPress = (query, indexerIds) => {
console.log('index', query); this.props.onSearchPress({ query, indexerIds });
this.props.onSearchPress({ query });
} }
onKeyUp = (event) => { onKeyUp = (event) => {
@@ -175,6 +174,8 @@ class SearchIndex extends Component {
const isLoaded = !!(!error && isPopulated && items.length && scroller); const isLoaded = !!(!error && isPopulated && items.length && scroller);
const hasNoIndexer = !totalItems; const hasNoIndexer = !totalItems;
console.log(hasNoIndexer);
return ( return (
<PageContent> <PageContent>
<PageToolbar> <PageToolbar>
@@ -246,8 +247,8 @@ class SearchIndex extends Component {
} }
{ {
!error && isPopulated && !items.length && !error && !isFetching && !items.length &&
<NoIndexer totalItems={totalItems} /> <NoSearchResults totalItems={totalItems} />
} }
</PageContentBody> </PageContentBody>

View File

@@ -95,25 +95,7 @@ export const defaultState = {
], ],
sortPredicates: { sortPredicates: {
...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;
}
}, },
selectedFilterKey: 'all', selectedFilterKey: 'all',
@@ -122,12 +104,6 @@ export const defaultState = {
filterPredicates, filterPredicates,
filterBuilderProps: [ filterBuilderProps: [
{
name: 'monitored',
label: translate('Monitored'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
},
{ {
name: 'title', name: 'title',
label: 'Indexer Name', label: 'Indexer Name',

View File

@@ -1,11 +1,10 @@
import _ from 'lodash'; import _ from 'lodash';
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props'; import { sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
// import { batchActions } from 'redux-batched-actions'; // import { batchActions } from 'redux-batched-actions';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate'; import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate';
import padNumber from 'Utilities/Number/padNumber';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import { updateItem } from './baseActions'; import { updateItem } from './baseActions';
import createFetchHandler from './Creators/createFetchHandler'; import createFetchHandler from './Creators/createFetchHandler';
@@ -24,123 +23,12 @@ export const filters = [
key: 'all', key: 'all',
label: translate('All'), label: translate('All'),
filters: [] 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 = { export const filterPredicates = {
added: function(item, filterValue, type) { added: function(item, filterValue, type) {
return dateFilterPredicate(item.added, 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; 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, isSaving: false,
saveError: null, saveError: null,
items: [], items: [],
sortKey: 'sortTitle', sortKey: 'name',
sortDirection: sortDirections.ASCENDING, sortDirection: sortDirections.ASCENDING,
pendingChanges: {} pendingChanges: {}
}; };

View File

@@ -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);

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Xml; using System.Xml;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
@@ -6,7 +6,6 @@ using NUnit.Framework;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.IndexerTests.NewznabTests namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{ {
@@ -53,8 +52,8 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
var caps = Subject.GetCapabilities(_settings); var caps = Subject.GetCapabilities(_settings);
caps.DefaultPageSize.Should().Be(25); caps.LimitsDefault.Value.Should().Be(25);
caps.MaxPageSize.Should().Be(60); caps.LimitsMax.Value.Should().Be(60);
} }
[Test] [Test]
@@ -64,8 +63,8 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
var caps = Subject.GetCapabilities(_settings); var caps = Subject.GetCapabilities(_settings);
caps.DefaultPageSize.Should().Be(100); caps.LimitsDefault.Value.Should().Be(100);
caps.MaxPageSize.Should().Be(100); caps.LimitsMax.Value.Should().Be(100);
} }
[Test] [Test]

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Linq; using System.Linq;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
@@ -13,7 +13,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[TestFixture] [TestFixture]
public class NewznabFixture : CoreTest<Newznab> public class NewznabFixture : CoreTest<Newznab>
{ {
private NewznabCapabilities _caps; private IndexerCapabilities _caps;
[SetUp] [SetUp]
public void Setup() public void Setup()
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
} }
}; };
_caps = new NewznabCapabilities(); _caps = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>() Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>())) .Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>()))
.Returns(_caps); .Returns(_caps);
@@ -64,8 +64,8 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[Test] [Test]
public void should_use_pagesize_reported_by_caps() public void should_use_pagesize_reported_by_caps()
{ {
_caps.MaxPageSize = 30; _caps.LimitsMax = 30;
_caps.DefaultPageSize = 25; _caps.LimitsDefault = 25;
Subject.PageSize.Should().Be(25); Subject.PageSize.Should().Be(25);
} }

View File

@@ -3,6 +3,7 @@ using System.Linq;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@@ -12,7 +13,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
public class NewznabRequestGeneratorFixture : CoreTest<NewznabRequestGenerator> public class NewznabRequestGeneratorFixture : CoreTest<NewznabRequestGenerator>
{ {
private MovieSearchCriteria _movieSearchCriteria; private MovieSearchCriteria _movieSearchCriteria;
private NewznabCapabilities _capabilities; private IndexerCapabilities _capabilities;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
@@ -29,7 +30,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
SceneTitles = new List<string> { "Star Wars" } SceneTitles = new List<string> { "Star Wars" }
}; };
_capabilities = new NewznabCapabilities(); _capabilities = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>() Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>())) .Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>()))
@@ -91,7 +92,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[Test] [Test]
public void should_not_search_by_imdbid_if_not_supported() public void should_not_search_by_imdbid_if_not_supported()
{ {
_capabilities.SupportedMovieSearchParameters = new[] { "q" }; _capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q };
var results = Subject.GetSearchRequests(_movieSearchCriteria); var results = Subject.GetSearchRequests(_movieSearchCriteria);
@@ -106,7 +107,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[Test] [Test]
public void should_search_by_imdbid_if_supported() public void should_search_by_imdbid_if_supported()
{ {
_capabilities.SupportedMovieSearchParameters = new[] { "q", "imdbid" }; _capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.ImdbId };
var results = Subject.GetSearchRequests(_movieSearchCriteria); var results = Subject.GetSearchRequests(_movieSearchCriteria);
results.GetTier(0).Should().HaveCount(1); results.GetTier(0).Should().HaveCount(1);
@@ -119,7 +120,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[Test] [Test]
public void should_search_by_tmdbid_if_supported() public void should_search_by_tmdbid_if_supported()
{ {
_capabilities.SupportedMovieSearchParameters = new[] { "q", "tmdbid" }; _capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.TmdbId };
var results = Subject.GetSearchRequests(_movieSearchCriteria); var results = Subject.GetSearchRequests(_movieSearchCriteria);
results.GetTier(0).Should().HaveCount(1); results.GetTier(0).Should().HaveCount(1);
@@ -132,7 +133,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[Test] [Test]
public void should_prefer_search_by_tmdbid_if_rid_supported() public void should_prefer_search_by_tmdbid_if_rid_supported()
{ {
_capabilities.SupportedMovieSearchParameters = new[] { "q", "tmdbid", "imdbid" }; _capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId };
var results = Subject.GetSearchRequests(_movieSearchCriteria); var results = Subject.GetSearchRequests(_movieSearchCriteria);
results.GetTier(0).Should().HaveCount(1); results.GetTier(0).Should().HaveCount(1);
@@ -146,8 +147,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[Test] [Test]
public void should_use_aggregrated_id_search_if_supported() public void should_use_aggregrated_id_search_if_supported()
{ {
_capabilities.SupportedMovieSearchParameters = new[] { "q", "tmdbid", "imdbid" }; _capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId };
_capabilities.SupportsAggregateIdSearch = true;
var results = Subject.GetSearchRequests(_movieSearchCriteria); var results = Subject.GetSearchRequests(_movieSearchCriteria);
results.GetTier(0).Should().HaveCount(1); results.GetTier(0).Should().HaveCount(1);
@@ -161,8 +161,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[Test] [Test]
public void should_not_use_aggregrated_id_search_if_no_ids_supported() public void should_not_use_aggregrated_id_search_if_no_ids_supported()
{ {
_capabilities.SupportedMovieSearchParameters = new[] { "q" }; _capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q };
_capabilities.SupportsAggregateIdSearch = true; // Turns true if indexer supplies supportedParams.
var results = Subject.GetSearchRequests(_movieSearchCriteria); var results = Subject.GetSearchRequests(_movieSearchCriteria);
results.Tiers.Should().Be(1); results.Tiers.Should().Be(1);
@@ -176,8 +175,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[Test] [Test]
public void should_not_use_aggregrated_id_search_if_no_ids_are_known() public void should_not_use_aggregrated_id_search_if_no_ids_are_known()
{ {
_capabilities.SupportedMovieSearchParameters = new[] { "q", "imdbid" }; _capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.ImdbId };
_capabilities.SupportsAggregateIdSearch = true; // Turns true if indexer supplies supportedParams.
_movieSearchCriteria.ImdbId = null; _movieSearchCriteria.ImdbId = null;
@@ -191,8 +189,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[Test] [Test]
public void should_fallback_to_q() public void should_fallback_to_q()
{ {
_capabilities.SupportedMovieSearchParameters = new[] { "q", "tmdbid", "imdbid" }; _capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId };
_capabilities.SupportsAggregateIdSearch = true;
var results = Subject.GetSearchRequests(_movieSearchCriteria); var results = Subject.GetSearchRequests(_movieSearchCriteria);
results.Tiers.Should().Be(2); results.Tiers.Should().Be(2);

View File

@@ -1,4 +1,4 @@
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Linq; using System.Linq;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
[TestFixture] [TestFixture]
public class TorznabFixture : CoreTest<Torznab> public class TorznabFixture : CoreTest<Torznab>
{ {
private NewznabCapabilities _caps; private IndexerCapabilities _caps;
[SetUp] [SetUp]
public void Setup() public void Setup()
@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
} }
}; };
_caps = new NewznabCapabilities(); _caps = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>() Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>())) .Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>()))
.Returns(_caps); .Returns(_caps);
@@ -129,8 +129,8 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
[Test] [Test]
public void should_use_pagesize_reported_by_caps() public void should_use_pagesize_reported_by_caps()
{ {
_caps.MaxPageSize = 30; _caps.LimitsMax = 30;
_caps.DefaultPageSize = 25; _caps.LimitsDefault = 25;
Subject.PageSize.Should().Be(25); Subject.PageSize.Should().Be(25);
} }

View File

@@ -10,6 +10,7 @@
<PackageReference Include="Dapper" Version="2.0.30" /> <PackageReference Include="Dapper" Version="2.0.30" />
<PackageReference Include="NBuilder" Version="6.1.0" /> <PackageReference Include="NBuilder" Version="6.1.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
<PackageReference Include="YamlDotNet" Version="8.1.2" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'"> <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
<PackageReference Include="coverlet.collector" Version="1.2.1" PrivateAssets="all" /> <PackageReference Include="coverlet.collector" Version="1.2.1" PrivateAssets="all" />

View File

@@ -1,4 +1,4 @@
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;

View File

@@ -7,7 +7,6 @@ using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.CustomFilters; using NzbDrone.Core.CustomFilters;
using NzbDrone.Core.Datastore.Converters; using NzbDrone.Core.Datastore.Converters;
using NzbDrone.Core.History;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.Jobs; using NzbDrone.Core.Jobs;
@@ -82,7 +81,6 @@ namespace NzbDrone.Core.Datastore
SqlMapper.AddTypeHandler(new DapperLanguageIntConverter()); SqlMapper.AddTypeHandler(new DapperLanguageIntConverter());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<Language>>(new LanguageIntConverter())); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<Language>>(new LanguageIntConverter()));
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<string>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<string>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<ParsedMovieInfo>(new LanguageIntConverter()));
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<ReleaseInfo>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<ReleaseInfo>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<HashSet<int>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<HashSet<int>>());
SqlMapper.AddTypeHandler(new OsPathConverter()); SqlMapper.AddTypeHandler(new OsPathConverter());

View File

@@ -13,11 +13,11 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public List<string> SceneTitles { get; set; } public List<string> SceneTitles { get; set; }
public virtual bool MonitoredEpisodesOnly { get; set; }
public virtual bool UserInvokedSearch { get; set; } public virtual bool UserInvokedSearch { get; set; }
public virtual bool InteractiveSearch { get; set; } public virtual bool InteractiveSearch { get; set; }
public string ImdbId { get; set; } public string ImdbId { get; set; }
public int TmdbId { get; set; } public int TmdbId { get; set; }
public List<int> IndexerIds { get; set; }
public List<string> QueryTitles => SceneTitles.Select(GetQueryTitle).ToList(); public List<string> QueryTitles => SceneTitles.Select(GetQueryTitle).ToList();

View File

@@ -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;
}
}

View File

@@ -1,26 +0,0 @@
using NLog;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.IndexerSearch
{
public class MovieSearchService : IExecute<MoviesSearchCommand>
{
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);
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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(
@"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]",
RegexOptions.Compiled);
public List<ReleaseInfo> 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;
}
}
}

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using NLog; using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@@ -13,7 +14,8 @@ namespace NzbDrone.Core.IndexerSearch
{ {
public interface ISearchForNzb public interface ISearchForNzb
{ {
List<ReleaseInfo> MovieSearch(string movieId, bool userInvokedSearch, bool interactiveSearch); List<ReleaseInfo> Search(string query, List<int> indexerIds, bool userInvokedSearch, bool interactiveSearch);
NewznabResults Search(NewznabRequest request, List<int> indexerIds, bool userInvokedSearch, bool interactiveSearch);
} }
public class NzbSearchService : ISearchForNzb public class NzbSearchService : ISearchForNzb
@@ -28,14 +30,21 @@ namespace NzbDrone.Core.IndexerSearch
_logger = logger; _logger = logger;
} }
public List<ReleaseInfo> MovieSearch(string movie, bool userInvokedSearch, bool interactiveSearch) public List<ReleaseInfo> Search(string query, List<int> indexerIds, bool userInvokedSearch, bool interactiveSearch)
{ {
var searchSpec = Get<MovieSearchCriteria>(movie, userInvokedSearch, interactiveSearch); var searchSpec = Get<MovieSearchCriteria>(query, indexerIds, userInvokedSearch, interactiveSearch);
return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
} }
private TSpec Get<TSpec>(string movie, bool userInvokedSearch, bool interactiveSearch) public NewznabResults Search(NewznabRequest request, List<int> indexerIds, bool userInvokedSearch, bool interactiveSearch)
{
var searchSpec = Get<MovieSearchCriteria>(request.q, indexerIds, userInvokedSearch, interactiveSearch);
return new NewznabResults { Releases = Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) };
}
private TSpec Get<TSpec>(string query, List<int> indexerIds, bool userInvokedSearch, bool interactiveSearch)
where TSpec : SearchCriteriaBase, new() where TSpec : SearchCriteriaBase, new()
{ {
var spec = new TSpec() var spec = new TSpec()
@@ -44,7 +53,8 @@ namespace NzbDrone.Core.IndexerSearch
InteractiveSearch = interactiveSearch InteractiveSearch = interactiveSearch
}; };
spec.SceneTitles = new List<string> { movie }; spec.SceneTitles = new List<string> { query };
spec.IndexerIds = indexerIds;
return spec; return spec;
} }
@@ -55,6 +65,11 @@ namespace NzbDrone.Core.IndexerSearch
_indexerFactory.InteractiveSearchEnabled() : _indexerFactory.InteractiveSearchEnabled() :
_indexerFactory.AutomaticSearchEnabled(); _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<ReleaseInfo>(); var reports = new List<ReleaseInfo>();
_logger.ProgressInfo("Searching {0} indexers for {1}", indexers.Count, criteriaBase.QueryTitles.Join(", ")); _logger.ProgressInfo("Searching {0} indexers for {1}", indexers.Count, criteriaBase.QueryTitles.Join(", "));

View File

@@ -20,7 +20,7 @@ namespace NzbDrone.Core.Indexers.Newznab
public override DownloadProtocol Protocol => DownloadProtocol.Usenet; public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private; 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() public override IIndexerRequestGenerator GetRequestGenerator()
{ {
@@ -36,6 +36,14 @@ namespace NzbDrone.Core.Indexers.Newznab
return new NewznabRssParser(Settings); 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<ProviderDefinition> DefaultDefinitions public override IEnumerable<ProviderDefinition> DefaultDefinitions
{ {
get get
@@ -75,7 +83,8 @@ namespace NzbDrone.Core.Indexers.Newznab
Protocol = DownloadProtocol.Usenet, Protocol = DownloadProtocol.Usenet,
Privacy = IndexerPrivacy.Private, Privacy = IndexerPrivacy.Private,
SupportsRss = SupportsRss, SupportsRss = SupportsRss,
SupportsSearch = SupportsSearch SupportsSearch = SupportsSearch,
Capabilities = Capabilities
}; };
} }
@@ -107,15 +116,15 @@ namespace NzbDrone.Core.Indexers.Newznab
failures.AddIfNotNull(TestCapabilities()); failures.AddIfNotNull(TestCapabilities());
} }
protected static List<int> CategoryIds(List<NewznabCategory> categories) protected static List<int> CategoryIds(List<IndexerCategory> categories)
{ {
var l = categories.Select(c => c.Id).ToList(); var l = categories.Select(c => c.Id).ToList();
foreach (var category in categories) 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; return null;
} }
if (capabilities.SupportedMovieSearchParameters != null && if (capabilities.MovieSearchParams != null &&
new[] { "q", "imdbid" }.Any(v => capabilities.SupportedMovieSearchParameters.Contains(v)) && new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId }.Any(v => capabilities.MovieSearchParams.Contains(v)) &&
new[] { "imdbtitle", "imdbyear" }.All(v => capabilities.SupportedMovieSearchParameters.Contains(v))) new[] { MovieSearchParam.ImdbTitle, MovieSearchParam.ImdbYear }.All(v => capabilities.MovieSearchParams.Contains(v)))
{ {
return null; return null;
} }

View File

@@ -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<NewznabCategory> Categories { get; set; }
public NewznabCapabilities()
{
DefaultPageSize = 100;
MaxPageSize = 100;
SupportedSearchParameters = new[] { "q" };
SupportedMovieSearchParameters = new[] { "q", "imdbid", "imdbtitle", "imdbyear" };
SupportsAggregateIdSearch = false;
Categories = new List<NewznabCategory>();
}
}
public class NewznabCategory
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public List<NewznabCategory> Subcategories { get; set; }
}
}

View File

@@ -12,23 +12,23 @@ namespace NzbDrone.Core.Indexers.Newznab
{ {
public interface INewznabCapabilitiesProvider public interface INewznabCapabilitiesProvider
{ {
NewznabCapabilities GetCapabilities(NewznabSettings settings); IndexerCapabilities GetCapabilities(NewznabSettings settings);
} }
public class NewznabCapabilitiesProvider : INewznabCapabilitiesProvider public class NewznabCapabilitiesProvider : INewznabCapabilitiesProvider
{ {
private readonly ICached<NewznabCapabilities> _capabilitiesCache; private readonly ICached<IndexerCapabilities> _capabilitiesCache;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly Logger _logger; private readonly Logger _logger;
public NewznabCapabilitiesProvider(ICacheManager cacheManager, IHttpClient httpClient, Logger logger) public NewznabCapabilitiesProvider(ICacheManager cacheManager, IHttpClient httpClient, Logger logger)
{ {
_capabilitiesCache = cacheManager.GetCache<NewznabCapabilities>(GetType()); _capabilitiesCache = cacheManager.GetCache<IndexerCapabilities>(GetType());
_httpClient = httpClient; _httpClient = httpClient;
_logger = logger; _logger = logger;
} }
public NewznabCapabilities GetCapabilities(NewznabSettings indexerSettings) public IndexerCapabilities GetCapabilities(NewznabSettings indexerSettings)
{ {
var key = indexerSettings.ToJson(); var key = indexerSettings.ToJson();
var capabilities = _capabilitiesCache.Get(key, () => FetchCapabilities(indexerSettings), TimeSpan.FromDays(7)); var capabilities = _capabilitiesCache.Get(key, () => FetchCapabilities(indexerSettings), TimeSpan.FromDays(7));
@@ -36,9 +36,9 @@ namespace NzbDrone.Core.Indexers.Newznab
return capabilities; 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('/')); 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; 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); var xDoc = XDocument.Parse(response.Content);
@@ -104,8 +104,8 @@ namespace NzbDrone.Core.Indexers.Newznab
var xmlLimits = xmlRoot.Element("limits"); var xmlLimits = xmlRoot.Element("limits");
if (xmlLimits != null) if (xmlLimits != null)
{ {
capabilities.DefaultPageSize = int.Parse(xmlLimits.Attribute("default").Value); capabilities.LimitsDefault = int.Parse(xmlLimits.Attribute("default").Value);
capabilities.MaxPageSize = int.Parse(xmlLimits.Attribute("max").Value); capabilities.LimitsMax = int.Parse(xmlLimits.Attribute("max").Value);
} }
var xmlSearching = xmlRoot.Element("searching"); var xmlSearching = xmlRoot.Element("searching");
@@ -114,22 +114,81 @@ namespace NzbDrone.Core.Indexers.Newznab
var xmlBasicSearch = xmlSearching.Element("search"); var xmlBasicSearch = xmlSearching.Element("search");
if (xmlBasicSearch == null || xmlBasicSearch.Attribute("available").Value != "yes") if (xmlBasicSearch == null || xmlBasicSearch.Attribute("available").Value != "yes")
{ {
capabilities.SupportedSearchParameters = null; capabilities.SearchParams = new List<SearchParam>();
} }
else if (xmlBasicSearch.Attribute("supportedParams") != null) 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"); var xmlMovieSearch = xmlSearching.Element("movie-search");
if (xmlMovieSearch == null || xmlMovieSearch.Attribute("available").Value != "yes") if (xmlMovieSearch == null || xmlMovieSearch.Attribute("available").Value != "yes")
{ {
capabilities.SupportedMovieSearchParameters = null; capabilities.MovieSearchParams = new List<MovieSearchParam>();
} }
else if (xmlMovieSearch.Attribute("supportedParams") != null) else if (xmlMovieSearch.Attribute("supportedParams") != null)
{ {
capabilities.SupportedMovieSearchParameters = xmlMovieSearch.Attribute("supportedParams").Value.Split(','); foreach (var param in xmlMovieSearch.Attribute("supportedParams").Value.Split(','))
capabilities.SupportsAggregateIdSearch = true; {
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<TvSearchParam>();
}
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<MusicSearchParam>();
}
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<BookSearchParam>();
}
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")) foreach (var xmlCategory in xmlCategories.Elements("category"))
{ {
var cat = new NewznabCategory var cat = new IndexerCategory
{ {
Id = int.Parse(xmlCategory.Attribute("id").Value), Id = int.Parse(xmlCategory.Attribute("id").Value),
Name = xmlCategory.Attribute("name").Value, Name = xmlCategory.Attribute("name").Value,
Description = xmlCategory.Attribute("description") != null ? xmlCategory.Attribute("description").Value : string.Empty, Description = xmlCategory.Attribute("description") != null ? xmlCategory.Attribute("description").Value : string.Empty
Subcategories = new List<NewznabCategory>()
}; };
foreach (var xmlSubcat in xmlCategory.Elements("subcat")) foreach (var xmlSubcat in xmlCategory.Elements("subcat"))
{ {
cat.Subcategories.Add(new NewznabCategory cat.SubCategories.Add(new IndexerCategory
{ {
Id = int.Parse(xmlSubcat.Attribute("id").Value), Id = int.Parse(xmlSubcat.Attribute("id").Value),
Name = xmlSubcat.Attribute("name").Value, Name = xmlSubcat.Attribute("name").Value,

View File

@@ -28,8 +28,8 @@ namespace NzbDrone.Core.Indexers.Newznab
{ {
var capabilities = _capabilitiesProvider.GetCapabilities(Settings); var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
return capabilities.SupportedSearchParameters != null && return capabilities.SearchParams != null &&
capabilities.SupportedSearchParameters.Contains("q"); capabilities.SearchParams.Contains(SearchParam.Q);
} }
} }
@@ -39,8 +39,8 @@ namespace NzbDrone.Core.Indexers.Newznab
{ {
var capabilities = _capabilitiesProvider.GetCapabilities(Settings); var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
return capabilities.SupportedMovieSearchParameters != null && return capabilities.MovieSearchParams != null &&
capabilities.SupportedMovieSearchParameters.Contains("imdbid"); capabilities.MovieSearchParams.Contains(MovieSearchParam.ImdbId);
} }
} }
@@ -50,8 +50,8 @@ namespace NzbDrone.Core.Indexers.Newznab
{ {
var capabilities = _capabilitiesProvider.GetCapabilities(Settings); var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
return capabilities.SupportedMovieSearchParameters != null && return capabilities.MovieSearchParams != null &&
capabilities.SupportedMovieSearchParameters.Contains("tmdbid"); capabilities.MovieSearchParams.Contains(MovieSearchParam.TmdbId);
} }
} }
@@ -61,7 +61,8 @@ namespace NzbDrone.Core.Indexers.Newznab
{ {
var capabilities = _capabilitiesProvider.GetCapabilities(Settings); 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); 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. // 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", "")); pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "movie", ""));
} }
else if (capabilities.SupportedSearchParameters != null) else if (capabilities.SearchParams != null)
{ {
pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", "")); pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", ""));
} }

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@@ -148,19 +147,6 @@ namespace NzbDrone.Core.Indexers.Newznab
return 0; 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) protected virtual int GetImdbYear(XElement item)
{ {
var imdbYearString = TryGetNewznabAttribute(item, "imdbyear"); var imdbYearString = TryGetNewznabAttribute(item, "imdbyear");

View File

@@ -20,7 +20,8 @@ namespace NzbDrone.Core.Indexers.Torznab
public override DownloadProtocol Protocol => DownloadProtocol.Torrent; public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private; 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() public override IIndexerRequestGenerator GetRequestGenerator()
{ {
@@ -63,7 +64,8 @@ namespace NzbDrone.Core.Indexers.Torznab
Settings = settings, Settings = settings,
Protocol = DownloadProtocol.Usenet, Protocol = DownloadProtocol.Usenet,
SupportsRss = SupportsRss, SupportsRss = SupportsRss,
SupportsSearch = SupportsSearch SupportsSearch = SupportsSearch,
Capabilities = new IndexerCapabilities()
}; };
} }
@@ -95,15 +97,15 @@ namespace NzbDrone.Core.Indexers.Torznab
failures.AddIfNotNull(TestCapabilities()); failures.AddIfNotNull(TestCapabilities());
} }
protected static List<int> CategoryIds(List<NewznabCategory> categories) protected static List<int> CategoryIds(List<IndexerCategory> categories)
{ {
var l = categories.Select(c => c.Id).ToList(); var l = categories.Select(c => c.Id).ToList();
foreach (var category in categories) 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; return null;
} }
if (capabilities.SupportedMovieSearchParameters != null && if (capabilities.MovieSearchParams != null &&
new[] { "q", "imdbid" }.Any(v => capabilities.SupportedMovieSearchParameters.Contains(v)) && new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId }.Any(v => capabilities.MovieSearchParams.Contains(v)) &&
new[] { "imdbtitle", "imdbyear" }.All(v => capabilities.SupportedMovieSearchParameters.Contains(v))) new[] { MovieSearchParam.ImdbTitle, MovieSearchParam.ImdbYear }.All(v => capabilities.MovieSearchParams.Contains(v)))
{ {
return null; return null;
} }

View File

@@ -1,11 +1,8 @@
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using FluentValidation; using FluentValidation;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Torznab 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(); private static readonly TorznabSettingsValidator Validator = new TorznabSettingsValidator();
public TorznabSettings() public TorznabSettings()
{ {
MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS;
RequiredFlags = new List<int>();
} }
[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<int> RequiredFlags { get; set; }
public override NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -86,6 +86,11 @@ namespace NzbDrone.Core.Indexers
return requests; return requests;
} }
public override IndexerCapabilities GetCapabilities()
{
return Capabilities;
}
protected virtual IList<ReleaseInfo> FetchReleases(Func<IIndexerRequestGenerator, IndexerPageableRequestChain> pageableRequestChainSelector, bool isRecent = false) protected virtual IList<ReleaseInfo> FetchReleases(Func<IIndexerRequestGenerator, IndexerPageableRequestChain> pageableRequestChainSelector, bool isRecent = false)
{ {
var releases = new List<ReleaseInfo>(); var releases = new List<ReleaseInfo>();

View File

@@ -16,5 +16,7 @@ namespace NzbDrone.Core.Indexers
IList<ReleaseInfo> FetchRecent(); IList<ReleaseInfo> FetchRecent();
IList<ReleaseInfo> Fetch(MovieSearchCriteria searchCriteria); IList<ReleaseInfo> Fetch(MovieSearchCriteria searchCriteria);
IndexerCapabilities GetCapabilities();
} }
} }

View File

@@ -68,6 +68,8 @@ namespace NzbDrone.Core.Indexers
public abstract IList<ReleaseInfo> FetchRecent(); public abstract IList<ReleaseInfo> FetchRecent();
public abstract IList<ReleaseInfo> Fetch(MovieSearchCriteria searchCriteria); public abstract IList<ReleaseInfo> Fetch(MovieSearchCriteria searchCriteria);
public abstract IndexerCapabilities GetCapabilities();
protected virtual IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases) protected virtual IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases)
{ {
var result = releases.DistinctBy(v => v.Guid).ToList(); var result = releases.DistinctBy(v => v.Guid).ToList();

View File

@@ -13,13 +13,16 @@ namespace NzbDrone.Core.Indexers
ImdbId, ImdbId,
TvdbId, TvdbId,
RId, RId,
TvMazeId
} }
public enum MovieSearchParam public enum MovieSearchParam
{ {
Q, Q,
ImdbId, ImdbId,
TmdbId TmdbId,
ImdbTitle,
ImdbYear
} }
public enum MusicSearchParam public enum MusicSearchParam
@@ -31,12 +34,23 @@ namespace NzbDrone.Core.Indexers
Year Year
} }
public enum SearchParam
{
Q
}
public enum BookSearchParam
{
Q
}
public class IndexerCapabilities public class IndexerCapabilities
{ {
public int? LimitsMax { get; set; } public int? LimitsMax { get; set; }
public int? LimitsDefault { get; set; } public int? LimitsDefault { get; set; }
public bool SearchAvailable { get; set; } public List<SearchParam> SearchParams;
public bool SearchAvailable => SearchParams.Count > 0;
public List<TvSearchParam> TvSearchParams; public List<TvSearchParam> TvSearchParams;
public bool TvSearchAvailable => TvSearchParams.Count > 0; public bool TvSearchAvailable => TvSearchParams.Count > 0;
@@ -45,6 +59,7 @@ namespace NzbDrone.Core.Indexers
public bool TvSearchImdbAvailable => TvSearchParams.Contains(TvSearchParam.ImdbId); public bool TvSearchImdbAvailable => TvSearchParams.Contains(TvSearchParam.ImdbId);
public bool TvSearchTvdbAvailable => TvSearchParams.Contains(TvSearchParam.TvdbId); public bool TvSearchTvdbAvailable => TvSearchParams.Contains(TvSearchParam.TvdbId);
public bool TvSearchTvRageAvailable => TvSearchParams.Contains(TvSearchParam.RId); public bool TvSearchTvRageAvailable => TvSearchParams.Contains(TvSearchParam.RId);
public bool TvSearchTvMazeAvailable => TvSearchParams.Contains(TvSearchParam.TvMazeId);
public List<MovieSearchParam> MovieSearchParams; public List<MovieSearchParam> MovieSearchParams;
public bool MovieSearchAvailable => MovieSearchParams.Count > 0; public bool MovieSearchAvailable => MovieSearchParams.Count > 0;
@@ -58,17 +73,18 @@ namespace NzbDrone.Core.Indexers
public bool MusicSearchLabelAvailable => MusicSearchParams.Contains(MusicSearchParam.Label); public bool MusicSearchLabelAvailable => MusicSearchParams.Contains(MusicSearchParam.Label);
public bool MusicSearchYearAvailable => MusicSearchParams.Contains(MusicSearchParam.Year); public bool MusicSearchYearAvailable => MusicSearchParams.Contains(MusicSearchParam.Year);
public bool BookSearchAvailable { get; set; } public List<BookSearchParam> BookSearchParams;
public bool BookSearchAvailable => BookSearchParams.Count > 0;
public List<IndexerCategory> Categories { get; private set; } public List<IndexerCategory> Categories { get; private set; }
public IndexerCapabilities() public IndexerCapabilities()
{ {
SearchAvailable = true; SearchParams = new List<SearchParam>();
TvSearchParams = new List<TvSearchParam>(); TvSearchParams = new List<TvSearchParam>();
MovieSearchParams = new List<MovieSearchParam>(); MovieSearchParams = new List<MovieSearchParam>();
MusicSearchParams = new List<MusicSearchParam>(); MusicSearchParams = new List<MusicSearchParam>();
BookSearchAvailable = false; BookSearchParams = new List<BookSearchParam>();
Categories = new List<IndexerCategory>(); Categories = new List<IndexerCategory>();
} }
@@ -181,6 +197,11 @@ namespace NzbDrone.Core.Indexers
parameters.Add("rid"); parameters.Add("rid");
} }
if (TvSearchTvMazeAvailable)
{
parameters.Add("tvmazeid");
}
return string.Join(",", parameters); return string.Join(",", parameters);
} }
@@ -244,7 +265,7 @@ namespace NzbDrone.Core.Indexers
{ {
var subCategories = Categories.SelectMany(c => c.SubCategories); var subCategories = Categories.SelectMany(c => c.SubCategories);
var allCategories = Categories.Concat(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; return supportsCategory;
} }
@@ -280,13 +301,13 @@ namespace NzbDrone.Core.Indexers
new XAttribute("available", BookSearchAvailable ? "yes" : "no"), new XAttribute("available", BookSearchAvailable ? "yes" : "no"),
new XAttribute("supportedParams", SupportedBookSearchParams))), new XAttribute("supportedParams", SupportedBookSearchParams))),
new XElement("categories", 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", select new XElement("category",
new XAttribute("id", c.ID), new XAttribute("id", c.Id),
new XAttribute("name", c.Name), new XAttribute("name", c.Name),
from sc in c.SubCategories from sc in c.SubCategories
select new XElement("subcat", select new XElement("subcat",
new XAttribute("id", sc.ID), new XAttribute("id", sc.Id),
new XAttribute("name", sc.Name)))))); new XAttribute("name", sc.Name))))));
return xdoc; return xdoc;
} }
@@ -296,12 +317,12 @@ namespace NzbDrone.Core.Indexers
public static IndexerCapabilities Concat(IndexerCapabilities left, IndexerCapabilities right) 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.TvSearchParams = left.TvSearchParams.Union(right.TvSearchParams).ToList();
left.MovieSearchParams = left.MovieSearchParams.Union(right.MovieSearchParams).ToList(); left.MovieSearchParams = left.MovieSearchParams.Union(right.MovieSearchParams).ToList();
left.MusicSearchParams = left.MusicSearchParams.Union(right.MusicSearchParams).ToList(); left.MusicSearchParams = left.MusicSearchParams.Union(right.MusicSearchParams).ToList();
left.BookSearchAvailable = left.BookSearchAvailable || right.BookSearchAvailable; 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) left.Categories.AddRange(right.Categories.Where(x => x.Id < 100000).Except(left.Categories)); // exclude indexer specific categories (>= 100000)
return left; return left;
} }
} }

View File

@@ -5,8 +5,9 @@ namespace NzbDrone.Core.Indexers
{ {
public class IndexerCategory public class IndexerCategory
{ {
public int ID { get; set; } public int Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Description { get; set; }
public List<IndexerCategory> SubCategories { get; private set; } public List<IndexerCategory> SubCategories { get; private set; }
@@ -14,7 +15,7 @@ namespace NzbDrone.Core.Indexers
public IndexerCategory(int id, string name) public IndexerCategory(int id, string name)
{ {
ID = id; Id = id;
Name = name; Name = name;
SubCategories = new List<IndexerCategory>(); SubCategories = new List<IndexerCategory>();
} }
@@ -25,14 +26,14 @@ namespace NzbDrone.Core.Indexers
public JToken ToJson() => public JToken ToJson() =>
new JObject new JObject
{ {
["ID"] = ID, ["ID"] = Id,
["Name"] = Name ["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. // Get Hash code should be calculated off read only properties.
// ID is not readonly // ID is not readonly
public override int GetHashCode() => ID; public override int GetHashCode() => Id;
} }
} }

View File

@@ -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<Language> Languages { get; set; } = new List<Language>();
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<string, object> ExtraInfo { get; set; } = new Dictionary<string, object>();
public override string ToString()
{
return string.Format("{0} - {1}", MovieTitle, Year);
}
}
}

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Text; using System.Text;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
@@ -19,6 +19,7 @@ namespace NzbDrone.Core.Parser.Model
public int TvdbId { get; set; } public int TvdbId { get; set; }
public int TvRageId { get; set; } public int TvRageId { get; set; }
public int ImdbId { get; set; } public int ImdbId { get; set; }
public int TmdbId { get; set; }
public DateTime PublishDate { get; set; } public DateTime PublishDate { get; set; }
public string Origin { get; set; } public string Origin { get; set; }

View File

@@ -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; }
}
}

View File

@@ -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(@"^.*(?<year>(19|20)\d{2}).*$", RegexOptions.Compiled);
private static readonly Regex EditionRegex = new Regex(@"\(?\b(?<edition>(((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(@"^(?<title>(?![(\[]).+?)((\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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,5 +1,9 @@
using System.Collections.Generic;
using Nancy; using Nancy;
using Nancy.ModelBinding;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch;
using Prowlarr.Http.REST; using Prowlarr.Http.REST;
namespace Prowlarr.Api.V1.Indexers namespace Prowlarr.Api.V1.Indexers
@@ -9,13 +13,19 @@ namespace Prowlarr.Api.V1.Indexers
public static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper(); public static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper();
private IIndexerFactory _indexerFactory { get; set; } private IIndexerFactory _indexerFactory { get; set; }
private ISearchForNzb _nzbSearchService { get; set; }
public IndexerModule(IndexerFactory indexerFactory) public IndexerModule(IndexerFactory indexerFactory, ISearchForNzb nzbSearchService)
: base(indexerFactory, "indexer", ResourceMapper) : base(indexerFactory, "indexer", ResourceMapper)
{ {
_indexerFactory = indexerFactory; _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) protected override void Validate(IndexerDefinition definition, bool includeWarnings)
@@ -28,25 +38,36 @@ namespace Prowlarr.Api.V1.Indexers
base.Validate(definition, includeWarnings); 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"); 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)); throw new NotFoundException("Indexer Not Found");
Response response = indexer.Capabilities.ToXml();
response.ContentType = "application/rss+xml";
return response;
} }
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");
} }
} }
} }

View File

@@ -10,10 +10,6 @@ namespace Prowlarr.Api.V1.Indexers
public bool EnableInteractiveSearch { get; set; } public bool EnableInteractiveSearch { get; set; }
public bool SupportsRss { get; set; } public bool SupportsRss { get; set; }
public bool SupportsSearch { 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 DownloadProtocol Protocol { get; set; }
public IndexerPrivacy Privacy { get; set; } public IndexerPrivacy Privacy { get; set; }
public IndexerCapabilities Capabilities { get; set; } public IndexerCapabilities Capabilities { get; set; }

View File

@@ -26,17 +26,26 @@ namespace Prowlarr.Api.V1.Search
{ {
if (Request.Query.query.HasValue) 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>(); return new List<SearchResource>();
} }
private List<SearchResource> GetSearchReleases(string query) private List<SearchResource> GetSearchReleases(string query, List<int> indexerIds)
{ {
try try
{ {
var decisions = _nzbSearhService.MovieSearch(query, true, true); var decisions = _nzbSearhService.Search(query, indexerIds, true, true);
return MapDecisions(decisions); return MapDecisions(decisions);
} }