Compare commits

...

4 Commits

Author SHA1 Message Date
Taloth Saldono 808836a65f Help text 2020-02-15 01:30:27 +01:00
Taloth Saldono 6f7e505bed Added Enabled & IndexerId to Edit Release Profile UI 2020-02-14 23:50:50 +01:00
Jacob ecf1d75954 ui v1 2020-02-14 23:21:22 +01:00
netpok c5257de436 New: Added aired-before field to kodi metadata to sort specials
closes #3073
2020-02-14 22:42:24 +01:00
28 changed files with 383 additions and 51 deletions

View File

@ -14,6 +14,7 @@ import PasswordInput from './PasswordInput';
import PathInputConnector from './PathInputConnector'; import PathInputConnector from './PathInputConnector';
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector'; import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
import LanguageProfileSelectInputConnector from './LanguageProfileSelectInputConnector'; import LanguageProfileSelectInputConnector from './LanguageProfileSelectInputConnector';
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector'; import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
import SeriesTypeSelectInput from './SeriesTypeSelectInput'; import SeriesTypeSelectInput from './SeriesTypeSelectInput';
import EnhancedSelectInput from './EnhancedSelectInput'; import EnhancedSelectInput from './EnhancedSelectInput';
@ -61,6 +62,9 @@ function getComponent(type) {
case inputTypes.LANGUAGE_PROFILE_SELECT: case inputTypes.LANGUAGE_PROFILE_SELECT:
return LanguageProfileSelectInputConnector; return LanguageProfileSelectInputConnector;
case inputTypes.INDEXER_SELECT:
return IndexerSelectInputConnector;
case inputTypes.ROOT_FOLDER_SELECT: case inputTypes.ROOT_FOLDER_SELECT:
return RootFolderSelectInputConnector; return RootFolderSelectInputConnector;

View File

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

View File

@ -10,6 +10,7 @@ export const PASSWORD = 'password';
export const PATH = 'path'; export const PATH = 'path';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect'; export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const LANGUAGE_PROFILE_SELECT = 'languageProfileSelect'; export const LANGUAGE_PROFILE_SELECT = 'languageProfileSelect';
export const INDEXER_SELECT = 'indexerSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const SELECT = 'select'; export const SELECT = 'select';
export const SERIES_TYPE_SELECT = 'seriesTypeSelect'; export const SERIES_TYPE_SELECT = 'seriesTypeSelect';
@ -30,6 +31,7 @@ export const all = [
PATH, PATH,
QUALITY_PROFILE_SELECT, QUALITY_PROFILE_SELECT,
LANGUAGE_PROFILE_SELECT, LANGUAGE_PROFILE_SELECT,
INDEXER_SELECT,
ROOT_FOLDER_SELECT, ROOT_FOLDER_SELECT,
SELECT, SELECT,
SERIES_TYPE_SELECT, SERIES_TYPE_SELECT,

View File

@ -30,11 +30,13 @@ function EditReleaseProfileModalContent(props) {
const { const {
id, id,
enabled,
required, required,
ignored, ignored,
preferred, preferred,
includePreferredWhenRenaming, includePreferredWhenRenaming,
tags tags,
indexerId
} = item; } = item;
return ( return (
@ -45,6 +47,18 @@ function EditReleaseProfileModalContent(props) {
<ModalBody> <ModalBody>
<Form {...otherProps}> <Form {...otherProps}>
<FormGroup>
<FormLabel>Enable Profile</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enabled"
helpText="Check to enable release profile"
{...enabled}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup> <FormGroup>
<FormLabel>Must Contain</FormLabel> <FormLabel>Must Contain</FormLabel>
@ -99,9 +113,23 @@ function EditReleaseProfileModalContent(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="includePreferredWhenRenaming" 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} {...includePreferredWhenRenaming}
onChange={onInputChange} 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> </FormGroup>

View File

@ -8,11 +8,13 @@ import { setReleaseProfileValue, saveReleaseProfile } from 'Store/Actions/settin
import EditReleaseProfileModalContent from './EditReleaseProfileModalContent'; import EditReleaseProfileModalContent from './EditReleaseProfileModalContent';
const newReleaseProfile = { const newReleaseProfile = {
enabled: true,
required: '', required: '',
ignored: '', ignored: '',
preferred: [], preferred: [],
includePreferredWhenRenaming: false, includePreferredWhenRenaming: false,
tags: [] tags: [],
indexerId: 0
}; };
function createMapStateToProps() { function createMapStateToProps() {

View File

@ -1,3 +1,4 @@
import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import split from 'Utilities/String/split'; import split from 'Utilities/String/split';
@ -55,11 +56,14 @@ class ReleaseProfile extends Component {
render() { render() {
const { const {
id, id,
enabled,
required, required,
ignored, ignored,
preferred, preferred,
tags, tags,
tagList indexerId,
tagList,
indexerList
} = this.props; } = this.props;
const { const {
@ -67,6 +71,8 @@ class ReleaseProfile extends Component {
isDeleteReleaseProfileModalOpen isDeleteReleaseProfileModalOpen
} = this.state; } = this.state;
const indexer = indexerId !== 0 && _.find(indexerList, { id: indexerId });
return ( return (
<Card <Card
className={styles.releaseProfile} className={styles.releaseProfile}
@ -92,6 +98,23 @@ class ReleaseProfile extends Component {
} }
</div> </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> <div>
{ {
split(ignored).map((item) => { split(ignored).map((item) => {
@ -111,28 +134,33 @@ class ReleaseProfile extends Component {
} }
</div> </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 <TagList
tags={tags} tags={tags}
tagList={tagList} tagList={tagList}
/> />
<div>
{
!enabled &&
<Label
kind={kinds.DISABLED}
outline={true}
>
Disabled
</Label>
}
{
indexer &&
<Label
kind={kinds.INFO}
outline={true}
>
{indexer.name}
</Label>
}
</div>
<EditReleaseProfileModalConnector <EditReleaseProfileModalConnector
id={id} id={id}
isOpen={isEditReleaseProfileModalOpen} isOpen={isEditReleaseProfileModalOpen}
@ -156,18 +184,23 @@ class ReleaseProfile extends Component {
ReleaseProfile.propTypes = { ReleaseProfile.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
enabled: PropTypes.bool.isRequired,
required: PropTypes.string.isRequired, required: PropTypes.string.isRequired,
ignored: PropTypes.string.isRequired, ignored: PropTypes.string.isRequired,
preferred: PropTypes.arrayOf(PropTypes.object).isRequired, preferred: PropTypes.arrayOf(PropTypes.object).isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired, tags: PropTypes.arrayOf(PropTypes.number).isRequired,
indexerId: PropTypes.number.isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired, tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteReleaseProfile: PropTypes.func.isRequired onConfirmDeleteReleaseProfile: PropTypes.func.isRequired
}; };
ReleaseProfile.defaultProps = { ReleaseProfile.defaultProps = {
enabled: true,
required: '', required: '',
ignored: '', ignored: '',
preferred: [] preferred: [],
indexerId: 0
}; };
export default ReleaseProfile; export default ReleaseProfile;

View File

@ -40,6 +40,7 @@ class ReleaseProfiles extends Component {
const { const {
items, items,
tagList, tagList,
indexerList,
onConfirmDeleteReleaseProfile, onConfirmDeleteReleaseProfile,
...otherProps ...otherProps
} = this.props; } = this.props;
@ -69,6 +70,7 @@ class ReleaseProfiles extends Component {
<ReleaseProfile <ReleaseProfile
key={item.id} key={item.id}
tagList={tagList} tagList={tagList}
indexerList={indexerList}
{...item} {...item}
onConfirmDeleteReleaseProfile={onConfirmDeleteReleaseProfile} onConfirmDeleteReleaseProfile={onConfirmDeleteReleaseProfile}
/> />
@ -92,6 +94,7 @@ ReleaseProfiles.propTypes = {
error: PropTypes.object, error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired, tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteReleaseProfile: PropTypes.func.isRequired onConfirmDeleteReleaseProfile: PropTypes.func.isRequired
}; };

View File

@ -2,24 +2,28 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; 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 createTagsSelector from 'Store/Selectors/createTagsSelector';
import ReleaseProfiles from './ReleaseProfiles'; import ReleaseProfiles from './ReleaseProfiles';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.settings.releaseProfiles, (state) => state.settings.releaseProfiles,
(state) => state.settings.indexers,
createTagsSelector(), createTagsSelector(),
(releaseProfiles, tagList) => { (releaseProfiles, indexers, tagList) => {
return { return {
...releaseProfiles, ...releaseProfiles,
tagList tagList,
isIndexersPopulated: indexers.isPopulated,
indexerList: indexers.items
}; };
} }
); );
} }
const mapDispatchToProps = { const mapDispatchToProps = {
fetchIndexers,
fetchReleaseProfiles, fetchReleaseProfiles,
deleteReleaseProfile deleteReleaseProfile
}; };
@ -31,6 +35,9 @@ class ReleaseProfilesConnector extends Component {
componentDidMount() { componentDidMount() {
this.props.fetchReleaseProfiles(); this.props.fetchReleaseProfiles();
if (!this.props.isIndexersPopulated) {
this.props.fetchIndexers();
}
} }
// //
@ -54,8 +61,10 @@ class ReleaseProfilesConnector extends Component {
} }
ReleaseProfilesConnector.propTypes = { ReleaseProfilesConnector.propTypes = {
isIndexersPopulated: PropTypes.bool.isRequired,
fetchReleaseProfiles: PropTypes.func.isRequired, fetchReleaseProfiles: PropTypes.func.isRequired,
deleteReleaseProfile: PropTypes.func.isRequired deleteReleaseProfile: PropTypes.func.isRequired,
fetchIndexers: PropTypes.func.isRequired
}; };
export default connect(createMapStateToProps, mapDispatchToProps)(ReleaseProfilesConnector); export default connect(createMapStateToProps, mapDispatchToProps)(ReleaseProfilesConnector);

View File

@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService
.Setup(s => s.AllForTags(It.IsAny<HashSet<int>>())) .Setup(s => s.AllForTags(It.IsAny<HashSet<int>>()))
.Returns(new List<ReleaseProfile>()); .Returns(new List<ReleaseProfile>());
Subject.Calculate(_series, _title).Should().Be(0); Subject.Calculate(_series, _title, 0).Should().Be(0);
} }
[Test] [Test]
@ -63,7 +63,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService
{ {
GivenMatchingTerms(); GivenMatchingTerms();
Subject.Calculate(_series, _title).Should().Be(0); Subject.Calculate(_series, _title, 0).Should().Be(0);
} }
[Test] [Test]
@ -71,7 +71,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService
{ {
GivenMatchingTerms("x264"); GivenMatchingTerms("x264");
Subject.Calculate(_series, _title).Should().Be(5); Subject.Calculate(_series, _title, 0).Should().Be(5);
} }
[Test] [Test]
@ -79,7 +79,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService
{ {
GivenMatchingTerms("x265"); GivenMatchingTerms("x265");
Subject.Calculate(_series, _title).Should().Be(-10); Subject.Calculate(_series, _title, 0).Should().Be(-10);
} }
[Test] [Test]
@ -89,7 +89,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService
GivenMatchingTerms("x264"); GivenMatchingTerms("x264");
Subject.Calculate(_series, _title).Should().Be(10); Subject.Calculate(_series, _title, 0).Should().Be(10);
} }
} }
} }

View File

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

View File

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

View File

@ -41,7 +41,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
languageProfile, languageProfile,
file.Quality, file.Quality,
file.Language, file.Language,
_preferredWordServiceCalculator.Calculate(subject.Series, file.GetSceneOrFileName()), _preferredWordServiceCalculator.Calculate(subject.Series, file.GetSceneOrFileName(), subject.Release.IndexerId),
subject.ParsedEpisodeInfo.Quality, subject.ParsedEpisodeInfo.Quality,
subject.PreferredWordScore)) subject.PreferredWordScore))
{ {

View File

@ -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); _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, if (!_upgradableSpecification.CutoffNotMet(qualityProfile,
languageProfile, languageProfile,

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Reflection;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using NLog; using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
@ -15,6 +17,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
private readonly IReleaseProfileService _releaseProfileService; private readonly IReleaseProfileService _releaseProfileService;
private readonly ITermMatcherService _termMatcherService; private readonly ITermMatcherService _termMatcherService;
private static readonly Regex keyValueRegex = new Regex(@"^([^:]+):([^:]+)$");
public ReleaseRestrictionsSpecification(ITermMatcherService termMatcherService, IReleaseProfileService releaseProfileService, Logger logger) public ReleaseRestrictionsSpecification(ITermMatcherService termMatcherService, IReleaseProfileService releaseProfileService, Logger logger)
{ {
_logger = logger; _logger = logger;
@ -30,16 +34,26 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
_logger.Debug("Checking if release meets restrictions: {0}", subject); _logger.Debug("Checking if release meets restrictions: {0}", subject);
var title = subject.Release.Title; 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 required = releaseProfiles.Where(r => r.Required.IsNotNullOrWhiteSpace());
var ignored = restrictions.Where(r => r.Ignored.IsNotNullOrWhiteSpace()); var ignored = releaseProfiles.Where(r => r.Ignored.IsNotNullOrWhiteSpace());
foreach (var r in required) 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()) if (foundTerms.Empty())
{ {
var terms = string.Join(", ", requiredTerms); var terms = string.Join(", ", requiredTerms);
@ -48,11 +62,21 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
} }
} }
foreach (var r in ignored) foreach (var r in ignored)
{ {
var ignoredTerms = r.Ignored.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); 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()) if (foundTerms.Any())
{ {
var terms = string.Join(", ", foundTerms); var terms = string.Join(", ", foundTerms);
@ -60,7 +84,6 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
return Decision.Reject("Contains these ignored terms: {0}", terms); return Decision.Reject("Contains these ignored terms: {0}", terms);
} }
} }
_logger.Debug("[{0}] No restrictions apply, allowing", subject); _logger.Debug("[{0}] No restrictions apply, allowing", subject);
return Decision.Accept(); return Decision.Accept();
} }
@ -69,5 +92,34 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
{ {
return terms.Where(t => _termMatcherService.IsMatch(t, title)).ToList(); 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;
}
} }
} }

