mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
Newznab Responses for Caps and Movie Search (rough)
This commit is contained in:
55
frontend/src/Components/Form/IndexersSelectInputConnector.js
Normal file
55
frontend/src/Components/Form/IndexersSelectInputConnector.js
Normal 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);
|
@@ -26,34 +26,16 @@ function MovieIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Monitored/Status
|
||||
Status
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="sortTitle"
|
||||
name="name"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Title')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="studio"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Studio')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="qualityProfileId"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('QualityProfile')}
|
||||
{translate('Name')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -66,66 +48,21 @@ function MovieIndexSortMenu(props) {
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="year"
|
||||
name="protocol"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Year')}
|
||||
{'Protocol'}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="inCinemas"
|
||||
name="privacy"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('InCinemas')}
|
||||
</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')}
|
||||
{'Privacy'}
|
||||
</SortMenuItem>
|
||||
</MenuContent>
|
||||
</SortMenu>
|
||||
|
@@ -4,16 +4,16 @@ import Label from 'Components/Label';
|
||||
|
||||
function CapabilitiesLabel(props) {
|
||||
const {
|
||||
supportsBooks,
|
||||
supportsMovies,
|
||||
supportsMusic,
|
||||
supportsTv
|
||||
} = props;
|
||||
movieSearchAvailable,
|
||||
tvSearchAvailable,
|
||||
musicSearchAvailable,
|
||||
bookSearchAvailable
|
||||
} = props.capabilities;
|
||||
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
supportsBooks ?
|
||||
bookSearchAvailable ?
|
||||
<Label>
|
||||
{'Books'}
|
||||
</Label> :
|
||||
@@ -21,7 +21,7 @@ function CapabilitiesLabel(props) {
|
||||
}
|
||||
|
||||
{
|
||||
supportsMovies ?
|
||||
movieSearchAvailable ?
|
||||
<Label>
|
||||
{'Movies'}
|
||||
</Label> :
|
||||
@@ -29,7 +29,7 @@ function CapabilitiesLabel(props) {
|
||||
}
|
||||
|
||||
{
|
||||
supportsMusic ?
|
||||
musicSearchAvailable ?
|
||||
<Label>
|
||||
{'Music'}
|
||||
</Label> :
|
||||
@@ -37,7 +37,7 @@ function CapabilitiesLabel(props) {
|
||||
}
|
||||
|
||||
{
|
||||
supportsTv ?
|
||||
tvSearchAvailable ?
|
||||
<Label>
|
||||
{'TV'}
|
||||
</Label> :
|
||||
@@ -45,7 +45,7 @@ function CapabilitiesLabel(props) {
|
||||
}
|
||||
|
||||
{
|
||||
!supportsTv && !supportsMusic && !supportsMovies && !supportsBooks ?
|
||||
!tvSearchAvailable && !musicSearchAvailable && !movieSearchAvailable && !bookSearchAvailable ?
|
||||
<Label>
|
||||
{'None'}
|
||||
</Label> :
|
||||
@@ -56,10 +56,7 @@ function CapabilitiesLabel(props) {
|
||||
}
|
||||
|
||||
CapabilitiesLabel.propTypes = {
|
||||
supportsTv: PropTypes.bool.isRequired,
|
||||
supportsBooks: PropTypes.bool.isRequired,
|
||||
supportsMusic: PropTypes.bool.isRequired,
|
||||
supportsMovies: PropTypes.bool.isRequired
|
||||
capabilities: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default CapabilitiesLabel;
|
||||
|
@@ -64,10 +64,7 @@ class MovieIndexRow extends Component {
|
||||
protocol,
|
||||
privacy,
|
||||
added,
|
||||
supportsTv,
|
||||
supportsBooks,
|
||||
supportsMusic,
|
||||
supportsMovies,
|
||||
capabilities,
|
||||
columns,
|
||||
isMovieEditorActive,
|
||||
isSelected,
|
||||
@@ -175,10 +172,7 @@ class MovieIndexRow extends Component {
|
||||
className={styles[column.name]}
|
||||
>
|
||||
<CapabilitiesLabel
|
||||
supportsBooks={supportsBooks}
|
||||
supportsMovies={supportsMovies}
|
||||
supportsMusic={supportsMusic}
|
||||
supportsTv={supportsTv}
|
||||
capabilities={capabilities}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
@@ -243,10 +237,7 @@ MovieIndexRow.propTypes = {
|
||||
enableRss: PropTypes.bool.isRequired,
|
||||
enableAutomaticSearch: PropTypes.bool.isRequired,
|
||||
enableInteractiveSearch: PropTypes.bool.isRequired,
|
||||
supportsTv: PropTypes.bool.isRequired,
|
||||
supportsBooks: PropTypes.bool.isRequired,
|
||||
supportsMusic: PropTypes.bool.isRequired,
|
||||
supportsMovies: PropTypes.bool.isRequired,
|
||||
capabilities: PropTypes.object.isRequired,
|
||||
added: PropTypes.string.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
6
frontend/src/Search/NoSearchResults.css
Normal file
6
frontend/src/Search/NoSearchResults.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.message {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
}
|
32
frontend/src/Search/NoSearchResults.js
Normal file
32
frontend/src/Search/NoSearchResults.js
Normal 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;
|
@@ -3,6 +3,11 @@
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.indexerContainer {
|
||||
margin-right: 20px;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import IndexersSelectInputConnector from 'Components/Form/IndexersSelectInputConnector';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
@@ -15,7 +16,8 @@ class SearchFooter extends Component {
|
||||
|
||||
this.state = {
|
||||
searchingReleases: false,
|
||||
searchQuery: ''
|
||||
searchQuery: '',
|
||||
indexerIds: []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,7 +42,7 @@ class SearchFooter extends Component {
|
||||
}
|
||||
|
||||
onSearchPress = () => {
|
||||
this.props.onSearchPress(this.state.searchQuery);
|
||||
this.props.onSearchPress(this.state.searchQuery, this.state.indexerIds);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -52,7 +54,8 @@ class SearchFooter extends Component {
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
searchQuery
|
||||
searchQuery,
|
||||
indexerIds
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
@@ -67,6 +70,16 @@ class SearchFooter extends Component {
|
||||
/>
|
||||
</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.buttonContainerContent}>
|
||||
<div className={styles.buttons}>
|
||||
|
@@ -11,13 +11,13 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import NoIndexer from 'Indexer/NoIndexer';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu';
|
||||
import SearchIndexSortMenu from './Menus/SearchIndexSortMenu';
|
||||
import NoSearchResults from './NoSearchResults';
|
||||
import SearchFooter from './SearchFooter.js';
|
||||
import SearchIndexTableConnector from './Table/SearchIndexTableConnector';
|
||||
import styles from './SearchIndex.css';
|
||||
@@ -126,9 +126,8 @@ class SearchIndex extends Component {
|
||||
this.setState({ jumpToCharacter });
|
||||
}
|
||||
|
||||
onSearchPress = (query) => {
|
||||
console.log('index', query);
|
||||
this.props.onSearchPress({ query });
|
||||
onSearchPress = (query, indexerIds) => {
|
||||
this.props.onSearchPress({ query, indexerIds });
|
||||
}
|
||||
|
||||
onKeyUp = (event) => {
|
||||
@@ -175,6 +174,8 @@ class SearchIndex extends Component {
|
||||
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
||||
const hasNoIndexer = !totalItems;
|
||||
|
||||
console.log(hasNoIndexer);
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<PageToolbar>
|
||||
@@ -246,8 +247,8 @@ class SearchIndex extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!error && isPopulated && !items.length &&
|
||||
<NoIndexer totalItems={totalItems} />
|
||||
!error && !isFetching && !items.length &&
|
||||
<NoSearchResults totalItems={totalItems} />
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
|
@@ -95,25 +95,7 @@ export const defaultState = {
|
||||
],
|
||||
|
||||
sortPredicates: {
|
||||
...sortPredicates,
|
||||
|
||||
studio: function(item) {
|
||||
const studio = item.studio;
|
||||
|
||||
return studio ? studio.toLowerCase() : '';
|
||||
},
|
||||
|
||||
collection: function(item) {
|
||||
const { collection ={} } = item;
|
||||
|
||||
return collection.name;
|
||||
},
|
||||
|
||||
ratings: function(item) {
|
||||
const { ratings = {} } = item;
|
||||
|
||||
return ratings.value;
|
||||
}
|
||||
...sortPredicates
|
||||
},
|
||||
|
||||
selectedFilterKey: 'all',
|
||||
@@ -122,12 +104,6 @@ export const defaultState = {
|
||||
filterPredicates,
|
||||
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'monitored',
|
||||
label: translate('Monitored'),
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.BOOL
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Indexer Name',
|
||||
|
@@ -1,11 +1,10 @@
|
||||
import _ from 'lodash';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
// import { batchActions } from 'redux-batched-actions';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate';
|
||||
import padNumber from 'Utilities/Number/padNumber';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { updateItem } from './baseActions';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
@@ -24,123 +23,12 @@ export const filters = [
|
||||
key: 'all',
|
||||
label: translate('All'),
|
||||
filters: []
|
||||
},
|
||||
{
|
||||
key: 'monitored',
|
||||
label: translate('MonitoredOnly'),
|
||||
filters: [
|
||||
{
|
||||
key: 'monitored',
|
||||
value: true,
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'unmonitored',
|
||||
label: translate('Unmonitored'),
|
||||
filters: [
|
||||
{
|
||||
key: 'monitored',
|
||||
value: false,
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'missing',
|
||||
label: translate('Missing'),
|
||||
filters: [
|
||||
{
|
||||
key: 'monitored',
|
||||
value: true,
|
||||
type: filterTypes.EQUAL
|
||||
},
|
||||
{
|
||||
key: 'hasFile',
|
||||
value: false,
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'wanted',
|
||||
label: translate('Wanted'),
|
||||
filters: [
|
||||
{
|
||||
key: 'monitored',
|
||||
value: true,
|
||||
type: filterTypes.EQUAL
|
||||
},
|
||||
{
|
||||
key: 'hasFile',
|
||||
value: false,
|
||||
type: filterTypes.EQUAL
|
||||
},
|
||||
{
|
||||
key: 'isAvailable',
|
||||
value: true,
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'cutoffunmet',
|
||||
label: translate('CutoffUnmet'),
|
||||
filters: [
|
||||
{
|
||||
key: 'monitored',
|
||||
value: true,
|
||||
type: filterTypes.EQUAL
|
||||
},
|
||||
{
|
||||
key: 'hasFile',
|
||||
value: true,
|
||||
type: filterTypes.EQUAL
|
||||
},
|
||||
{
|
||||
key: 'qualityCutoffNotMet',
|
||||
value: true,
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const filterPredicates = {
|
||||
added: function(item, filterValue, type) {
|
||||
return dateFilterPredicate(item.added, filterValue, type);
|
||||
},
|
||||
|
||||
collection: function(item, filterValue, type) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
const { collection } = item;
|
||||
|
||||
return predicate(collection ? collection.name : '', filterValue);
|
||||
},
|
||||
|
||||
inCinemas: function(item, filterValue, type) {
|
||||
return dateFilterPredicate(item.inCinemas, filterValue, type);
|
||||
},
|
||||
|
||||
physicalRelease: function(item, filterValue, type) {
|
||||
return dateFilterPredicate(item.physicalRelease, filterValue, type);
|
||||
},
|
||||
|
||||
digitalRelease: function(item, filterValue, type) {
|
||||
return dateFilterPredicate(item.digitalRelease, filterValue, type);
|
||||
},
|
||||
|
||||
ratings: function(item, filterValue, type) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
|
||||
return predicate(item.ratings.value * 10, filterValue);
|
||||
},
|
||||
|
||||
qualityCutoffNotMet: function(item) {
|
||||
const { movieFile = {} } = item;
|
||||
|
||||
return movieFile.qualityCutoffNotMet;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -165,33 +53,6 @@ export const sortPredicates = {
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
movieStatus: function(item) {
|
||||
let result = 0;
|
||||
let qualityName = '';
|
||||
|
||||
const hasMovieFile = !!item.movieFile;
|
||||
|
||||
if (item.isAvailable) {
|
||||
result++;
|
||||
}
|
||||
|
||||
if (item.monitored) {
|
||||
result += 2;
|
||||
}
|
||||
|
||||
if (hasMovieFile) {
|
||||
// TODO: Consider Quality Weight for Sorting within status of hasMovie
|
||||
if (item.movieFile.qualityCutoffNotMet) {
|
||||
result += 4;
|
||||
} else {
|
||||
result += 8;
|
||||
}
|
||||
qualityName = item.movieFile.quality.quality.name;
|
||||
}
|
||||
|
||||
return padNumber(result.toString(), 2) + qualityName;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -205,7 +66,7 @@ export const defaultState = {
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
items: [],
|
||||
sortKey: 'sortTitle',
|
||||
sortKey: 'name',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
pendingChanges: {}
|
||||
};
|
||||
|
@@ -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);
|
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Xml;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
@@ -6,7 +6,6 @@ using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers.Newznab;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
{
|
||||
@@ -53,8 +52,8 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
|
||||
var caps = Subject.GetCapabilities(_settings);
|
||||
|
||||
caps.DefaultPageSize.Should().Be(25);
|
||||
caps.MaxPageSize.Should().Be(60);
|
||||
caps.LimitsDefault.Value.Should().Be(25);
|
||||
caps.LimitsMax.Value.Should().Be(60);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -64,8 +63,8 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
|
||||
var caps = Subject.GetCapabilities(_settings);
|
||||
|
||||
caps.DefaultPageSize.Should().Be(100);
|
||||
caps.MaxPageSize.Should().Be(100);
|
||||
caps.LimitsDefault.Value.Should().Be(100);
|
||||
caps.LimitsMax.Value.Should().Be(100);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
@@ -13,7 +13,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
[TestFixture]
|
||||
public class NewznabFixture : CoreTest<Newznab>
|
||||
{
|
||||
private NewznabCapabilities _caps;
|
||||
private IndexerCapabilities _caps;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
}
|
||||
};
|
||||
|
||||
_caps = new NewznabCapabilities();
|
||||
_caps = new IndexerCapabilities();
|
||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>()))
|
||||
.Returns(_caps);
|
||||
@@ -64,8 +64,8 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
[Test]
|
||||
public void should_use_pagesize_reported_by_caps()
|
||||
{
|
||||
_caps.MaxPageSize = 30;
|
||||
_caps.DefaultPageSize = 25;
|
||||
_caps.LimitsMax = 30;
|
||||
_caps.LimitsDefault = 25;
|
||||
|
||||
Subject.PageSize.Should().Be(25);
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Newznab;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
@@ -12,7 +13,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
public class NewznabRequestGeneratorFixture : CoreTest<NewznabRequestGenerator>
|
||||
{
|
||||
private MovieSearchCriteria _movieSearchCriteria;
|
||||
private NewznabCapabilities _capabilities;
|
||||
private IndexerCapabilities _capabilities;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
@@ -29,7 +30,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
SceneTitles = new List<string> { "Star Wars" }
|
||||
};
|
||||
|
||||
_capabilities = new NewznabCapabilities();
|
||||
_capabilities = new IndexerCapabilities();
|
||||
|
||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>()))
|
||||
@@ -91,7 +92,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
[Test]
|
||||
public void should_not_search_by_imdbid_if_not_supported()
|
||||
{
|
||||
_capabilities.SupportedMovieSearchParameters = new[] { "q" };
|
||||
_capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q };
|
||||
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
|
||||
@@ -106,7 +107,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
[Test]
|
||||
public void should_search_by_imdbid_if_supported()
|
||||
{
|
||||
_capabilities.SupportedMovieSearchParameters = new[] { "q", "imdbid" };
|
||||
_capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.ImdbId };
|
||||
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
results.GetTier(0).Should().HaveCount(1);
|
||||
@@ -119,7 +120,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
[Test]
|
||||
public void should_search_by_tmdbid_if_supported()
|
||||
{
|
||||
_capabilities.SupportedMovieSearchParameters = new[] { "q", "tmdbid" };
|
||||
_capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.TmdbId };
|
||||
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
results.GetTier(0).Should().HaveCount(1);
|
||||
@@ -132,7 +133,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
[Test]
|
||||
public void should_prefer_search_by_tmdbid_if_rid_supported()
|
||||
{
|
||||
_capabilities.SupportedMovieSearchParameters = new[] { "q", "tmdbid", "imdbid" };
|
||||
_capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId };
|
||||
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
results.GetTier(0).Should().HaveCount(1);
|
||||
@@ -146,8 +147,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
[Test]
|
||||
public void should_use_aggregrated_id_search_if_supported()
|
||||
{
|
||||
_capabilities.SupportedMovieSearchParameters = new[] { "q", "tmdbid", "imdbid" };
|
||||
_capabilities.SupportsAggregateIdSearch = true;
|
||||
_capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId };
|
||||
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
results.GetTier(0).Should().HaveCount(1);
|
||||
@@ -161,8 +161,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
[Test]
|
||||
public void should_not_use_aggregrated_id_search_if_no_ids_supported()
|
||||
{
|
||||
_capabilities.SupportedMovieSearchParameters = new[] { "q" };
|
||||
_capabilities.SupportsAggregateIdSearch = true; // Turns true if indexer supplies supportedParams.
|
||||
_capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q };
|
||||
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
results.Tiers.Should().Be(1);
|
||||
@@ -176,8 +175,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
[Test]
|
||||
public void should_not_use_aggregrated_id_search_if_no_ids_are_known()
|
||||
{
|
||||
_capabilities.SupportedMovieSearchParameters = new[] { "q", "imdbid" };
|
||||
_capabilities.SupportsAggregateIdSearch = true; // Turns true if indexer supplies supportedParams.
|
||||
_capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.ImdbId };
|
||||
|
||||
_movieSearchCriteria.ImdbId = null;
|
||||
|
||||
@@ -191,8 +189,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
[Test]
|
||||
public void should_fallback_to_q()
|
||||
{
|
||||
_capabilities.SupportedMovieSearchParameters = new[] { "q", "tmdbid", "imdbid" };
|
||||
_capabilities.SupportsAggregateIdSearch = true;
|
||||
_capabilities.MovieSearchParams = new List<MovieSearchParam> { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId };
|
||||
|
||||
var results = Subject.GetSearchRequests(_movieSearchCriteria);
|
||||
results.Tiers.Should().Be(2);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
using FluentAssertions;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Indexers.Newznab;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
||||
[TestFixture]
|
||||
public class TorznabFixture : CoreTest<Torznab>
|
||||
{
|
||||
private NewznabCapabilities _caps;
|
||||
private IndexerCapabilities _caps;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
||||
}
|
||||
};
|
||||
|
||||
_caps = new NewznabCapabilities();
|
||||
_caps = new IndexerCapabilities();
|
||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>()))
|
||||
.Returns(_caps);
|
||||
@@ -129,8 +129,8 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
||||
[Test]
|
||||
public void should_use_pagesize_reported_by_caps()
|
||||
{
|
||||
_caps.MaxPageSize = 30;
|
||||
_caps.DefaultPageSize = 25;
|
||||
_caps.LimitsMax = 30;
|
||||
_caps.LimitsDefault = 25;
|
||||
|
||||
Subject.PageSize.Should().Be(25);
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@
|
||||
<PackageReference Include="Dapper" Version="2.0.30" />
|
||||
<PackageReference Include="NBuilder" Version="6.1.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
|
||||
<PackageReference Include="YamlDotNet" Version="8.1.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
|
||||
<PackageReference Include="coverlet.collector" Version="1.2.1" PrivateAssets="all" />
|
||||
|
@@ -1,4 +1,4 @@
|
||||
using FizzWare.NBuilder;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
@@ -7,7 +7,6 @@ using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.CustomFilters;
|
||||
using NzbDrone.Core.Datastore.Converters;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Instrumentation;
|
||||
using NzbDrone.Core.Jobs;
|
||||
@@ -82,7 +81,6 @@ namespace NzbDrone.Core.Datastore
|
||||
SqlMapper.AddTypeHandler(new DapperLanguageIntConverter());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<Language>>(new LanguageIntConverter()));
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<string>>());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<ParsedMovieInfo>(new LanguageIntConverter()));
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<ReleaseInfo>());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<HashSet<int>>());
|
||||
SqlMapper.AddTypeHandler(new OsPathConverter());
|
||||
|
@@ -13,11 +13,11 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
public List<string> SceneTitles { get; set; }
|
||||
public virtual bool MonitoredEpisodesOnly { get; set; }
|
||||
public virtual bool UserInvokedSearch { get; set; }
|
||||
public virtual bool InteractiveSearch { get; set; }
|
||||
public string ImdbId { get; set; }
|
||||
public int TmdbId { get; set; }
|
||||
public List<int> IndexerIds { get; set; }
|
||||
|
||||
public List<string> QueryTitles => SceneTitles.Select(GetQueryTitle).ToList();
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
28
src/NzbDrone.Core/IndexerSearch/NewznabRequest.cs
Normal file
28
src/NzbDrone.Core/IndexerSearch/NewznabRequest.cs
Normal 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; }
|
||||
}
|
||||
}
|
91
src/NzbDrone.Core/IndexerSearch/NewznabResults.cs
Normal file
91
src/NzbDrone.Core/IndexerSearch/NewznabResults.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -13,7 +14,8 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
{
|
||||
public interface ISearchForNzb
|
||||
{
|
||||
List<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
|
||||
@@ -28,14 +30,21 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
_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);
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
var spec = new TSpec()
|
||||
@@ -44,7 +53,8 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
InteractiveSearch = interactiveSearch
|
||||
};
|
||||
|
||||
spec.SceneTitles = new List<string> { movie };
|
||||
spec.SceneTitles = new List<string> { query };
|
||||
spec.IndexerIds = indexerIds;
|
||||
|
||||
return spec;
|
||||
}
|
||||
@@ -55,6 +65,11 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
_indexerFactory.InteractiveSearchEnabled() :
|
||||
_indexerFactory.AutomaticSearchEnabled();
|
||||
|
||||
if (criteriaBase.IndexerIds != null && criteriaBase.IndexerIds.Count > 0)
|
||||
{
|
||||
indexers = indexers.Where(i => criteriaBase.IndexerIds.Contains(i.Definition.Id)).ToList();
|
||||
}
|
||||
|
||||
var reports = new List<ReleaseInfo>();
|
||||
|
||||
_logger.ProgressInfo("Searching {0} indexers for {1}", indexers.Count, criteriaBase.QueryTitles.Join(", "));
|
||||
|
@@ -20,7 +20,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
|
||||
public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings).DefaultPageSize;
|
||||
public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings).LimitsDefault.Value;
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
@@ -36,6 +36,14 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
return new NewznabRssParser(Settings);
|
||||
}
|
||||
|
||||
public override IndexerCapabilities GetCapabilities()
|
||||
{
|
||||
// TODO: This uses indexer capabilities when called so we don't have to keep up with all of them
|
||||
// however, this is not pulled on a all pull from UI, doing so will kill the UI load if an indexer is down
|
||||
// should we just purge and manage
|
||||
return _capabilitiesProvider.GetCapabilities(Settings);
|
||||
}
|
||||
|
||||
public override IEnumerable<ProviderDefinition> DefaultDefinitions
|
||||
{
|
||||
get
|
||||
@@ -75,7 +83,8 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
Protocol = DownloadProtocol.Usenet,
|
||||
Privacy = IndexerPrivacy.Private,
|
||||
SupportsRss = SupportsRss,
|
||||
SupportsSearch = SupportsSearch
|
||||
SupportsSearch = SupportsSearch,
|
||||
Capabilities = Capabilities
|
||||
};
|
||||
}
|
||||
|
||||
@@ -107,15 +116,15 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
failures.AddIfNotNull(TestCapabilities());
|
||||
}
|
||||
|
||||
protected static List<int> CategoryIds(List<NewznabCategory> categories)
|
||||
protected static List<int> CategoryIds(List<IndexerCategory> categories)
|
||||
{
|
||||
var l = categories.Select(c => c.Id).ToList();
|
||||
|
||||
foreach (var category in categories)
|
||||
{
|
||||
if (category.Subcategories != null)
|
||||
if (category.SubCategories != null)
|
||||
{
|
||||
l.AddRange(CategoryIds(category.Subcategories));
|
||||
l.AddRange(CategoryIds(category.SubCategories));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,14 +148,14 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
}
|
||||
}
|
||||
|
||||
if (capabilities.SupportedSearchParameters != null && capabilities.SupportedSearchParameters.Contains("q"))
|
||||
if (capabilities.SearchParams != null && capabilities.SearchParams.Contains(SearchParam.Q))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (capabilities.SupportedMovieSearchParameters != null &&
|
||||
new[] { "q", "imdbid" }.Any(v => capabilities.SupportedMovieSearchParameters.Contains(v)) &&
|
||||
new[] { "imdbtitle", "imdbyear" }.All(v => capabilities.SupportedMovieSearchParameters.Contains(v)))
|
||||
if (capabilities.MovieSearchParams != null &&
|
||||
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId }.Any(v => capabilities.MovieSearchParams.Contains(v)) &&
|
||||
new[] { MovieSearchParam.ImdbTitle, MovieSearchParam.ImdbYear }.All(v => capabilities.MovieSearchParams.Contains(v)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -12,23 +12,23 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
public interface INewznabCapabilitiesProvider
|
||||
{
|
||||
NewznabCapabilities GetCapabilities(NewznabSettings settings);
|
||||
IndexerCapabilities GetCapabilities(NewznabSettings settings);
|
||||
}
|
||||
|
||||
public class NewznabCapabilitiesProvider : INewznabCapabilitiesProvider
|
||||
{
|
||||
private readonly ICached<NewznabCapabilities> _capabilitiesCache;
|
||||
private readonly ICached<IndexerCapabilities> _capabilitiesCache;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public NewznabCapabilitiesProvider(ICacheManager cacheManager, IHttpClient httpClient, Logger logger)
|
||||
{
|
||||
_capabilitiesCache = cacheManager.GetCache<NewznabCapabilities>(GetType());
|
||||
_capabilitiesCache = cacheManager.GetCache<IndexerCapabilities>(GetType());
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public NewznabCapabilities GetCapabilities(NewznabSettings indexerSettings)
|
||||
public IndexerCapabilities GetCapabilities(NewznabSettings indexerSettings)
|
||||
{
|
||||
var key = indexerSettings.ToJson();
|
||||
var capabilities = _capabilitiesCache.Get(key, () => FetchCapabilities(indexerSettings), TimeSpan.FromDays(7));
|
||||
@@ -36,9 +36,9 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
private NewznabCapabilities FetchCapabilities(NewznabSettings indexerSettings)
|
||||
private IndexerCapabilities FetchCapabilities(NewznabSettings indexerSettings)
|
||||
{
|
||||
var capabilities = new NewznabCapabilities();
|
||||
var capabilities = new IndexerCapabilities();
|
||||
|
||||
var url = string.Format("{0}{1}?t=caps", indexerSettings.BaseUrl.TrimEnd('/'), indexerSettings.ApiPath.TrimEnd('/'));
|
||||
|
||||
@@ -81,9 +81,9 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
private NewznabCapabilities ParseCapabilities(HttpResponse response)
|
||||
private IndexerCapabilities ParseCapabilities(HttpResponse response)
|
||||
{
|
||||
var capabilities = new NewznabCapabilities();
|
||||
var capabilities = new IndexerCapabilities();
|
||||
|
||||
var xDoc = XDocument.Parse(response.Content);
|
||||
|
||||
@@ -104,8 +104,8 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
var xmlLimits = xmlRoot.Element("limits");
|
||||
if (xmlLimits != null)
|
||||
{
|
||||
capabilities.DefaultPageSize = int.Parse(xmlLimits.Attribute("default").Value);
|
||||
capabilities.MaxPageSize = int.Parse(xmlLimits.Attribute("max").Value);
|
||||
capabilities.LimitsDefault = int.Parse(xmlLimits.Attribute("default").Value);
|
||||
capabilities.LimitsMax = int.Parse(xmlLimits.Attribute("max").Value);
|
||||
}
|
||||
|
||||
var xmlSearching = xmlRoot.Element("searching");
|
||||
@@ -114,22 +114,81 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
var xmlBasicSearch = xmlSearching.Element("search");
|
||||
if (xmlBasicSearch == null || xmlBasicSearch.Attribute("available").Value != "yes")
|
||||
{
|
||||
capabilities.SupportedSearchParameters = null;
|
||||
capabilities.SearchParams = new List<SearchParam>();
|
||||
}
|
||||
else if (xmlBasicSearch.Attribute("supportedParams") != null)
|
||||
{
|
||||
capabilities.SupportedSearchParameters = xmlBasicSearch.Attribute("supportedParams").Value.Split(',');
|
||||
foreach (var param in xmlBasicSearch.Attribute("supportedParams").Value.Split(','))
|
||||
{
|
||||
if (Enum.TryParse(param, true, out SearchParam searchParam))
|
||||
{
|
||||
capabilities.SearchParams.AddIfNotNull(searchParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var xmlMovieSearch = xmlSearching.Element("movie-search");
|
||||
if (xmlMovieSearch == null || xmlMovieSearch.Attribute("available").Value != "yes")
|
||||
{
|
||||
capabilities.SupportedMovieSearchParameters = null;
|
||||
capabilities.MovieSearchParams = new List<MovieSearchParam>();
|
||||
}
|
||||
else if (xmlMovieSearch.Attribute("supportedParams") != null)
|
||||
{
|
||||
capabilities.SupportedMovieSearchParameters = xmlMovieSearch.Attribute("supportedParams").Value.Split(',');
|
||||
capabilities.SupportsAggregateIdSearch = true;
|
||||
foreach (var param in xmlMovieSearch.Attribute("supportedParams").Value.Split(','))
|
||||
{
|
||||
if (Enum.TryParse(param, true, out MovieSearchParam searchParam))
|
||||
{
|
||||
capabilities.MovieSearchParams.AddIfNotNull(searchParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var xmlTvSearch = xmlSearching.Element("tv-search");
|
||||
if (xmlTvSearch == null || xmlTvSearch.Attribute("available").Value != "yes")
|
||||
{
|
||||
capabilities.TvSearchParams = new List<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"))
|
||||
{
|
||||
var cat = new NewznabCategory
|
||||
var cat = new IndexerCategory
|
||||
{
|
||||
Id = int.Parse(xmlCategory.Attribute("id").Value),
|
||||
Name = xmlCategory.Attribute("name").Value,
|
||||
Description = xmlCategory.Attribute("description") != null ? xmlCategory.Attribute("description").Value : string.Empty,
|
||||
Subcategories = new List<NewznabCategory>()
|
||||
Description = xmlCategory.Attribute("description") != null ? xmlCategory.Attribute("description").Value : string.Empty
|
||||
};
|
||||
|
||||
foreach (var xmlSubcat in xmlCategory.Elements("subcat"))
|
||||
{
|
||||
cat.Subcategories.Add(new NewznabCategory
|
||||
cat.SubCategories.Add(new IndexerCategory
|
||||
{
|
||||
Id = int.Parse(xmlSubcat.Attribute("id").Value),
|
||||
Name = xmlSubcat.Attribute("name").Value,
|
||||
|
@@ -28,8 +28,8 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
|
||||
|
||||
return capabilities.SupportedSearchParameters != null &&
|
||||
capabilities.SupportedSearchParameters.Contains("q");
|
||||
return capabilities.SearchParams != null &&
|
||||
capabilities.SearchParams.Contains(SearchParam.Q);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
|
||||
|
||||
return capabilities.SupportedMovieSearchParameters != null &&
|
||||
capabilities.SupportedMovieSearchParameters.Contains("imdbid");
|
||||
return capabilities.MovieSearchParams != null &&
|
||||
capabilities.MovieSearchParams.Contains(MovieSearchParam.ImdbId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
|
||||
|
||||
return capabilities.SupportedMovieSearchParameters != null &&
|
||||
capabilities.SupportedMovieSearchParameters.Contains("tmdbid");
|
||||
return capabilities.MovieSearchParams != null &&
|
||||
capabilities.MovieSearchParams.Contains(MovieSearchParam.TmdbId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,8 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
|
||||
|
||||
return capabilities.SupportsAggregateIdSearch;
|
||||
// TODO: Fix this, return capabilities.SupportsAggregateIdSearch;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +73,11 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
|
||||
|
||||
// Some indexers might forget to enable movie search, but normal search still works fine. Thus we force a normal search.
|
||||
if (capabilities.SupportedMovieSearchParameters != null)
|
||||
if (capabilities.MovieSearchParams != null)
|
||||
{
|
||||
pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "movie", ""));
|
||||
}
|
||||
else if (capabilities.SupportedSearchParameters != null)
|
||||
else if (capabilities.SearchParams != null)
|
||||
{
|
||||
pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", ""));
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -148,19 +147,6 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected virtual string GetImdbTitle(XElement item)
|
||||
{
|
||||
var imdbTitle = TryGetNewznabAttribute(item, "imdbtitle");
|
||||
if (!imdbTitle.IsNullOrWhiteSpace())
|
||||
{
|
||||
return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(
|
||||
Parser.Parser.ReplaceGermanUmlauts(
|
||||
Parser.Parser.NormalizeTitle(imdbTitle).Replace(" ", ".")));
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
protected virtual int GetImdbYear(XElement item)
|
||||
{
|
||||
var imdbYearString = TryGetNewznabAttribute(item, "imdbyear");
|
||||
|
@@ -20,7 +20,8 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings).DefaultPageSize;
|
||||
|
||||
public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings).LimitsDefault.Value;
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
@@ -63,7 +64,8 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
Settings = settings,
|
||||
Protocol = DownloadProtocol.Usenet,
|
||||
SupportsRss = SupportsRss,
|
||||
SupportsSearch = SupportsSearch
|
||||
SupportsSearch = SupportsSearch,
|
||||
Capabilities = new IndexerCapabilities()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,15 +97,15 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
failures.AddIfNotNull(TestCapabilities());
|
||||
}
|
||||
|
||||
protected static List<int> CategoryIds(List<NewznabCategory> categories)
|
||||
protected static List<int> CategoryIds(List<IndexerCategory> categories)
|
||||
{
|
||||
var l = categories.Select(c => c.Id).ToList();
|
||||
|
||||
foreach (var category in categories)
|
||||
{
|
||||
if (category.Subcategories != null)
|
||||
if (category.SubCategories != null)
|
||||
{
|
||||
l.AddRange(CategoryIds(category.Subcategories));
|
||||
l.AddRange(CategoryIds(category.SubCategories));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,14 +129,14 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
}
|
||||
}
|
||||
|
||||
if (capabilities.SupportedSearchParameters != null && capabilities.SupportedSearchParameters.Contains("q"))
|
||||
if (capabilities.SearchParams != null && capabilities.SearchParams.Contains(SearchParam.Q))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (capabilities.SupportedMovieSearchParameters != null &&
|
||||
new[] { "q", "imdbid" }.Any(v => capabilities.SupportedMovieSearchParameters.Contains(v)) &&
|
||||
new[] { "imdbtitle", "imdbyear" }.All(v => capabilities.SupportedMovieSearchParameters.Contains(v)))
|
||||
if (capabilities.MovieSearchParams != null &&
|
||||
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId }.Any(v => capabilities.MovieSearchParams.Contains(v)) &&
|
||||
new[] { MovieSearchParam.ImdbTitle, MovieSearchParam.ImdbYear }.All(v => capabilities.MovieSearchParams.Contains(v)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
@@ -1,11 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Indexers.Newznab;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Torznab
|
||||
@@ -47,22 +44,14 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
}
|
||||
}
|
||||
|
||||
public class TorznabSettings : NewznabSettings, ITorrentIndexerSettings
|
||||
public class TorznabSettings : NewznabSettings
|
||||
{
|
||||
private static readonly TorznabSettingsValidator Validator = new TorznabSettingsValidator();
|
||||
|
||||
public TorznabSettings()
|
||||
{
|
||||
MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS;
|
||||
RequiredFlags = new List<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()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
@@ -86,6 +86,11 @@ namespace NzbDrone.Core.Indexers
|
||||
return requests;
|
||||
}
|
||||
|
||||
public override IndexerCapabilities GetCapabilities()
|
||||
{
|
||||
return Capabilities;
|
||||
}
|
||||
|
||||
protected virtual IList<ReleaseInfo> FetchReleases(Func<IIndexerRequestGenerator, IndexerPageableRequestChain> pageableRequestChainSelector, bool isRecent = false)
|
||||
{
|
||||
var releases = new List<ReleaseInfo>();
|
||||
|
@@ -16,5 +16,7 @@ namespace NzbDrone.Core.Indexers
|
||||
|
||||
IList<ReleaseInfo> FetchRecent();
|
||||
IList<ReleaseInfo> Fetch(MovieSearchCriteria searchCriteria);
|
||||
|
||||
IndexerCapabilities GetCapabilities();
|
||||
}
|
||||
}
|
||||
|
@@ -68,6 +68,8 @@ namespace NzbDrone.Core.Indexers
|
||||
public abstract IList<ReleaseInfo> FetchRecent();
|
||||
public abstract IList<ReleaseInfo> Fetch(MovieSearchCriteria searchCriteria);
|
||||
|
||||
public abstract IndexerCapabilities GetCapabilities();
|
||||
|
||||
protected virtual IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases)
|
||||
{
|
||||
var result = releases.DistinctBy(v => v.Guid).ToList();
|
||||
|
@@ -13,13 +13,16 @@ namespace NzbDrone.Core.Indexers
|
||||
ImdbId,
|
||||
TvdbId,
|
||||
RId,
|
||||
TvMazeId
|
||||
}
|
||||
|
||||
public enum MovieSearchParam
|
||||
{
|
||||
Q,
|
||||
ImdbId,
|
||||
TmdbId
|
||||
TmdbId,
|
||||
ImdbTitle,
|
||||
ImdbYear
|
||||
}
|
||||
|
||||
public enum MusicSearchParam
|
||||
@@ -31,12 +34,23 @@ namespace NzbDrone.Core.Indexers
|
||||
Year
|
||||
}
|
||||
|
||||
public enum SearchParam
|
||||
{
|
||||
Q
|
||||
}
|
||||
|
||||
public enum BookSearchParam
|
||||
{
|
||||
Q
|
||||
}
|
||||
|
||||
public class IndexerCapabilities
|
||||
{
|
||||
public int? LimitsMax { get; set; }
|
||||
public int? LimitsDefault { get; set; }
|
||||
|
||||
public bool SearchAvailable { get; set; }
|
||||
public List<SearchParam> SearchParams;
|
||||
public bool SearchAvailable => SearchParams.Count > 0;
|
||||
|
||||
public List<TvSearchParam> TvSearchParams;
|
||||
public bool TvSearchAvailable => TvSearchParams.Count > 0;
|
||||
@@ -45,6 +59,7 @@ namespace NzbDrone.Core.Indexers
|
||||
public bool TvSearchImdbAvailable => TvSearchParams.Contains(TvSearchParam.ImdbId);
|
||||
public bool TvSearchTvdbAvailable => TvSearchParams.Contains(TvSearchParam.TvdbId);
|
||||
public bool TvSearchTvRageAvailable => TvSearchParams.Contains(TvSearchParam.RId);
|
||||
public bool TvSearchTvMazeAvailable => TvSearchParams.Contains(TvSearchParam.TvMazeId);
|
||||
|
||||
public List<MovieSearchParam> MovieSearchParams;
|
||||
public bool MovieSearchAvailable => MovieSearchParams.Count > 0;
|
||||
@@ -58,17 +73,18 @@ namespace NzbDrone.Core.Indexers
|
||||
public bool MusicSearchLabelAvailable => MusicSearchParams.Contains(MusicSearchParam.Label);
|
||||
public bool MusicSearchYearAvailable => MusicSearchParams.Contains(MusicSearchParam.Year);
|
||||
|
||||
public bool BookSearchAvailable { get; set; }
|
||||
public List<BookSearchParam> BookSearchParams;
|
||||
public bool BookSearchAvailable => BookSearchParams.Count > 0;
|
||||
|
||||
public List<IndexerCategory> Categories { get; private set; }
|
||||
|
||||
public IndexerCapabilities()
|
||||
{
|
||||
SearchAvailable = true;
|
||||
SearchParams = new List<SearchParam>();
|
||||
TvSearchParams = new List<TvSearchParam>();
|
||||
MovieSearchParams = new List<MovieSearchParam>();
|
||||
MusicSearchParams = new List<MusicSearchParam>();
|
||||
BookSearchAvailable = false;
|
||||
BookSearchParams = new List<BookSearchParam>();
|
||||
Categories = new List<IndexerCategory>();
|
||||
}
|
||||
|
||||
@@ -181,6 +197,11 @@ namespace NzbDrone.Core.Indexers
|
||||
parameters.Add("rid");
|
||||
}
|
||||
|
||||
if (TvSearchTvMazeAvailable)
|
||||
{
|
||||
parameters.Add("tvmazeid");
|
||||
}
|
||||
|
||||
return string.Join(",", parameters);
|
||||
}
|
||||
|
||||
@@ -244,7 +265,7 @@ namespace NzbDrone.Core.Indexers
|
||||
{
|
||||
var subCategories = Categories.SelectMany(c => c.SubCategories);
|
||||
var allCategories = Categories.Concat(subCategories);
|
||||
var supportsCategory = allCategories.Any(i => categories.Any(c => c == i.ID));
|
||||
var supportsCategory = allCategories.Any(i => categories.Any(c => c == i.Id));
|
||||
return supportsCategory;
|
||||
}
|
||||
|
||||
@@ -280,13 +301,13 @@ namespace NzbDrone.Core.Indexers
|
||||
new XAttribute("available", BookSearchAvailable ? "yes" : "no"),
|
||||
new XAttribute("supportedParams", SupportedBookSearchParams))),
|
||||
new XElement("categories",
|
||||
from c in Categories.OrderBy(x => x.ID < 100000 ? "z" + x.ID.ToString() : x.Name)
|
||||
from c in Categories.OrderBy(x => x.Id < 100000 ? "z" + x.Id.ToString() : x.Name)
|
||||
select new XElement("category",
|
||||
new XAttribute("id", c.ID),
|
||||
new XAttribute("id", c.Id),
|
||||
new XAttribute("name", c.Name),
|
||||
from sc in c.SubCategories
|
||||
select new XElement("subcat",
|
||||
new XAttribute("id", sc.ID),
|
||||
new XAttribute("id", sc.Id),
|
||||
new XAttribute("name", sc.Name))))));
|
||||
return xdoc;
|
||||
}
|
||||
@@ -296,12 +317,12 @@ namespace NzbDrone.Core.Indexers
|
||||
|
||||
public static IndexerCapabilities Concat(IndexerCapabilities left, IndexerCapabilities right)
|
||||
{
|
||||
left.SearchAvailable = left.SearchAvailable || right.SearchAvailable;
|
||||
left.SearchParams = left.SearchParams.Union(right.SearchParams).ToList();
|
||||
left.TvSearchParams = left.TvSearchParams.Union(right.TvSearchParams).ToList();
|
||||
left.MovieSearchParams = left.MovieSearchParams.Union(right.MovieSearchParams).ToList();
|
||||
left.MusicSearchParams = left.MusicSearchParams.Union(right.MusicSearchParams).ToList();
|
||||
left.BookSearchAvailable = left.BookSearchAvailable || right.BookSearchAvailable;
|
||||
left.Categories.AddRange(right.Categories.Where(x => x.ID < 100000).Except(left.Categories)); // exclude indexer specific categories (>= 100000)
|
||||
left.BookSearchParams = left.BookSearchParams.Union(right.BookSearchParams).ToList();
|
||||
left.Categories.AddRange(right.Categories.Where(x => x.Id < 100000).Except(left.Categories)); // exclude indexer specific categories (>= 100000)
|
||||
return left;
|
||||
}
|
||||
}
|
||||
|
@@ -5,8 +5,9 @@ namespace NzbDrone.Core.Indexers
|
||||
{
|
||||
public class IndexerCategory
|
||||
{
|
||||
public int ID { get; set; }
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
|
||||
public List<IndexerCategory> SubCategories { get; private set; }
|
||||
|
||||
@@ -14,7 +15,7 @@ namespace NzbDrone.Core.Indexers
|
||||
|
||||
public IndexerCategory(int id, string name)
|
||||
{
|
||||
ID = id;
|
||||
Id = id;
|
||||
Name = name;
|
||||
SubCategories = new List<IndexerCategory>();
|
||||
}
|
||||
@@ -25,14 +26,14 @@ namespace NzbDrone.Core.Indexers
|
||||
public JToken ToJson() =>
|
||||
new JObject
|
||||
{
|
||||
["ID"] = ID,
|
||||
["ID"] = Id,
|
||||
["Name"] = Name
|
||||
};
|
||||
|
||||
public override bool Equals(object obj) => (obj as IndexerCategory)?.ID == ID;
|
||||
public override bool Equals(object obj) => (obj as IndexerCategory)?.Id == Id;
|
||||
|
||||
// Get Hash code should be calculated off read only properties.
|
||||
// ID is not readonly
|
||||
public override int GetHashCode() => ID;
|
||||
public override int GetHashCode() => Id;
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Text;
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace NzbDrone.Core.Parser.Model
|
||||
public int TvdbId { get; set; }
|
||||
public int TvRageId { get; set; }
|
||||
public int ImdbId { get; set; }
|
||||
public int TmdbId { get; set; }
|
||||
public DateTime PublishDate { get; set; }
|
||||
|
||||
public string Origin { get; set; }
|
||||
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using Nancy;
|
||||
using Nancy.ModelBinding;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.IndexerSearch;
|
||||
using Prowlarr.Http.REST;
|
||||
|
||||
namespace Prowlarr.Api.V1.Indexers
|
||||
@@ -9,13 +13,19 @@ namespace Prowlarr.Api.V1.Indexers
|
||||
public static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper();
|
||||
|
||||
private IIndexerFactory _indexerFactory { get; set; }
|
||||
private ISearchForNzb _nzbSearchService { get; set; }
|
||||
|
||||
public IndexerModule(IndexerFactory indexerFactory)
|
||||
public IndexerModule(IndexerFactory indexerFactory, ISearchForNzb nzbSearchService)
|
||||
: base(indexerFactory, "indexer", ResourceMapper)
|
||||
{
|
||||
_indexerFactory = indexerFactory;
|
||||
_nzbSearchService = nzbSearchService;
|
||||
|
||||
Get("{id}/newznab", x => GetNewznabResponse(x.id));
|
||||
Get("{id}/newznab", x =>
|
||||
{
|
||||
var request = this.Bind<NewznabRequest>();
|
||||
return GetNewznabResponse(request);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Validate(IndexerDefinition definition, bool includeWarnings)
|
||||
@@ -28,24 +38,35 @@ namespace Prowlarr.Api.V1.Indexers
|
||||
base.Validate(definition, includeWarnings);
|
||||
}
|
||||
|
||||
private object GetNewznabResponse(int id)
|
||||
private object GetNewznabResponse(NewznabRequest request)
|
||||
{
|
||||
var requestType = Request.Query.t;
|
||||
var requestType = request.t;
|
||||
|
||||
if (!requestType.HasValue)
|
||||
if (requestType.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new BadRequestException("Missing Function Parameter");
|
||||
}
|
||||
|
||||
if (requestType.Value == "caps")
|
||||
var indexer = _indexerFactory.Get(request.id);
|
||||
|
||||
if (indexer == null)
|
||||
{
|
||||
var indexer = _indexerFactory.GetInstance(_indexerFactory.Get(id));
|
||||
Response response = indexer.Capabilities.ToXml();
|
||||
throw new NotFoundException("Indexer Not Found");
|
||||
}
|
||||
|
||||
var indexerInstance = _indexerFactory.GetInstance(indexer);
|
||||
|
||||
switch (requestType)
|
||||
{
|
||||
case "caps":
|
||||
Response response = indexerInstance.GetCapabilities().ToXml();
|
||||
response.ContentType = "application/rss+xml";
|
||||
return response;
|
||||
}
|
||||
else
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
@@ -10,10 +10,6 @@ namespace Prowlarr.Api.V1.Indexers
|
||||
public bool EnableInteractiveSearch { get; set; }
|
||||
public bool SupportsRss { get; set; }
|
||||
public bool SupportsSearch { get; set; }
|
||||
public bool SupportsMusic { get; set; }
|
||||
public bool SupportsTv { get; set; }
|
||||
public bool SupportsMovies { get; set; }
|
||||
public bool SupportsBooks { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
public IndexerPrivacy Privacy { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
|
@@ -26,17 +26,26 @@ namespace Prowlarr.Api.V1.Search
|
||||
{
|
||||
if (Request.Query.query.HasValue)
|
||||
{
|
||||
return GetSearchReleases(Request.Query.query);
|
||||
var indexerIds = Request.Query.indexerIds.HasValue ? (List<int>)Request.Query.indexerIds.split(',') : new List<int>();
|
||||
|
||||
if (indexerIds.Count > 0)
|
||||
{
|
||||
return GetSearchReleases(Request.Query.query, indexerIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
return GetSearchReleases(Request.Query.query, null);
|
||||
}
|
||||
}
|
||||
|
||||
return new List<SearchResource>();
|
||||
}
|
||||
|
||||
private List<SearchResource> GetSearchReleases(string query)
|
||||
private List<SearchResource> GetSearchReleases(string query, List<int> indexerIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var decisions = _nzbSearhService.MovieSearch(query, true, true);
|
||||
var decisions = _nzbSearhService.Search(query, indexerIds, true, true);
|
||||
|
||||
return MapDecisions(decisions);
|
||||
}
|
||||
|
Reference in New Issue
Block a user