1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2024-11-24 19:52:39 +01:00

New: Add tag support to indexers

Closes #487
This commit is contained in:
6cUbi57z 2021-03-22 00:00:06 +00:00 committed by Mark McDowall
parent 14b551b027
commit c3d54b312e
18 changed files with 407 additions and 39 deletions

View File

@ -42,6 +42,7 @@ function EditIndexerModalContent(props) {
enableInteractiveSearch,
supportsRss,
supportsSearch,
tags,
fields,
priority
} = item;
@ -132,6 +133,7 @@ function EditIndexerModalContent(props) {
);
})
}
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
@ -148,6 +150,18 @@ function EditIndexerModalContent(props) {
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Tags</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText="Only use this indexer for series with at least one matching tag. Leave blank to use with all series."
{...tags}
onChange={onInputChange}
/>
</FormGroup>
</Form>
}
</ModalBody>

View File

@ -3,6 +3,7 @@ import React, { Component } from 'react';
import { icons, kinds } from 'Helpers/Props';
import Card from 'Components/Card';
import Label from 'Components/Label';
import TagList from 'Components/TagList';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import EditIndexerModalConnector from './EditIndexerModalConnector';
@ -67,6 +68,8 @@ class Indexer extends Component {
enableRss,
enableAutomaticSearch,
enableInteractiveSearch,
tags,
tagList,
supportsRss,
supportsSearch,
priority,
@ -132,6 +135,11 @@ class Indexer extends Component {
}
</div>
<TagList
tags={tags}
tagList={tagList}
/>
<EditIndexerModalConnector
id={id}
isOpen={this.state.isEditIndexerModalOpen}
@ -160,6 +168,8 @@ Indexer.propTypes = {
enableRss: PropTypes.bool.isRequired,
enableAutomaticSearch: PropTypes.bool.isRequired,
enableInteractiveSearch: PropTypes.bool.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
supportsRss: PropTypes.bool.isRequired,
supportsSearch: PropTypes.bool.isRequired,
showPriority: PropTypes.bool.isRequired,

View File

@ -53,6 +53,7 @@ class Indexers extends Component {
render() {
const {
items,
tagList,
dispatchCloneIndexer,
onConfirmDeleteIndexer,
...otherProps
@ -78,6 +79,7 @@ class Indexers extends Component {
<Indexer
key={item.id}
{...item}
tagList={tagList}
showPriority={showPriority}
onCloneIndexerPress={this.onCloneIndexerPress}
onConfirmDeleteIndexer={onConfirmDeleteIndexer}
@ -118,6 +120,7 @@ Indexers.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchCloneIndexer: PropTypes.func.isRequired,
onConfirmDeleteIndexer: PropTypes.func.isRequired
};

View File

@ -4,13 +4,20 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import sortByName from 'Utilities/Array/sortByName';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import { fetchIndexers, deleteIndexer, cloneIndexer } from 'Store/Actions/settingsActions';
import Indexers from './Indexers';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.indexers', sortByName),
(indexers) => indexers
createTagsSelector(),
(indexers, tagList) => {
return {
...indexers,
tagList
};
}
);
}

View File

@ -21,6 +21,7 @@ function TagDetailsModalContent(props) {
importLists,
notifications,
releaseProfiles,
indexers,
onModalClose,
onDeleteTagPress
} = props;
@ -38,7 +39,7 @@ function TagDetailsModalContent(props) {
}
{
!!series.length &&
series.length ?
<FieldSet legend="Series">
{
series.map((item) => {
@ -49,11 +50,12 @@ function TagDetailsModalContent(props) {
);
})
}
</FieldSet>
</FieldSet> :
null
}
{
!!delayProfiles.length &&
delayProfiles.length ?
<FieldSet legend="Delay Profile">
{
delayProfiles.map((item) => {
@ -78,11 +80,12 @@ function TagDetailsModalContent(props) {
);
})
}
</FieldSet>
</FieldSet> :
null
}
{
!!notifications.length &&
notifications.length ?
<FieldSet legend="Connections">
{
notifications.map((item) => {
@ -93,11 +96,12 @@ function TagDetailsModalContent(props) {
);
})
}
</FieldSet>
</FieldSet> :
null
}
{
!!importLists.length &&
importLists.length ?
<FieldSet legend="Import Lists">
{
importLists.map((item) => {
@ -108,11 +112,12 @@ function TagDetailsModalContent(props) {
);
})
}
</FieldSet>
</FieldSet> :
null
}
{
!!releaseProfiles.length &&
releaseProfiles.length ?
<FieldSet legend="Release Profiles">
{
releaseProfiles.map((item) => {
@ -154,7 +159,24 @@ function TagDetailsModalContent(props) {
);
})
}
</FieldSet>
</FieldSet> :
null
}
{
indexers.length ?
<FieldSet legend="Indexers">
{
indexers.map((item) => {
return (
<div key={item.id}>
{item.name}
</div>
);
})
}
</FieldSet> :
null
}
</ModalBody>
@ -189,6 +211,7 @@ TagDetailsModalContent.propTypes = {
importLists: PropTypes.arrayOf(PropTypes.object).isRequired,
notifications: PropTypes.arrayOf(PropTypes.object).isRequired,
releaseProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteTagPress: PropTypes.func.isRequired
};

View File

@ -69,6 +69,14 @@ function createMatchingReleaseProfilesSelector() {
);
}
function createMatchingIndexersSelector() {
return createSelector(
(state, { indexerIds }) => indexerIds,
(state) => state.settings.indexers.items,
findMatchingItems
);
}
function createMapStateToProps() {
return createSelector(
createMatchingSeriesSelector(),
@ -76,13 +84,15 @@ function createMapStateToProps() {
createMatchingImportListsSelector(),
createMatchingNotificationsSelector(),
createMatchingReleaseProfilesSelector(),
(series, delayProfiles, importLists, notifications, releaseProfiles) => {
createMatchingIndexersSelector(),
(series, delayProfiles, importLists, notifications, releaseProfiles, indexers) => {
return {
series,
delayProfiles,
importLists,
notifications,
releaseProfiles
releaseProfiles,
indexers
};
}
);

View File

@ -56,6 +56,7 @@ class Tag extends Component {
importListIds,
notificationIds,
restrictionIds,
indexerIds,
seriesIds
} = this.props;
@ -69,6 +70,7 @@ class Tag extends Component {
importListIds.length ||
notificationIds.length ||
restrictionIds.length ||
indexerIds.length ||
seriesIds.length
);
@ -124,6 +126,14 @@ class Tag extends Component {
</div> :
null
}
{
indexerIds.length ?
<div>
{indexerIds.length} indexer{indexerIds.length > 1 && 's'}
</div> :
null
}
</div>
}
@ -142,6 +152,7 @@ class Tag extends Component {
importListIds={importListIds}
notificationIds={notificationIds}
restrictionIds={restrictionIds}
indexerIds={indexerIds}
isOpen={isDetailsModalOpen}
onModalClose={this.onDetailsModalClose}
onDeleteTagPress={this.onDeleteTagPress}
@ -168,6 +179,7 @@ Tag.propTypes = {
importListIds: PropTypes.arrayOf(PropTypes.number).isRequired,
notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired,
indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
seriesIds: PropTypes.arrayOf(PropTypes.number).isRequired,
onConfirmDeleteTag: PropTypes.func.isRequired
};
@ -177,6 +189,7 @@ Tag.defaultProps = {
importListIds: [],
notificationIds: [],
restrictionIds: [],
indexerIds: [],
seriesIds: []
};

View File

@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchTagDetails } from 'Store/Actions/tagActions';
import { fetchDelayProfiles, fetchNotifications, fetchReleaseProfiles, fetchImportLists } from 'Store/Actions/settingsActions';
import { fetchDelayProfiles, fetchNotifications, fetchReleaseProfiles, fetchImportLists, fetchIndexers } from 'Store/Actions/settingsActions';
import Tags from './Tags';
function createMapStateToProps() {
@ -29,7 +29,8 @@ const mapDispatchToProps = {
dispatchFetchDelayProfiles: fetchDelayProfiles,
dispatchFetchImportLists: fetchImportLists,
dispatchFetchNotifications: fetchNotifications,
dispatchFetchReleaseProfiles: fetchReleaseProfiles
dispatchFetchReleaseProfiles: fetchReleaseProfiles,
dispatchFetchIndexers: fetchIndexers
};
class MetadatasConnector extends Component {
@ -43,7 +44,8 @@ class MetadatasConnector extends Component {
dispatchFetchDelayProfiles,
dispatchFetchImportLists,
dispatchFetchNotifications,
dispatchFetchReleaseProfiles
dispatchFetchReleaseProfiles,
dispatchFetchIndexers
} = this.props;
dispatchFetchTagDetails();
@ -51,6 +53,7 @@ class MetadatasConnector extends Component {
dispatchFetchImportLists();
dispatchFetchNotifications();
dispatchFetchReleaseProfiles();
dispatchFetchIndexers();
}
//
@ -70,7 +73,8 @@ MetadatasConnector.propTypes = {
dispatchFetchDelayProfiles: PropTypes.func.isRequired,
dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchNotifications: PropTypes.func.isRequired,
dispatchFetchReleaseProfiles: PropTypes.func.isRequired
dispatchFetchReleaseProfiles: PropTypes.func.isRequired,
dispatchFetchIndexers: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);

View File

@ -5,8 +5,8 @@ using NUnit.Framework;
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.DecisionEngineTests
{

View File

@ -0,0 +1,110 @@
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
{
[TestFixture]
public class IndexerTagSpecificationFixture : CoreTest<IndexerTagSpecification>
{
private IndexerTagSpecification _specification;
private RemoteEpisode _parseResultMulti;
private IndexerDefinition _fakeIndexerDefinition;
private Series _fakeSeries;
private Episode _firstEpisode;
private Episode _secondEpisode;
private ReleaseInfo _fakeRelease;
[SetUp]
public void Setup()
{
_fakeIndexerDefinition = new IndexerDefinition
{
Tags = new HashSet<int>()
};
Mocker
.GetMock<IIndexerRepository>()
.Setup(m => m.Get(It.IsAny<int>()))
.Returns(_fakeIndexerDefinition);
_specification = Mocker.Resolve<IndexerTagSpecification>();
_fakeSeries = Builder<Series>.CreateNew()
.With(c => c.Monitored = true)
.With(c => c.Tags = new HashSet<int>())
.Build();
_fakeRelease = new ReleaseInfo
{
IndexerId = 1
};
_firstEpisode = new Episode { Monitored = true };
_secondEpisode = new Episode { Monitored = true };
var doubleEpisodeList = new List<Episode> { _firstEpisode, _secondEpisode };
_parseResultMulti = new RemoteEpisode
{
Series = _fakeSeries,
Episodes = doubleEpisodeList,
Release = _fakeRelease
};
}
[Test]
public void indexer_and_series_without_tags_should_return_true()
{
_fakeIndexerDefinition.Tags = new HashSet<int>();
_fakeSeries.Tags = new HashSet<int>();
_specification.IsSatisfiedBy(_parseResultMulti, new SingleEpisodeSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
}
[Test]
public void indexer_with_tags_series_without_tags_should_return_false()
{
_fakeIndexerDefinition.Tags = new HashSet<int> { 123 };
_fakeSeries.Tags = new HashSet<int>();
_specification.IsSatisfiedBy(_parseResultMulti, new SingleEpisodeSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeFalse();
}
[Test]
public void indexer_without_tags_series_with_tags_should_return_true()
{
_fakeIndexerDefinition.Tags = new HashSet<int>();
_fakeSeries.Tags = new HashSet<int> { 123 };
_specification.IsSatisfiedBy(_parseResultMulti, new SingleEpisodeSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
}
[Test]
public void indexer_with_tags_series_with_matching_tags_should_return_true()
{
_fakeIndexerDefinition.Tags = new HashSet<int> { 123, 456 };
_fakeSeries.Tags = new HashSet<int> { 123, 789 };
_specification.IsSatisfiedBy(_parseResultMulti, new SingleEpisodeSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
}
[Test]
public void indexer_with_tags_series_with_different_tags_should_return_false()
{
_fakeIndexerDefinition.Tags = new HashSet<int> { 456 };
_fakeSeries.Tags = new HashSet<int> { 123, 789 };
_specification.IsSatisfiedBy(_parseResultMulti, new SingleEpisodeSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeFalse();
}
}
}

View File

@ -135,6 +135,114 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
return result;
}
[Test]
public void Tags_IndexerTags_SeriesNoTags_IndexerNotIncluded()
{
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition {
Id = 1,
Tags = new HashSet<int> { 3 }
});
WithEpisodes();
var allCriteria = WatchForSearchCriteria();
Subject.EpisodeSearch(_xemEpisodes.First(), true, false);
var criteria = allCriteria.OfType<SingleEpisodeSearchCriteria>().ToList();
criteria.Count.Should().Be(0);
}
[Test]
public void Tags_IndexerNoTags_SeriesTags_IndexerIncluded()
{
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition
{
Id = 1
});
_xemSeries = Builder<Series>.CreateNew()
.With(v => v.UseSceneNumbering = true)
.With(v => v.Monitored = true)
.With(v => v.Tags = new HashSet<int> { 3 })
.Build();
Mocker.GetMock<ISeriesService>()
.Setup(v => v.GetSeries(_xemSeries.Id))
.Returns(_xemSeries);
WithEpisodes();
var allCriteria = WatchForSearchCriteria();
Subject.EpisodeSearch(_xemEpisodes.First(), true, false);
var criteria = allCriteria.OfType<SingleEpisodeSearchCriteria>().ToList();
criteria.Count.Should().Be(1);
}
[Test]
public void Tags_IndexerAndSeriesTagsMatch_IndexerIncluded()
{
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition
{
Id = 1,
Tags = new HashSet<int> { 1, 2, 3 }
});
_xemSeries = Builder<Series>.CreateNew()
.With(v => v.UseSceneNumbering = true)
.With(v => v.Monitored = true)
.With(v => v.Tags = new HashSet<int> { 3, 4, 5 })
.Build();
Mocker.GetMock<ISeriesService>()
.Setup(v => v.GetSeries(_xemSeries.Id))
.Returns(_xemSeries);
WithEpisodes();
var allCriteria = WatchForSearchCriteria();
Subject.EpisodeSearch(_xemEpisodes.First(), true, false);
var criteria = allCriteria.OfType<SingleEpisodeSearchCriteria>().ToList();
criteria.Count.Should().Be(1);
}
[Test]
public void Tags_IndexerAndSeriesTagsMismatch_IndexerNotIncluded()
{
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition
{
Id = 1,
Tags = new HashSet<int> { 1, 2, 3 }
});
_xemSeries = Builder<Series>.CreateNew()
.With(v => v.UseSceneNumbering = true)
.With(v => v.Monitored = true)
.With(v => v.Tags = new HashSet<int> { 4, 5, 6 })
.Build();
Mocker.GetMock<ISeriesService>()
.Setup(v => v.GetSeries(_xemSeries.Id))
.Returns(_xemSeries);
WithEpisodes();
var allCriteria = WatchForSearchCriteria();
Subject.EpisodeSearch(_xemEpisodes.First(), true, false);
var criteria = allCriteria.OfType<SingleEpisodeSearchCriteria>().ToList();
criteria.Count.Should().Be(0);
}
[Test]
public void scene_episodesearch()
{

View File

@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(159)]
public class add_indexer_tags : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Indexers").AddColumn("Tags").AsString().Nullable();
}
}
}

View File

@ -66,8 +66,7 @@ namespace NzbDrone.Core.Datastore
.Ignore(i => i.Enable)
.Ignore(i => i.Protocol)
.Ignore(i => i.SupportsRss)
.Ignore(i => i.SupportsSearch)
.Ignore(d => d.Tags);
.Ignore(i => i.SupportsSearch);
Mapper.Entity<NotificationDefinition>().RegisterDefinition("Notifications")
.Ignore(i => i.SupportsOnGrab)

View File

@ -0,0 +1,39 @@
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
{
public class IndexerTagSpecification : IDecisionEngineSpecification
{
private readonly Logger _logger;
private readonly IIndexerRepository _indexerRepository;
public IndexerTagSpecification(Logger logger, IIndexerRepository indexerRepository)
{
_logger = logger;
_indexerRepository = indexerRepository;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
{
// If indexer has tags, check that at least one of them is present on the series
var indexerTags = _indexerRepository.Get(subject.Release.IndexerId).Tags;
if (indexerTags.Any() && indexerTags.Intersect(subject.Series.Tags).Empty())
{
_logger.Debug("Indexer {0} has tags. None of these are present on series {1}. Rejecting", subject.Release.Indexer, subject.Series);
return Decision.Reject("Series tags do not match any of the indexer tags");
}
return Decision.Accept();
}
}
}

View File

@ -503,6 +503,9 @@ namespace NzbDrone.Core.IndexerSearch
_indexerFactory.InteractiveSearchEnabled() :
_indexerFactory.AutomaticSearchEnabled();
// Filter indexers to untagged indexers and indexers with intersecting tags
indexers = indexers.Where(i => i.Definition.Tags.Empty() || i.Definition.Tags.Intersect(criteriaBase.Series.Tags).Any()).ToList();
var reports = new List<ReleaseInfo>();
_logger.ProgressInfo("Searching {0} indexers for {1}", indexers.Count, criteriaBase);

View File

@ -12,12 +12,13 @@ namespace NzbDrone.Core.Tags
public List<int> RestrictionIds { get; set; }
public List<int> DelayProfileIds { get; set; }
public List<int> ImportListIds { get; set; }
public List<int> IndexerIds { get; set; }
public bool InUse
{
get
{
return (SeriesIds.Any() || NotificationIds.Any() || RestrictionIds.Any() || DelayProfileIds.Any() || ImportListIds.Any());
return (SeriesIds.Any() || NotificationIds.Any() || RestrictionIds.Any() || DelayProfileIds.Any() || ImportListIds.Any() || IndexerIds.Any());
}
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.ImportLists;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Notifications;
using NzbDrone.Core.Profiles.Delay;
@ -33,6 +34,7 @@ namespace NzbDrone.Core.Tags
private readonly INotificationFactory _notificationFactory;
private readonly IReleaseProfileService _releaseProfileService;
private readonly ISeriesService _seriesService;
private readonly IIndexerFactory _indexerService;
public TagService(ITagRepository repo,
IEventAggregator eventAggregator,
@ -40,7 +42,8 @@ namespace NzbDrone.Core.Tags
IImportListFactory importListFactory,
INotificationFactory notificationFactory,
IReleaseProfileService releaseProfileService,
ISeriesService seriesService)
ISeriesService seriesService,
IIndexerFactory indexerService)
{
_repo = repo;
_eventAggregator = eventAggregator;
@ -49,6 +52,7 @@ namespace NzbDrone.Core.Tags
_notificationFactory = notificationFactory;
_releaseProfileService = releaseProfileService;
_seriesService = seriesService;
_indexerService = indexerService;
}
public Tag GetTag(int tagId)
@ -81,16 +85,18 @@ namespace NzbDrone.Core.Tags
var notifications = _notificationFactory.AllForTag(tagId);
var restrictions = _releaseProfileService.AllForTag(tagId);
var series = _seriesService.AllForTag(tagId);
var indexers = _indexerService.AllForTag(tagId);
return new TagDetails
{
Id = tagId,
Label = tag.Label,
DelayProfileIds = delayProfiles.Select(c => c.Id).ToList(),
ImportListIds = importLists.Select(c => c.Id).ToList(),
NotificationIds = notifications.Select(c => c.Id).ToList(),
RestrictionIds = restrictions.Select(c => c.Id).ToList(),
SeriesIds = series.Select(c => c.Id).ToList()
{
Id = tagId,
Label = tag.Label,
DelayProfileIds = delayProfiles.Select(c => c.Id).ToList(),
ImportListIds = importLists.Select(c => c.Id).ToList(),
NotificationIds = notifications.Select(c => c.Id).ToList(),
RestrictionIds = restrictions.Select(c => c.Id).ToList(),
SeriesIds = series.Select(c => c.Id).ToList(),
IndexerIds = indexers.Select(c => c.Id).ToList()
};
}
@ -102,21 +108,23 @@ namespace NzbDrone.Core.Tags
var notifications = _notificationFactory.All();
var restrictions = _releaseProfileService.All();
var series = _seriesService.GetAllSeries();
var indexers = _indexerService.All();
var details = new List<TagDetails>();
foreach (var tag in tags)
{
details.Add(new TagDetails
{
Id = tag.Id,
Label = tag.Label,
DelayProfileIds = delayProfiles.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
ImportListIds = importLists.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
NotificationIds = notifications.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
RestrictionIds = restrictions.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
SeriesIds = series.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList()
}
{
Id = tag.Id,
Label = tag.Label,
DelayProfileIds = delayProfiles.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
ImportListIds = importLists.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
NotificationIds = notifications.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
RestrictionIds = restrictions.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
SeriesIds = series.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
IndexerIds = indexers.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList()
}
);
}

View File

@ -12,6 +12,7 @@ namespace Sonarr.Api.V3.Tags
public List<int> ImportListIds { get; set; }
public List<int> NotificationIds { get; set; }
public List<int> RestrictionIds { get; set; }
public List<int> IndexerIds { get; set; }
public List<int> SeriesIds { get; set; }
}
@ -29,6 +30,7 @@ namespace Sonarr.Api.V3.Tags
ImportListIds = model.ImportListIds,
NotificationIds = model.NotificationIds,
RestrictionIds = model.RestrictionIds,
IndexerIds = model.IndexerIds,
SeriesIds = model.SeriesIds
};
}