View File

@ -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. // 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. // 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( var cutoffUnmet = _upgradableSpecification.CutoffNotMet(
subject.Series.QualityProfile, subject.Series.QualityProfile,

View File

@ -38,7 +38,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
subject.Series.LanguageProfile, subject.Series.LanguageProfile,
file.Quality, file.Quality,
file.Language, file.Language,
_preferredWordServiceCalculator.Calculate(subject.Series, file.GetSceneOrFileName()), _preferredWordServiceCalculator.Calculate(subject.Series, file.GetSceneOrFileName(), subject.Release.IndexerId),
subject.ParsedEpisodeInfo.Quality, subject.ParsedEpisodeInfo.Quality,
subject.ParsedEpisodeInfo.Language, subject.ParsedEpisodeInfo.Language,
subject.PreferredWordScore)) subject.PreferredWordScore))

View File

@ -14,7 +14,7 @@ namespace NzbDrone.Core.Download.Aggregation.Aggregators
public RemoteEpisode Aggregate(RemoteEpisode remoteEpisode) 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; return remoteEpisode;
} }

View File

@ -255,10 +255,16 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
details.Add(new XElement("aired", episode.AirDate)); details.Add(new XElement("aired", episode.AirDate));
details.Add(new XElement("plot", episode.Overview)); details.Add(new XElement("plot", episode.Overview));
//If trakt ever gets airs before information for specials we should add set it if (episode.SeasonNumber == 0 && episode.AiredAfterSeasonNumber.HasValue)
details.Add(new XElement("displayseason")); {
details.Add(new XElement("displayepisode")); 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); var uniqueId = new XElement("uniqueid", episode.Id);
uniqueId.SetAttributeValue("type", "sonarr"); uniqueId.SetAttributeValue("type", "sonarr");
uniqueId.SetAttributeValue("default", true); uniqueId.SetAttributeValue("default", true);

