Compare commits
4 Commits
develop
...
phantom-ad
Author | SHA1 | Date |
---|---|---|
Taloth Saldono | 808836a65f | |
Taloth Saldono | 6f7e505bed | |
Jacob | ecf1d75954 | |
netpok | c5257de436 |
|
@ -14,6 +14,7 @@ import PasswordInput from './PasswordInput';
|
|||
import PathInputConnector from './PathInputConnector';
|
||||
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
|
||||
import LanguageProfileSelectInputConnector from './LanguageProfileSelectInputConnector';
|
||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
||||
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
@ -61,6 +62,9 @@ function getComponent(type) {
|
|||
case inputTypes.LANGUAGE_PROFILE_SELECT:
|
||||
return LanguageProfileSelectInputConnector;
|
||||
|
||||
case inputTypes.INDEXER_SELECT:
|
||||
return IndexerSelectInputConnector;
|
||||
|
||||
case inputTypes.ROOT_FOLDER_SELECT:
|
||||
return RootFolderSelectInputConnector;
|
||||
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.indexers,
|
||||
(state, { includeAny }) => includeAny,
|
||||
(indexers, includeAny) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = indexers;
|
||||
|
||||
const values = _.map(items.sort(sortByName), (indexer) => {
|
||||
return {
|
||||
key: indexer.id,
|
||||
value: indexer.name
|
||||
};
|
||||
});
|
||||
|
||||
if (includeAny) {
|
||||
values.unshift({
|
||||
key: 0,
|
||||
value: '(Any)'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
values
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchIndexers: fetchIndexers
|
||||
};
|
||||
|
||||
class IndexerSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.dispatchFetchIndexers();
|
||||
}
|
||||
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
values
|
||||
} = this.props;
|
||||
|
||||
if (!value || !_.some(values, (option) => parseInt(option.key) === value)) {
|
||||
this.onChange({ name, value: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
this.props.onChange({ name, value: parseInt(value) });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...this.props}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndexerSelectInputConnector.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
includeAny: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexers: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
IndexerSelectInputConnector.defaultProps = {
|
||||
includeAny: false
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSelectInputConnector);
|
|
@ -10,6 +10,7 @@ export const PASSWORD = 'password';
|
|||
export const PATH = 'path';
|
||||
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
|
||||
export const LANGUAGE_PROFILE_SELECT = 'languageProfileSelect';
|
||||
export const INDEXER_SELECT = 'indexerSelect';
|
||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||
export const SELECT = 'select';
|
||||
export const SERIES_TYPE_SELECT = 'seriesTypeSelect';
|
||||
|
@ -30,6 +31,7 @@ export const all = [
|
|||
PATH,
|
||||
QUALITY_PROFILE_SELECT,
|
||||
LANGUAGE_PROFILE_SELECT,
|
||||
INDEXER_SELECT,
|
||||
ROOT_FOLDER_SELECT,
|
||||
SELECT,
|
||||
SERIES_TYPE_SELECT,
|
||||
|
|
|
@ -30,11 +30,13 @@ function EditReleaseProfileModalContent(props) {
|
|||
|
||||
const {
|
||||
id,
|
||||
enabled,
|
||||
required,
|
||||
ignored,
|
||||
preferred,
|
||||
includePreferredWhenRenaming,
|
||||
tags
|
||||
tags,
|
||||
indexerId
|
||||
} = item;
|
||||
|
||||
return (
|
||||
|
@ -45,6 +47,18 @@ function EditReleaseProfileModalContent(props) {
|
|||
|
||||
<ModalBody>
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>Enable Profile</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enabled"
|
||||
helpText="Check to enable release profile"
|
||||
{...enabled}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Must Contain</FormLabel>
|
||||
|
||||
|
@ -99,9 +113,23 @@ function EditReleaseProfileModalContent(props) {
|
|||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="includePreferredWhenRenaming"
|
||||
helpText="Include in {Preferred Words} renaming format"
|
||||
helpText={indexerId.value === 0 ? 'Include in {Preferred Words} renaming format' : 'Only supported when Indexer is set to (All)'}
|
||||
{...includePreferredWhenRenaming}
|
||||
onChange={onInputChange}
|
||||
isDisabled={indexerId.value !== 0}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Indexer</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.INDEXER_SELECT}
|
||||
name="indexerId"
|
||||
helpText="Specify what indexer the profile applies to"
|
||||
{...indexerId}
|
||||
includeAny={true}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
|
|
@ -8,11 +8,13 @@ import { setReleaseProfileValue, saveReleaseProfile } from 'Store/Actions/settin
|
|||
import EditReleaseProfileModalContent from './EditReleaseProfileModalContent';
|
||||
|
||||
const newReleaseProfile = {
|
||||
enabled: true,
|
||||
required: '',
|
||||
ignored: '',
|
||||
preferred: [],
|
||||
includePreferredWhenRenaming: false,
|
||||
tags: []
|
||||
tags: [],
|
||||
indexerId: 0
|
||||
};
|
||||
|
||||
function createMapStateToProps() {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import split from 'Utilities/String/split';
|
||||
|
@ -55,11 +56,14 @@ class ReleaseProfile extends Component {
|
|||
render() {
|
||||
const {
|
||||
id,
|
||||
enabled,
|
||||
required,
|
||||
ignored,
|
||||
preferred,
|
||||
tags,
|
||||
tagList
|
||||
indexerId,
|
||||
tagList,
|
||||
indexerList
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
|
@ -67,6 +71,8 @@ class ReleaseProfile extends Component {
|
|||
isDeleteReleaseProfileModalOpen
|
||||
} = this.state;
|
||||
|
||||
const indexer = indexerId !== 0 && _.find(indexerList, { id: indexerId });
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.releaseProfile}
|
||||
|
@ -92,6 +98,23 @@ class ReleaseProfile extends Component {
|
|||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
preferred.map((item) => {
|
||||
const isPreferred = item.value >= 0;
|
||||
|
||||
return (
|
||||
<Label
|
||||
key={item.key}
|
||||
kind={isPreferred ? kinds.DEFAULT : kinds.WARNING}
|
||||
>
|
||||
{item.key} {isPreferred && '+'}{item.value}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
split(ignored).map((item) => {
|
||||
|
@ -111,28 +134,33 @@ class ReleaseProfile extends Component {
|
|||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
preferred.map((item) => {
|
||||
const isPreferred = item.value >= 0;
|
||||
|
||||
return (
|
||||
<Label
|
||||
key={item.key}
|
||||
kind={isPreferred ? kinds.DEFAULT : kinds.WARNING}
|
||||
>
|
||||
{item.key} {isPreferred && '+'}{item.value}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<TagList
|
||||
tags={tags}
|
||||
tagList={tagList}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{
|
||||
!enabled &&
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
>
|
||||
Disabled
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
indexer &&
|
||||
<Label
|
||||
kind={kinds.INFO}
|
||||
outline={true}
|
||||
>
|
||||
{indexer.name}
|
||||
</Label>
|
||||
}
|
||||
</div>
|
||||
|
||||
<EditReleaseProfileModalConnector
|
||||
id={id}
|
||||
isOpen={isEditReleaseProfileModalOpen}
|
||||
|
@ -156,18 +184,23 @@ class ReleaseProfile extends Component {
|
|||
|
||||
ReleaseProfile.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
required: PropTypes.string.isRequired,
|
||||
ignored: PropTypes.string.isRequired,
|
||||
preferred: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
indexerId: PropTypes.number.isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteReleaseProfile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ReleaseProfile.defaultProps = {
|
||||
enabled: true,
|
||||
required: '',
|
||||
ignored: '',
|
||||
preferred: []
|
||||
preferred: [],
|
||||
indexerId: 0
|
||||
};
|
||||
|
||||
export default ReleaseProfile;
|
||||
|
|
|
@ -40,6 +40,7 @@ class ReleaseProfiles extends Component {
|
|||
const {
|
||||
items,
|
||||
tagList,
|
||||
indexerList,
|
||||
onConfirmDeleteReleaseProfile,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
@ -69,6 +70,7 @@ class ReleaseProfiles extends Component {
|
|||
<ReleaseProfile
|
||||
key={item.id}
|
||||
tagList={tagList}
|
||||
indexerList={indexerList}
|
||||
{...item}
|
||||
onConfirmDeleteReleaseProfile={onConfirmDeleteReleaseProfile}
|
||||
/>
|
||||
|
@ -92,6 +94,7 @@ ReleaseProfiles.propTypes = {
|
|||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteReleaseProfile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -2,24 +2,28 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchReleaseProfiles, deleteReleaseProfile } from 'Store/Actions/settingsActions';
|
||||
import { fetchReleaseProfiles, deleteReleaseProfile, fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import ReleaseProfiles from './ReleaseProfiles';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.releaseProfiles,
|
||||
(state) => state.settings.indexers,
|
||||
createTagsSelector(),
|
||||
(releaseProfiles, tagList) => {
|
||||
(releaseProfiles, indexers, tagList) => {
|
||||
return {
|
||||
...releaseProfiles,
|
||||
tagList
|
||||
tagList,
|
||||
isIndexersPopulated: indexers.isPopulated,
|
||||
indexerList: indexers.items
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchIndexers,
|
||||
fetchReleaseProfiles,
|
||||
deleteReleaseProfile
|
||||
};
|
||||
|
@ -31,6 +35,9 @@ class ReleaseProfilesConnector extends Component {
|
|||
|
||||
componentDidMount() {
|
||||
this.props.fetchReleaseProfiles();
|
||||
if (!this.props.isIndexersPopulated) {
|
||||
this.props.fetchIndexers();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -54,8 +61,10 @@ class ReleaseProfilesConnector extends Component {
|
|||
}
|
||||
|
||||
ReleaseProfilesConnector.propTypes = {
|
||||
isIndexersPopulated: PropTypes.bool.isRequired,
|
||||
fetchReleaseProfiles: PropTypes.func.isRequired,
|
||||
deleteReleaseProfile: PropTypes.func.isRequired
|
||||
deleteReleaseProfile: PropTypes.func.isRequired,
|
||||
fetchIndexers: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ReleaseProfilesConnector);
|
||||
|
|
|
@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService
|
|||
.Setup(s => s.AllForTags(It.IsAny<HashSet<int>>()))
|
||||
.Returns(new List<ReleaseProfile>());
|
||||
|
||||
Subject.Calculate(_series, _title).Should().Be(0);
|
||||
Subject.Calculate(_series, _title, 0).Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -63,7 +63,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService
|
|||
{
|
||||
GivenMatchingTerms();
|
||||
|
||||
Subject.Calculate(_series, _title).Should().Be(0);
|
||||
Subject.Calculate(_series, _title, 0).Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -71,7 +71,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService
|
|||
{
|
||||
GivenMatchingTerms("x264");
|
||||
|
||||
Subject.Calculate(_series, _title).Should().Be(5);
|
||||
Subject.Calculate(_series, _title, 0).Should().Be(5);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -79,7 +79,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService
|
|||
{
|
||||
GivenMatchingTerms("x265");
|
||||
|
||||
Subject.Calculate(_series, _title).Should().Be(-10);
|
||||
Subject.Calculate(_series, _title, 0).Should().Be(-10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -89,7 +89,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService
|
|||
|
||||
GivenMatchingTerms("x264");
|
||||
|
||||
Subject.Calculate(_series, _title).Should().Be(10);
|
||||
Subject.Calculate(_series, _title, 0).Should().Be(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(136)]
|
||||
public class add_indexer_and_enabled_to_release_profiles : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("ReleaseProfiles").AddColumn("Enabled").AsBoolean().WithDefaultValue(true);
|
||||
Alter.Table("ReleaseProfiles").AddColumn("IndexerId").AsInt32().WithDefaultValue(0);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(137)]
|
||||
public class add_airedbefore_to_episodes : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Episodes").AddColumn("AiredAfterSeasonNumber").AsInt32().Nullable()
|
||||
.AddColumn("AiredBeforeSeasonNumber").AsInt32().Nullable()
|
||||
.AddColumn("AiredBeforeEpisodeNumber").AsInt32().Nullable();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -41,7 +41,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
languageProfile,
|
||||
file.Quality,
|
||||
file.Language,
|
||||
_preferredWordServiceCalculator.Calculate(subject.Series, file.GetSceneOrFileName()),
|
||||
_preferredWordServiceCalculator.Calculate(subject.Series, file.GetSceneOrFileName(), subject.Release.IndexerId),
|
||||
subject.ParsedEpisodeInfo.Quality,
|
||||
subject.PreferredWordScore))
|
||||
{
|
||||
|
|
|
@ -53,7 +53,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
}
|
||||
|
||||
_logger.Debug("Checking if existing release in queue meets cutoff. Queued: {0} - {1}", remoteEpisode.ParsedEpisodeInfo.Quality, remoteEpisode.ParsedEpisodeInfo.Language);
|
||||
var queuedItemPreferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Series, queueItem.Title);
|
||||
var queuedItemPreferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Series, queueItem.Title, subject.Release.IndexerId);
|
||||
|
||||
if (!_upgradableSpecification.CutoffNotMet(qualityProfile,
|
||||
languageProfile,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
|
@ -15,6 +17,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
private readonly IReleaseProfileService _releaseProfileService;
|
||||
private readonly ITermMatcherService _termMatcherService;
|
||||
|
||||
private static readonly Regex keyValueRegex = new Regex(@"^([^:]+):([^:]+)$");
|
||||
|
||||
public ReleaseRestrictionsSpecification(ITermMatcherService termMatcherService, IReleaseProfileService releaseProfileService, Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
@ -30,16 +34,26 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
_logger.Debug("Checking if release meets restrictions: {0}", subject);
|
||||
|
||||
var title = subject.Release.Title;
|
||||
var restrictions = _releaseProfileService.AllForTags(subject.Series.Tags);
|
||||
var releaseProfiles = _releaseProfileService.EnabledForTags(subject.Series.Tags, subject.Release.IndexerId);
|
||||
|
||||
var required = restrictions.Where(r => r.Required.IsNotNullOrWhiteSpace());
|
||||
var ignored = restrictions.Where(r => r.Ignored.IsNotNullOrWhiteSpace());
|
||||
var required = releaseProfiles.Where(r => r.Required.IsNotNullOrWhiteSpace());
|
||||
var ignored = releaseProfiles.Where(r => r.Ignored.IsNotNullOrWhiteSpace());
|
||||
|
||||
foreach (var r in required)
|
||||
{
|
||||
var requiredTerms = r.Required.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
var requiredTerms = r.Required.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
|
||||
// separate key-value terms and normal terms
|
||||
var reqKeyValues = requiredTerms.Where(kv => keyValueRegex.IsMatch(kv)).ToList();
|
||||
var reqTitleTerms = requiredTerms.Where(t => !keyValueRegex.IsMatch(t)).ToList();
|
||||
|
||||
// check title terms
|
||||
var foundTerms = ContainsAny(reqTitleTerms, title);
|
||||
|
||||
// check key-value terms
|
||||
foundTerms.AddRange(ContainsAnyKeyValues(reqKeyValues, subject));
|
||||
|
||||
|
||||
var foundTerms = ContainsAny(requiredTerms, title);
|
||||
if (foundTerms.Empty())
|
||||
{
|
||||
var terms = string.Join(", ", requiredTerms);
|
||||
|
@ -48,11 +62,21 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
foreach (var r in ignored)
|
||||
{
|
||||
var ignoredTerms = r.Ignored.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
|
||||
var foundTerms = ContainsAny(ignoredTerms, title);
|
||||
// separate key-value terms and normal terms
|
||||
var ignKeyValues = ignoredTerms.Where(kv => keyValueRegex.IsMatch(kv)).ToList();
|
||||
var ignTitleTerms = ignoredTerms.Where(t => !keyValueRegex.IsMatch(t)).ToList();
|
||||
|
||||
// check title terms
|
||||
var foundTerms = ContainsAny(ignTitleTerms, title);
|
||||
|
||||
// check key-value terms
|
||||
foundTerms.AddRange(ContainsAnyKeyValues(ignKeyValues, subject));
|
||||
|
||||
if (foundTerms.Any())
|
||||
{
|
||||
var terms = string.Join(", ", foundTerms);
|
||||
|
@ -60,7 +84,6 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
return Decision.Reject("Contains these ignored terms: {0}", terms);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("[{0}] No restrictions apply, allowing", subject);
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
@ -69,5 +92,34 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
{
|
||||
return terms.Where(t => _termMatcherService.IsMatch(t, title)).ToList();
|
||||
}
|
||||
|
||||
private List<string> ContainsAnyKeyValues(List<string> terms, RemoteEpisode subject)
|
||||
{
|
||||
var foundTerms = new List<string>();
|
||||
|
||||
foreach (var kv in terms)
|
||||
{
|
||||
var match = keyValueRegex.Match(kv);
|
||||
var key = match.Groups[1].Value;
|
||||
var value = match.Groups[2].Value;
|
||||
|
||||
try
|
||||
{
|
||||
IReleaseFilter releaseFilter = Assembly.GetExecutingAssembly().
|
||||
CreateInstance("NzbDrone.Core.Profiles.Releases." + key + "ReleaseFilter", true) as IReleaseFilter;
|
||||
|
||||
if (releaseFilter.Matches(value, subject))
|
||||
{
|
||||
foundTerms.Add(kv);
|
||||
}
|
||||
}
|
||||
catch (NullReferenceException)
|
||||
{
|
||||
_logger.Debug("Unsupported key {0}", key);
|
||||
}
|
||||
}
|
||||
|
||||
return foundTerms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||
|
||||
// The series will be the same as the one in history since it's the same episode.
|
||||
// Instead of fetching the series from the DB reuse the known series.
|
||||
var preferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Series, mostRecent.SourceTitle);
|
||||
var preferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Series, mostRecent.SourceTitle, subject.Release.IndexerId);
|
||||
|
||||
var cutoffUnmet = _upgradableSpecification.CutoffNotMet(
|
||||
subject.Series.QualityProfile,
|
||||
|
|
|
@ -38,7 +38,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
subject.Series.LanguageProfile,
|
||||
file.Quality,
|
||||
file.Language,
|
||||
_preferredWordServiceCalculator.Calculate(subject.Series, file.GetSceneOrFileName()),
|
||||
_preferredWordServiceCalculator.Calculate(subject.Series, file.GetSceneOrFileName(), subject.Release.IndexerId),
|
||||
subject.ParsedEpisodeInfo.Quality,
|
||||
subject.ParsedEpisodeInfo.Language,
|
||||
subject.PreferredWordScore))
|
||||
|
|
|
@ -14,7 +14,7 @@ namespace NzbDrone.Core.Download.Aggregation.Aggregators
|
|||
|
||||
public RemoteEpisode Aggregate(RemoteEpisode remoteEpisode)
|
||||
{
|
||||
remoteEpisode.PreferredWordScore = _preferredWordServiceCalculator.Calculate(remoteEpisode.Series, remoteEpisode.Release.Title);
|
||||
remoteEpisode.PreferredWordScore = _preferredWordServiceCalculator.Calculate(remoteEpisode.Series, remoteEpisode.Release.Title, remoteEpisode.Release.IndexerId);
|
||||
|
||||
return remoteEpisode;
|
||||
}
|
||||
|
|
|
@ -255,9 +255,15 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
|
|||
details.Add(new XElement("aired", episode.AirDate));
|
||||
details.Add(new XElement("plot", episode.Overview));
|
||||
|
||||
//If trakt ever gets airs before information for specials we should add set it
|
||||
details.Add(new XElement("displayseason"));
|
||||
details.Add(new XElement("displayepisode"));
|
||||
if (episode.SeasonNumber == 0 && episode.AiredAfterSeasonNumber.HasValue)
|
||||
{
|
||||
details.Add(new XElement("displayafterseason", episode.AiredAfterSeasonNumber));
|
||||
}
|
||||
else if (episode.SeasonNumber == 0 && episode.AiredBeforeSeasonNumber.HasValue)
|
||||
{
|
||||
details.Add(new XElement("displayseason", episode.AiredBeforeSeasonNumber));
|
||||
details.Add(new XElement("displayepisode", episode.AiredBeforeEpisodeNumber ?? -1));
|
||||
}
|
||||
|
||||
var uniqueId = new XElement("uniqueid", episode.Id);
|
||||
uniqueId.SetAttributeValue("type", "sonarr");
|
||||
|
|
|
@ -7,6 +7,9 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
|
|||
public int SeasonNumber { get; set; }
|
||||
public int EpisodeNumber { get; set; }
|
||||
public int? AbsoluteEpisodeNumber { get; set; }
|
||||
public int? AiredAfterSeasonNumber { get; set; }
|
||||
public int? AiredBeforeSeasonNumber { get; set; }
|
||||
public int? AiredBeforeEpisodeNumber { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string AirDate { get; set; }
|
||||
public DateTime? AirDateUtc { get; set; }
|
||||
|
|
|
@ -222,6 +222,9 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
|||
episode.EpisodeNumber = oracleEpisode.EpisodeNumber;
|
||||
episode.AbsoluteEpisodeNumber = oracleEpisode.AbsoluteEpisodeNumber;
|
||||
episode.Title = oracleEpisode.Title;
|
||||
episode.AiredAfterSeasonNumber = oracleEpisode.AiredAfterSeasonNumber;
|
||||
episode.AiredBeforeSeasonNumber = oracleEpisode.AiredBeforeSeasonNumber;
|
||||
episode.AiredBeforeEpisodeNumber = oracleEpisode.AiredBeforeEpisodeNumber;
|
||||
|
||||
episode.AirDate = oracleEpisode.AirDate;
|
||||
episode.AirDateUtc = oracleEpisode.AirDateUtc;
|
||||
|
|
|
@ -8,7 +8,7 @@ namespace NzbDrone.Core.Profiles.Releases
|
|||
{
|
||||
public interface IPreferredWordService
|
||||
{
|
||||
int Calculate(Series series, string title);
|
||||
int Calculate(Series series, string title, int indexerId);
|
||||
List<string> GetMatchingPreferredWords(Series series, string title);
|
||||
}
|
||||
|
||||
|
@ -25,11 +25,11 @@ namespace NzbDrone.Core.Profiles.Releases
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public int Calculate(Series series, string title)
|
||||
public int Calculate(Series series, string title, int indexerId)
|
||||
{
|
||||
_logger.Trace("Calculating preferred word score for '{0}'", title);
|
||||
|
||||
var releaseProfiles = _releaseProfileService.AllForTags(series.Tags);
|
||||
var releaseProfiles = _releaseProfileService.EnabledForTags(series.Tags, indexerId);
|
||||
var matchingPairs = new List<KeyValuePair<string, int>>();
|
||||
|
||||
foreach (var releaseProfile in releaseProfiles)
|
||||
|
@ -54,7 +54,7 @@ namespace NzbDrone.Core.Profiles.Releases
|
|||
|
||||
public List<string> GetMatchingPreferredWords(Series series, string title)
|
||||
{
|
||||
var releaseProfiles = _releaseProfileService.AllForTags(series.Tags);
|
||||
var releaseProfiles = _releaseProfileService.EnabledForTags(series.Tags, 0);
|
||||
var matchingPairs = new List<KeyValuePair<string, int>>();
|
||||
|
||||
_logger.Trace("Calculating preferred word score for '{0}'", title);
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Profiles.Releases
|
||||
{
|
||||
public interface IReleaseFilter
|
||||
{
|
||||
string Key { get; }
|
||||
bool Matches(string filterValue, RemoteEpisode subject);
|
||||
//List<string> SuggestAutoComplete(string filterValue, int indexerId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Profiles.Releases
|
||||
{
|
||||
public class OriginReleaseFilter : IReleaseFilter
|
||||
{
|
||||
public string Key => "Origin";
|
||||
|
||||
public bool Matches(string filterValue, RemoteEpisode subject)
|
||||
{
|
||||
return filterValue.Equals(subject.Release.Origin, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,17 +5,21 @@ namespace NzbDrone.Core.Profiles.Releases
|
|||
{
|
||||
public class ReleaseProfile : ModelBase
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
public string Required { get; set; }
|
||||
public string Ignored { get; set; }
|
||||
public List<KeyValuePair<string, int>> Preferred { get; set; }
|
||||
public bool IncludePreferredWhenRenaming { get; set; }
|
||||
public int IndexerId { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
|
||||
public ReleaseProfile()
|
||||
{
|
||||
Enabled = true;
|
||||
Preferred = new List<KeyValuePair<string, int>>();
|
||||
IncludePreferredWhenRenaming = true;
|
||||
Tags = new HashSet<int>();
|
||||
IndexerId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ namespace NzbDrone.Core.Profiles.Releases
|
|||
List<ReleaseProfile> All();
|
||||
List<ReleaseProfile> AllForTag(int tagId);
|
||||
List<ReleaseProfile> AllForTags(HashSet<int> tagIds);
|
||||
List<ReleaseProfile> EnabledForTags(HashSet<int> tagIds, int indexerId);
|
||||
ReleaseProfile Get(int id);
|
||||
void Delete(int id);
|
||||
ReleaseProfile Add(ReleaseProfile restriction);
|
||||
|
@ -48,6 +49,13 @@ namespace NzbDrone.Core.Profiles.Releases
|
|||
return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList();
|
||||
}
|
||||
|
||||
public List<ReleaseProfile> EnabledForTags(HashSet<int> tagIds, int indexerId)
|
||||
{
|
||||
return (List<ReleaseProfile>)AllForTags(tagIds)
|
||||
.Where(r => r.Enabled)
|
||||
.Where(r => r.IndexerId == indexerId || r.IndexerId == 0).ToList();
|
||||
}
|
||||
|
||||
public ReleaseProfile Get(int id)
|
||||
{
|
||||
return _repo.Get(id);
|
||||
|
|
|
@ -29,6 +29,9 @@ namespace NzbDrone.Core.Tv
|
|||
public int? SceneAbsoluteEpisodeNumber { get; set; }
|
||||
public int? SceneSeasonNumber { get; set; }
|
||||
public int? SceneEpisodeNumber { get; set; }
|
||||
public int? AiredAfterSeasonNumber { get; set; }
|
||||
public int? AiredBeforeSeasonNumber { get; set; }
|
||||
public int? AiredBeforeEpisodeNumber { get; set; }
|
||||
public bool UnverifiedSceneNumbering { get; set; }
|
||||
public Ratings Ratings { get; set; }
|
||||
public List<MediaCover.MediaCover> Images { get; set; }
|
||||
|
|
|
@ -66,6 +66,9 @@ namespace NzbDrone.Core.Tv
|
|||
episodeToUpdate.EpisodeNumber = episode.EpisodeNumber;
|
||||
episodeToUpdate.SeasonNumber = episode.SeasonNumber;
|
||||
episodeToUpdate.AbsoluteEpisodeNumber = episode.AbsoluteEpisodeNumber;
|
||||
episodeToUpdate.AiredAfterSeasonNumber = episode.AiredAfterSeasonNumber;
|
||||
episodeToUpdate.AiredBeforeSeasonNumber = episode.AiredBeforeSeasonNumber;
|
||||
episodeToUpdate.AiredBeforeEpisodeNumber = episode.AiredBeforeEpisodeNumber;
|
||||
episodeToUpdate.Title = episode.Title ?? "TBA";
|
||||
episodeToUpdate.Overview = episode.Overview;
|
||||
episodeToUpdate.AirDate = episode.AirDate;
|
||||
|
|
|
@ -7,10 +7,12 @@ namespace Sonarr.Api.V3.Profiles.Release
|
|||
{
|
||||
public class ReleaseProfileResource : RestResource
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
public string Required { get; set; }
|
||||
public string Ignored { get; set; }
|
||||
public List<KeyValuePair<string, int>> Preferred { get; set; }
|
||||
public bool IncludePreferredWhenRenaming { get; set; }
|
||||
public int IndexerId { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
|
||||
public ReleaseProfileResource()
|
||||
|
@ -29,10 +31,12 @@ namespace Sonarr.Api.V3.Profiles.Release
|
|||
{
|
||||
Id = model.Id,
|
||||
|
||||
Enabled = model.Enabled,
|
||||
Required = model.Required,
|
||||
Ignored = model.Ignored,
|
||||
Preferred = model.Preferred,
|
||||
IncludePreferredWhenRenaming = model.IncludePreferredWhenRenaming,
|
||||
IndexerId = model.IndexerId,
|
||||
Tags = new HashSet<int>(model.Tags)
|
||||
};
|
||||
}
|
||||
|
@ -45,10 +49,12 @@ namespace Sonarr.Api.V3.Profiles.Release
|
|||
{
|
||||
Id = resource.Id,
|
||||
|
||||
Enabled = resource.Enabled,
|
||||
Required = resource.Required,
|
||||
Ignored = resource.Ignored,
|
||||
Preferred = resource.Preferred,
|
||||
IncludePreferredWhenRenaming = resource.IncludePreferredWhenRenaming,
|
||||
IndexerId = resource.IndexerId,
|
||||
Tags = new HashSet<int>(resource.Tags)
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue