New: Added option to filter Release Profile to a specific indexer

pull/3596/head
Jacob 5 years ago committed by Taloth Saldono
parent c07a67ae3c
commit 99728a604d

@ -1,19 +1,8 @@
.enhancedSelect {
composes: input from '~Components/Form/Input.css';
composes: link from '~Components/Link/Link.css';
position: relative;
display: flex;
align-items: center;
padding: 6px 16px;
width: 100%;
height: 35px;
border: 1px solid $inputBorderColor;
border-radius: 4px;
background-color: $white;
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
color: $black;
cursor: default;
}
.hasError {

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

@ -98,7 +98,9 @@ class KeyValueListInput extends Component {
className,
value,
keyPlaceholder,
valuePlaceholder
valuePlaceholder,
hasError,
hasWarning
} = this.props;
const { isFocused } = this.state;
@ -106,7 +108,9 @@ class KeyValueListInput extends Component {
return (
<div className={classNames(
className,
isFocused && styles.isFocused
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
)}
>
{

@ -12,6 +12,14 @@
}
}
.hasError {
composes: hasError from '~Components/Form/Input.css';
}
.hasWarning {
composes: hasWarning from '~Components/Form/Input.css';
}
.internalInput {
flex: 1 1 0%;
margin-left: 3px;

@ -210,6 +210,8 @@ class TagInput extends Component {
const {
className,
inputContainerClassName,
hasError,
hasWarning,
...otherProps
} = this.props;
@ -226,7 +228,9 @@ class TagInput extends Component {
className={styles.internalInput}
inputContainerClassName={classNames(
inputContainerClassName,
isFocused && styles.isFocused
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
)}
value={value}
suggestions={suggestions}

@ -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}
@ -94,17 +100,15 @@ class ReleaseProfile extends Component {
<div>
{
split(ignored).map((item) => {
if (!item) {
return null;
}
preferred.map((item) => {
const isPreferred = item.value >= 0;
return (
<Label
key={item}
kind={kinds.DANGER}
key={item.key}
kind={isPreferred ? kinds.DEFAULT : kinds.WARNING}
>
{item}
{item.key} {isPreferred && '+'}{item.value}
</Label>
);
})
@ -113,15 +117,17 @@ class ReleaseProfile extends Component {
<div>
{
preferred.map((item) => {
const isPreferred = item.value >= 0;
split(ignored).map((item) => {
if (!item) {
return null;
}
return (
<Label
key={item.key}
kind={isPreferred ? kinds.DEFAULT : kinds.WARNING}
key={item}
kind={kinds.DANGER}
>
{item.key} {isPreferred && '+'}{item.value}
{item}
</Label>
);
})
@ -133,6 +139,28 @@ class ReleaseProfile extends Component {
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);

@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
private void GivenRestictions(string required, string ignored)
{
Mocker.GetMock<IReleaseProfileService>()
.Setup(s => s.AllForTags(It.IsAny<HashSet<int>>()))
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
.Returns(new List<ReleaseProfile>
{
new ReleaseProfile()
@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
public void should_be_true_when_restrictions_are_empty()
{
Mocker.GetMock<IReleaseProfileService>()
.Setup(s => s.AllForTags(It.IsAny<HashSet<int>>()))
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
.Returns(new List<ReleaseProfile>());
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
@ -117,7 +117,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
_remoteEpisode.Release.Title = "[ www.Speed.cd ] -Whose.Line.is.it.Anyway.US.S10E24.720p.HDTV.x264-BAJSKORV";
Mocker.GetMock<IReleaseProfileService>()
.Setup(s => s.AllForTags(It.IsAny<HashSet<int>>()))
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
.Returns(new List<ReleaseProfile>
{
new ReleaseProfile { Required = "x264", Ignored = "www.Speed.cd" }

@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService
});
Mocker.GetMock<IReleaseProfileService>()
.Setup(s => s.AllForTags(It.IsAny<HashSet<int>>()))
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
.Returns(_releaseProfiles);
}
@ -52,10 +52,10 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService
public void should_return_0_when_there_are_no_release_profiles()
{
Mocker.GetMock<IReleaseProfileService>()
.Setup(s => s.AllForTags(It.IsAny<HashSet<int>>()))
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<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);
}
}
}

@ -40,12 +40,11 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService
.Setup(s => s.MatchingTerm(It.IsAny<string>(), _title))
.Returns<string, string>((term, title) => title.Contains(term) ? term : null);
}
private void GivenReleaseProfile()
{
Mocker.GetMock<IReleaseProfileService>()
.Setup(s => s.AllForTags(It.IsAny<HashSet<int>>()))
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
.Returns(_releaseProfiles);
}
@ -53,7 +52,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService
public void should_return_empty_list_when_there_are_no_release_profiles()
{
Mocker.GetMock<IReleaseProfileService>()
.Setup(s => s.AllForTags(It.IsAny<HashSet<int>>()))
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
.Returns(new List<ReleaseProfile>());
Subject.GetMatchingPreferredWords(_series, _title).Should().BeEmpty();

@ -16,6 +16,7 @@ namespace NzbDrone.Core.Datastore
{
IEnumerable<TModel> All();
int Count();
TModel Find(int id);
TModel Get(int id);
IEnumerable<TModel> Get(IEnumerable<int> ids);
TModel SingleOrDefault();
@ -65,10 +66,17 @@ namespace NzbDrone.Core.Datastore
return DataMapper.Query<TModel>().GetRowCount();
}
public TModel Get(int id)
public TModel Find(int id)
{
var model = Query.Where(c => c.Id == id).SingleOrDefault();
return model;
}
public TModel Get(int id)
{
var model = Find(id);
if (model == null)
{
throw new ModelNotFoundException(typeof(TModel), id);

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

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

@ -30,10 +30,10 @@ 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)
{

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

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

@ -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 AllForTags(tagIds)
.Where(r => r.Enabled)
.Where(r => r.IndexerId == indexerId || r.IndexerId == 0).ToList();
}
public ReleaseProfile Get(int id)
{
return _repo.Get(id);

@ -9,6 +9,7 @@ namespace NzbDrone.Core.ThingiProvider
{
List<TProviderDefinition> All();
List<TProvider> GetAvailableProviders();
bool Exists(int id);
TProviderDefinition Get(int id);
TProviderDefinition Create(TProviderDefinition definition);
void Update(TProviderDefinition definition);

@ -91,6 +91,11 @@ namespace NzbDrone.Core.ThingiProvider
return Active().Select(GetInstance).ToList();
}
public bool Exists(int id)
{
return _providerRepository.Find(id) != null;
}
public TProviderDefinition Get(int id)
{
return _providerRepository.Get(id);

@ -2,6 +2,7 @@ using System.Collections.Generic;
using FluentValidation;
using FluentValidation.Results;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Profiles.Releases;
using Sonarr.Http;
@ -10,11 +11,13 @@ namespace Sonarr.Api.V3.Profiles.Release
public class ReleaseProfileModule : SonarrRestModule<ReleaseProfileResource>
{
private readonly IReleaseProfileService _releaseProfileService;
private readonly IIndexerFactory _indexerFactory;
public ReleaseProfileModule(IReleaseProfileService releaseProfileService)
public ReleaseProfileModule(IReleaseProfileService releaseProfileService, IIndexerFactory indexerFactory)
{
_releaseProfileService = releaseProfileService;
_indexerFactory = indexerFactory;
GetResourceById = GetReleaseProfile;
GetResourceAll = GetAll;
@ -28,6 +31,11 @@ namespace Sonarr.Api.V3.Profiles.Release
{
context.AddFailure("'Must contain', 'Must not contain' or 'Preferred' is required");
}
if (restriction.Enabled && restriction.IndexerId != 0 && !_indexerFactory.Exists(restriction.IndexerId))
{
context.AddFailure(nameof(ReleaseProfile.IndexerId), "Indexer does not exist");
}
});
}

@ -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…
Cancel
Save