View File

@ -7,6 +7,9 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
public int SeasonNumber { get; set; } public int SeasonNumber { get; set; }
public int EpisodeNumber { get; set; } public int EpisodeNumber { get; set; }
public int? AbsoluteEpisodeNumber { 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 Title { get; set; }
public string AirDate { get; set; } public string AirDate { get; set; }
public DateTime? AirDateUtc { get; set; } public DateTime? AirDateUtc { get; set; }

View File

@ -222,6 +222,9 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
episode.EpisodeNumber = oracleEpisode.EpisodeNumber; episode.EpisodeNumber = oracleEpisode.EpisodeNumber;
episode.AbsoluteEpisodeNumber = oracleEpisode.AbsoluteEpisodeNumber; episode.AbsoluteEpisodeNumber = oracleEpisode.AbsoluteEpisodeNumber;
episode.Title = oracleEpisode.Title; episode.Title = oracleEpisode.Title;
episode.AiredAfterSeasonNumber = oracleEpisode.AiredAfterSeasonNumber;
episode.AiredBeforeSeasonNumber = oracleEpisode.AiredBeforeSeasonNumber;
episode.AiredBeforeEpisodeNumber = oracleEpisode.AiredBeforeEpisodeNumber;
episode.AirDate = oracleEpisode.AirDate; episode.AirDate = oracleEpisode.AirDate;
episode.AirDateUtc = oracleEpisode.AirDateUtc; episode.AirDateUtc = oracleEpisode.AirDateUtc;

View File

@ -8,7 +8,7 @@ namespace NzbDrone.Core.Profiles.Releases
{ {
public interface IPreferredWordService public interface IPreferredWordService
{ {
int Calculate(Series series, string title); int Calculate(Series series, string title, int indexerId);
List<string> GetMatchingPreferredWords(Series series, string title); List<string> GetMatchingPreferredWords(Series series, string title);
} }
@ -25,11 +25,11 @@ namespace NzbDrone.Core.Profiles.Releases
_logger = logger; _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); _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>>(); var matchingPairs = new List<KeyValuePair<string, int>>();
foreach (var releaseProfile in releaseProfiles) foreach (var releaseProfile in releaseProfiles)
@ -54,7 +54,7 @@ namespace NzbDrone.Core.Profiles.Releases
public List<string> GetMatchingPreferredWords(Series series, string title) 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>>(); var matchingPairs = new List<KeyValuePair<string, int>>();
_logger.Trace("Calculating preferred word score for '{0}'", title); _logger.Trace("Calculating preferred word score for '{0}'", title);

View File

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

View File

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

View File

@ -5,17 +5,21 @@ namespace NzbDrone.Core.Profiles.Releases
{ {
public class ReleaseProfile : ModelBase public class ReleaseProfile : ModelBase
{ {
public bool Enabled { get; set; }
public string Required { get; set; } public string Required { get; set; }
public string Ignored { get; set; } public string Ignored { get; set; }
public List<KeyValuePair<string, int>> Preferred { get; set; } public List<KeyValuePair<string, int>> Preferred { get; set; }
public bool IncludePreferredWhenRenaming { get; set; } public bool IncludePreferredWhenRenaming { get; set; }
public int IndexerId { get; set; }
public HashSet<int> Tags { get; set; } public HashSet<int> Tags { get; set; }
public ReleaseProfile() public ReleaseProfile()
{ {
Enabled = true;
Preferred = new List<KeyValuePair<string, int>>(); Preferred = new List<KeyValuePair<string, int>>();
IncludePreferredWhenRenaming = true; IncludePreferredWhenRenaming = true;
Tags = new HashSet<int>(); Tags = new HashSet<int>();
IndexerId = 0;
} }
} }

View File

@ -10,6 +10,7 @@ namespace NzbDrone.Core.Profiles.Releases
List<ReleaseProfile> All(); List<ReleaseProfile> All();
List<ReleaseProfile> AllForTag(int tagId); List<ReleaseProfile> AllForTag(int tagId);
List<ReleaseProfile> AllForTags(HashSet<int> tagIds); List<ReleaseProfile> AllForTags(HashSet<int> tagIds);
List<ReleaseProfile> EnabledForTags(HashSet<int> tagIds, int indexerId);
ReleaseProfile Get(int id); ReleaseProfile Get(int id);
void Delete(int id); void Delete(int id);
ReleaseProfile Add(ReleaseProfile restriction); 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(); 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) public ReleaseProfile Get(int id)
{ {
return _repo.Get(id); return _repo.Get(id);

View File

@ -29,6 +29,9 @@ namespace NzbDrone.Core.Tv
public int? SceneAbsoluteEpisodeNumber { get; set; } public int? SceneAbsoluteEpisodeNumber { get; set; }
public int? SceneSeasonNumber { get; set; } public int? SceneSeasonNumber { get; set; }
public int? SceneEpisodeNumber { 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 bool UnverifiedSceneNumbering { get; set; }
public Ratings Ratings { get; set; } public Ratings Ratings { get; set; }
public List<MediaCover.MediaCover> Images { get; set; } public List<MediaCover.MediaCover> Images { get; set; }

View File

@ -66,6 +66,9 @@ namespace NzbDrone.Core.Tv
episodeToUpdate.EpisodeNumber = episode.EpisodeNumber; episodeToUpdate.EpisodeNumber = episode.EpisodeNumber;
episodeToUpdate.SeasonNumber = episode.SeasonNumber; episodeToUpdate.SeasonNumber = episode.SeasonNumber;
episodeToUpdate.AbsoluteEpisodeNumber = episode.AbsoluteEpisodeNumber; episodeToUpdate.AbsoluteEpisodeNumber = episode.AbsoluteEpisodeNumber;
episodeToUpdate.AiredAfterSeasonNumber = episode.AiredAfterSeasonNumber;
episodeToUpdate.AiredBeforeSeasonNumber = episode.AiredBeforeSeasonNumber;
episodeToUpdate.AiredBeforeEpisodeNumber = episode.AiredBeforeEpisodeNumber;
episodeToUpdate.Title = episode.Title ?? "TBA"; episodeToUpdate.Title = episode.Title ?? "TBA";
episodeToUpdate.Overview = episode.Overview; episodeToUpdate.Overview = episode.Overview;
episodeToUpdate.AirDate = episode.AirDate; episodeToUpdate.AirDate = episode.AirDate;

View File

@ -7,10 +7,12 @@ namespace Sonarr.Api.V3.Profiles.Release
{ {
public class ReleaseProfileResource : RestResource public class ReleaseProfileResource : RestResource
{ {
public bool Enabled { get; set; }
public string Required { get; set; } public string Required { get; set; }
public string Ignored { get; set; } public string Ignored { get; set; }
public List<KeyValuePair<string, int>> Preferred { get; set; } public List<KeyValuePair<string, int>> Preferred { get; set; }
public bool IncludePreferredWhenRenaming { get; set; } public bool IncludePreferredWhenRenaming { get; set; }
public int IndexerId { get; set; }
public HashSet<int> Tags { get; set; } public HashSet<int> Tags { get; set; }
public ReleaseProfileResource() public ReleaseProfileResource()
@ -29,10 +31,12 @@ namespace Sonarr.Api.V3.Profiles.Release
{ {
Id = model.Id, Id = model.Id,
Enabled = model.Enabled,
Required = model.Required, Required = model.Required,
Ignored = model.Ignored, Ignored = model.Ignored,
Preferred = model.Preferred, Preferred = model.Preferred,
IncludePreferredWhenRenaming = model.IncludePreferredWhenRenaming, IncludePreferredWhenRenaming = model.IncludePreferredWhenRenaming,
IndexerId = model.IndexerId,
Tags = new HashSet<int>(model.Tags) Tags = new HashSet<int>(model.Tags)
}; };
} }
@ -45,10 +49,12 @@ namespace Sonarr.Api.V3.Profiles.Release
{ {
Id = resource.Id, Id = resource.Id,
Enabled = resource.Enabled,
Required = resource.Required, Required = resource.Required,
Ignored = resource.Ignored, Ignored = resource.Ignored,
Preferred = resource.Preferred, Preferred = resource.Preferred,
IncludePreferredWhenRenaming = resource.IncludePreferredWhenRenaming, IncludePreferredWhenRenaming = resource.IncludePreferredWhenRenaming,
IndexerId = resource.IndexerId,
Tags = new HashSet<int>(resource.Tags) Tags = new HashSet<int>(resource.Tags)
}; };
} }