diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js index 84ae2fd85..4f0b7e3df 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import Card from 'Components/Card'; import Label from 'Components/Label'; import ConfirmModal from 'Components/Modal/ConfirmModal'; +import TagList from 'Components/TagList'; import { kinds } from 'Helpers/Props'; import EditDownloadClientModalConnector from './EditDownloadClientModalConnector'; import styles from './DownloadClient.css'; @@ -55,7 +56,9 @@ class DownloadClient extends Component { id, name, enable, - priority + priority, + tags, + tagList } = this.props; return ( @@ -93,6 +96,11 @@ class DownloadClient extends Component { } + + ); @@ -108,6 +110,7 @@ DownloadClients.propTypes = { isFetching: PropTypes.bool.isRequired, error: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, + tagList: PropTypes.arrayOf(PropTypes.object).isRequired, onConfirmDeleteDownloadClient: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js index 9cba9c1cc..d9e543469 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js @@ -4,13 +4,20 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; import sortByName from 'Utilities/Array/sortByName'; import DownloadClients from './DownloadClients'; function createMapStateToProps() { return createSelector( createSortedSectionSelector('settings.downloadClients', sortByName), - (downloadClients) => downloadClients + createTagsSelector(), + (downloadClients, tagList) => { + return { + ...downloadClients, + tagList + }; + } ); } diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js index 05c1e3ead..f6ed3c118 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js @@ -50,6 +50,7 @@ class EditDownloadClientModalContent extends Component { removeCompletedDownloads, removeFailedDownloads, fields, + tags, message } = item; @@ -137,6 +138,18 @@ class EditDownloadClientModalContent extends Component { /> + + Tags + + + +
{ + setIsTagsModalOpen(true); + }, [setIsTagsModalOpen]); + + const onTagsModalClose = useCallback(() => { + setIsTagsModalOpen(false); + }, [setIsTagsModalOpen]); + + const onApplyTagsPress = useCallback( + (tags: number[], applyTags: string) => { + setIsSavingTags(true); + setIsTagsModalOpen(false); + + dispatch( + bulkEditDownloadClients({ + ids: selectedIds, + tags, + applyTags, + }) + ); + }, + [selectedIds, dispatch] + ); + const onSelectAllChange = useCallback( ({ value }: SelectStateInputProps) => { setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); @@ -213,6 +246,14 @@ function ManageDownloadClientsModalContent( > Edit + + + Set Tags + @@ -225,6 +266,13 @@ function ManageDownloadClientsModalContent( downloadClientIds={selectedIds} /> + + {removeFailedDownloads ? 'Yes' : 'No'} + + + + ); } diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModal.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModal.tsx new file mode 100644 index 000000000..2e24d60e8 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModal.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import TagsModalContent from './TagsModalContent'; + +interface TagsModalProps { + isOpen: boolean; + ids: number[]; + onApplyTagsPress: (tags: number[], applyTags: string) => void; + onModalClose: () => void; +} + +function TagsModal(props: TagsModalProps) { + const { isOpen, onModalClose, ...otherProps } = props; + + return ( + + + + ); +} + +export default TagsModal; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.css b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.css new file mode 100644 index 000000000..63be9aadd --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.css @@ -0,0 +1,12 @@ +.renameIcon { + margin-left: 5px; +} + +.message { + margin-top: 20px; + margin-bottom: 10px; +} + +.result { + padding-top: 4px; +} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.css.d.ts b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.css.d.ts new file mode 100644 index 000000000..9b4321dcc --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.css.d.ts @@ -0,0 +1,9 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'message': string; + 'renameIcon': string; + 'result': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.tsx new file mode 100644 index 000000000..e8885596f --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.tsx @@ -0,0 +1,180 @@ +import { uniq } from 'lodash'; +import React, { useCallback, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import { DownloadClientAppState } from 'App/State/SettingsAppState'; +import { Tag } from 'App/State/TagsAppState'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Label from 'Components/Label'; +import Button from 'Components/Link/Button'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { inputTypes, kinds, sizes } from 'Helpers/Props'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import DownloadClient from 'typings/DownloadClient'; +import styles from './TagsModalContent.css'; + +interface TagsModalContentProps { + ids: number[]; + onApplyTagsPress: (tags: number[], applyTags: string) => void; + onModalClose: () => void; +} + +function TagsModalContent(props: TagsModalContentProps) { + const { ids, onModalClose, onApplyTagsPress } = props; + + const allDownloadClients: DownloadClientAppState = useSelector( + (state: AppState) => state.settings.downloadClients + ); + const tagList: Tag[] = useSelector(createTagsSelector()); + + const [tags, setTags] = useState([]); + const [applyTags, setApplyTags] = useState('add'); + + const seriesTags = useMemo(() => { + const tags = ids.reduce((acc: number[], id) => { + const s = allDownloadClients.items.find( + (s: DownloadClient) => s.id === id + ); + + if (s) { + acc.push(...s.tags); + } + + return acc; + }, []); + + return uniq(tags); + }, [ids, allDownloadClients]); + + const onTagsChange = useCallback( + ({ value }: { value: number[] }) => { + setTags(value); + }, + [setTags] + ); + + const onApplyTagsChange = useCallback( + ({ value }: { value: string }) => { + setApplyTags(value); + }, + [setApplyTags] + ); + + const onApplyPress = useCallback(() => { + onApplyTagsPress(tags, applyTags); + }, [tags, applyTags, onApplyTagsPress]); + + const applyTagsOptions = [ + { key: 'add', value: 'Add' }, + { key: 'remove', value: 'Remove' }, + { key: 'replace', value: 'Replace' }, + ]; + + return ( + + Tags + + +
+ + Tags + + + + + + Apply Tags + + + + + + Result + +
+ {seriesTags.map((id) => { + const tag = tagList.find((t) => t.id === id); + + if (!tag) { + return null; + } + + const removeTag = + (applyTags === 'remove' && tags.indexOf(id) > -1) || + (applyTags === 'replace' && tags.indexOf(id) === -1); + + return ( + + ); + })} + + {(applyTags === 'add' || applyTags === 'replace') && + tags.map((id) => { + const tag = tagList.find((t) => t.id === id); + + if (!tag) { + return null; + } + + if (seriesTags.indexOf(id) > -1) { + return null; + } + + return ( + + ); + })} +
+
+
+
+ + + + + + +
+ ); +} + +export default TagsModalContent; diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js index 6f4d8f02c..af05ce3de 100644 --- a/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js +++ b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js @@ -21,6 +21,7 @@ function TagDetailsModalContent(props) { notifications, releaseProfiles, indexers, + downloadClients, autoTags, onModalClose, onDeleteTagPress @@ -179,6 +180,22 @@ function TagDetailsModalContent(props) { null } + { + downloadClients.length ? +
+ { + downloadClients.map((item) => { + return ( +
+ {item.name} +
+ ); + }) + } +
: + null + } + { autoTags.length ?
@@ -228,6 +245,7 @@ TagDetailsModalContent.propTypes = { notifications: PropTypes.arrayOf(PropTypes.object).isRequired, releaseProfiles: PropTypes.arrayOf(PropTypes.object).isRequired, indexers: PropTypes.arrayOf(PropTypes.object).isRequired, + downloadClients: PropTypes.arrayOf(PropTypes.object).isRequired, autoTags: PropTypes.arrayOf(PropTypes.object).isRequired, onModalClose: PropTypes.func.isRequired, onDeleteTagPress: PropTypes.func.isRequired diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js b/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js index ce138b5f7..e33a958f7 100644 --- a/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js +++ b/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js @@ -77,6 +77,14 @@ function createMatchingIndexersSelector() { ); } +function createMatchingDownloadClientsSelector() { + return createSelector( + (state, { downloadClientIds }) => downloadClientIds, + (state) => state.settings.downloadClients.items, + findMatchingItems + ); +} + function createMatchingAutoTagsSelector() { return createSelector( (state, { autoTagIds }) => autoTagIds, @@ -93,8 +101,9 @@ function createMapStateToProps() { createMatchingNotificationsSelector(), createMatchingReleaseProfilesSelector(), createMatchingIndexersSelector(), + createMatchingDownloadClientsSelector(), createMatchingAutoTagsSelector(), - (series, delayProfiles, importLists, notifications, releaseProfiles, indexers, autoTags) => { + (series, delayProfiles, importLists, notifications, releaseProfiles, indexers, downloadClients, autoTags) => { return { series, delayProfiles, @@ -102,6 +111,7 @@ function createMapStateToProps() { notifications, releaseProfiles, indexers, + downloadClients, autoTags }; } diff --git a/frontend/src/Settings/Tags/Tag.js b/frontend/src/Settings/Tags/Tag.js index b16a58f75..9764f6f05 100644 --- a/frontend/src/Settings/Tags/Tag.js +++ b/frontend/src/Settings/Tags/Tag.js @@ -58,6 +58,7 @@ class Tag extends Component { notificationIds, restrictionIds, indexerIds, + downloadClientIds, autoTagIds, seriesIds } = this.props; @@ -73,6 +74,7 @@ class Tag extends Component { notificationIds.length || restrictionIds.length || indexerIds.length || + downloadClientIds.length || autoTagIds.length || seriesIds.length ); @@ -121,6 +123,11 @@ class Tag extends Component { count={indexerIds.length} /> + + WithUsenetClient(int priority = 0) + private Mock WithUsenetClient(int priority = 0, HashSet tags = null) { var mock = new Mock(MockBehavior.Default); mock.SetupGet(s => s.Definition) @@ -42,6 +42,7 @@ namespace NzbDrone.Core.Test.Download .CreateNew() .With(v => v.Id = _nextId++) .With(v => v.Priority = priority) + .With(v => v.Tags = tags ?? new HashSet()) .Build()); _downloadClients.Add(mock.Object); @@ -51,7 +52,7 @@ namespace NzbDrone.Core.Test.Download return mock; } - private Mock WithTorrentClient(int priority = 0) + private Mock WithTorrentClient(int priority = 0, HashSet tags = null) { var mock = new Mock(MockBehavior.Default); mock.SetupGet(s => s.Definition) @@ -59,6 +60,7 @@ namespace NzbDrone.Core.Test.Download .CreateNew() .With(v => v.Id = _nextId++) .With(v => v.Priority = priority) + .With(v => v.Tags = tags ?? new HashSet()) .Build()); _downloadClients.Add(mock.Object); @@ -148,6 +150,69 @@ namespace NzbDrone.Core.Test.Download client4.Definition.Id.Should().Be(2); } + [Test] + public void should_roundrobin_over_clients_with_matching_tags() + { + var seriesTags = new HashSet { 1 }; + var clientTags = new HashSet { 1 }; + + WithTorrentClient(); + WithTorrentClient(0, clientTags); + WithTorrentClient(); + WithTorrentClient(0, clientTags); + + var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); + var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); + var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); + var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); + + client1.Definition.Id.Should().Be(2); + client2.Definition.Id.Should().Be(4); + client3.Definition.Id.Should().Be(2); + client4.Definition.Id.Should().Be(4); + } + + [Test] + public void should_roundrobin_over_non_tagged_when_no_matching_tags() + { + var seriesTags = new HashSet { 2 }; + var clientTags = new HashSet { 1 }; + + WithTorrentClient(); + WithTorrentClient(0, clientTags); + WithTorrentClient(); + WithTorrentClient(0, clientTags); + + var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); + var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); + var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); + var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); + + client1.Definition.Id.Should().Be(1); + client2.Definition.Id.Should().Be(3); + client3.Definition.Id.Should().Be(1); + client4.Definition.Id.Should().Be(3); + } + + [Test] + public void should_fail_to_choose_when_clients_have_tags_but_no_match() + { + var seriesTags = new HashSet { 2 }; + var clientTags = new HashSet { 1 }; + + WithTorrentClient(0, clientTags); + WithTorrentClient(0, clientTags); + WithTorrentClient(0, clientTags); + WithTorrentClient(0, clientTags); + + var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); + var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); + var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); + var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); + + Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags).Should().BeNull(); + } + [Test] public void should_skip_blocked_torrent_client() { @@ -162,7 +227,6 @@ namespace NzbDrone.Core.Test.Download var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent); - var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent); client1.Definition.Id.Should().Be(2); client2.Definition.Id.Should().Be(4); diff --git a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs index 7a834911c..8666c81ac 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs @@ -32,8 +32,8 @@ namespace NzbDrone.Core.Test.Download .Returns(_downloadClients); Mocker.GetMock() - .Setup(v => v.GetDownloadClient(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((v, i, f) => _downloadClients.FirstOrDefault(d => d.Protocol == v)); + .Setup(v => v.GetDownloadClient(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns>((v, i, f, t) => _downloadClients.FirstOrDefault(d => d.Protocol == v)); var episodes = Builder.CreateListOfSize(2) .TheFirst(1).With(s => s.Id = 12) diff --git a/src/NzbDrone.Core/Datastore/Migration/191_add_download_client_tags.cs b/src/NzbDrone.Core/Datastore/Migration/191_add_download_client_tags.cs new file mode 100644 index 000000000..4d6a8c746 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/191_add_download_client_tags.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(191)] + public class add_download_client_tags : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("DownloadClients").AddColumn("Tags").AsString().Nullable(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 1ce954022..c2aa5230b 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -102,8 +102,7 @@ namespace NzbDrone.Core.Datastore Mapper.Entity("DownloadClients").RegisterModel() .Ignore(x => x.ImplementationName) - .Ignore(d => d.Protocol) - .Ignore(d => d.Tags); + .Ignore(d => d.Protocol); Mapper.Entity("SceneMappings").RegisterModel(); diff --git a/src/NzbDrone.Core/Download/DownloadClientProvider.cs b/src/NzbDrone.Core/Download/DownloadClientProvider.cs index 370b66dea..8e8bcf8be 100644 --- a/src/NzbDrone.Core/Download/DownloadClientProvider.cs +++ b/src/NzbDrone.Core/Download/DownloadClientProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Download.Clients; using NzbDrone.Core.Indexers; @@ -9,7 +10,7 @@ namespace NzbDrone.Core.Download { public interface IProvideDownloadClient { - IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol, int indexerId = 0, bool filterBlockedClients = false); + IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol, int indexerId = 0, bool filterBlockedClients = false, HashSet tags = null); IEnumerable GetDownloadClients(bool filterBlockedClients = false); IDownloadClient Get(int id); } @@ -35,11 +36,20 @@ namespace NzbDrone.Core.Download _lastUsedDownloadClient = cacheManager.GetCache(GetType(), "lastDownloadClientId"); } - public IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol, int indexerId = 0, bool filterBlockedClients = false) + public IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol, int indexerId = 0, bool filterBlockedClients = false, HashSet tags = null) { var blockedProviders = new HashSet(_downloadClientStatusService.GetBlockedProviders().Select(v => v.ProviderId)); var availableProviders = _downloadClientFactory.GetAvailableProviders().Where(v => v.Protocol == downloadProtocol).ToList(); + if (tags != null) + { + var matchingTagsClients = availableProviders.Where(i => i.Definition.Tags.Intersect(tags).Any()).ToList(); + + availableProviders = matchingTagsClients.Count > 0 ? + matchingTagsClients : + availableProviders.Where(i => i.Definition.Tags.Empty()).ToList(); + } + if (!availableProviders.Any()) { return null; diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs index be09564b7..d07041ad5 100644 --- a/src/NzbDrone.Core/Download/DownloadService.cs +++ b/src/NzbDrone.Core/Download/DownloadService.cs @@ -53,9 +53,11 @@ namespace NzbDrone.Core.Download { var filterBlockedClients = remoteEpisode.Release.PendingReleaseReason == PendingReleaseReason.DownloadClientUnavailable; + var tags = remoteEpisode.Series?.Tags; + var downloadClient = downloadClientId.HasValue ? _downloadClientProvider.Get(downloadClientId.Value) - : _downloadClientProvider.GetDownloadClient(remoteEpisode.Release.DownloadProtocol, remoteEpisode.Release.IndexerId, filterBlockedClients); + : _downloadClientProvider.GetDownloadClient(remoteEpisode.Release.DownloadProtocol, remoteEpisode.Release.IndexerId, filterBlockedClients, tags); DownloadReport(remoteEpisode, downloadClient); } diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs index e0474d977..60f403e9e 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs @@ -24,19 +24,19 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc { private readonly Logger _logger; private readonly IMapCoversToLocal _mediaCoverService; - private readonly ITagService _tagService; + private readonly ITagRepository _tagRepo; private readonly IDetectXbmcNfo _detectNfo; private readonly IDiskProvider _diskProvider; public XbmcMetadata(IDetectXbmcNfo detectNfo, IDiskProvider diskProvider, IMapCoversToLocal mediaCoverService, - ITagService tagService, + ITagRepository tagRepo, Logger logger) { _logger = logger; _mediaCoverService = mediaCoverService; - _tagService = tagService; + _tagRepo = tagRepo; _diskProvider = diskProvider; _detectNfo = detectNfo; } @@ -183,7 +183,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc if (series.Tags.Any()) { - var tags = _tagService.GetTags(series.Tags); + var tags = _tagRepo.Get(series.Tags); foreach (var tag in tags) { diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs index 5a548c786..27e5eccef 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { using var mapper = _database.OpenConnection(); - var usedTags = new[] { "Series", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists", "Indexers", "AutoTagging" } + var usedTags = new[] { "Series", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists", "Indexers", "AutoTagging", "DownloadClients" } .SelectMany(v => GetUsedTags(v, mapper)) .Distinct() .ToArray(); diff --git a/src/NzbDrone.Core/Tags/TagDetails.cs b/src/NzbDrone.Core/Tags/TagDetails.cs index 387f4db47..d571db5bb 100644 --- a/src/NzbDrone.Core/Tags/TagDetails.cs +++ b/src/NzbDrone.Core/Tags/TagDetails.cs @@ -14,7 +14,15 @@ namespace NzbDrone.Core.Tags public List ImportListIds { get; set; } public List IndexerIds { get; set; } public List AutoTagIds { get; set; } + public List DownloadClientIds { get; set; } - public bool InUse => SeriesIds.Any() || NotificationIds.Any() || RestrictionIds.Any() || DelayProfileIds.Any() || ImportListIds.Any() || IndexerIds.Any() || AutoTagIds.Any(); + public bool InUse => SeriesIds.Any() || + NotificationIds.Any() || + RestrictionIds.Any() || + DelayProfileIds.Any() || + ImportListIds.Any() || + IndexerIds.Any() || + AutoTagIds.Any() || + DownloadClientIds.Any(); } } diff --git a/src/NzbDrone.Core/Tags/TagService.cs b/src/NzbDrone.Core/Tags/TagService.cs index 8e28e3f35..da67f0705 100644 --- a/src/NzbDrone.Core/Tags/TagService.cs +++ b/src/NzbDrone.Core/Tags/TagService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Core.AutoTagging; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Download; using NzbDrone.Core.ImportLists; using NzbDrone.Core.Indexers; using NzbDrone.Core.Messaging.Events; @@ -36,6 +37,7 @@ namespace NzbDrone.Core.Tags private readonly ISeriesService _seriesService; private readonly IIndexerFactory _indexerService; private readonly IAutoTaggingService _autoTaggingService; + private readonly IDownloadClientFactory _downloadClientFactory; public TagService(ITagRepository repo, IEventAggregator eventAggregator, @@ -45,7 +47,8 @@ namespace NzbDrone.Core.Tags IReleaseProfileService releaseProfileService, ISeriesService seriesService, IIndexerFactory indexerService, - IAutoTaggingService autoTaggingService) + IAutoTaggingService autoTaggingService, + IDownloadClientFactory downloadClientFactory) { _repo = repo; _eventAggregator = eventAggregator; @@ -56,6 +59,7 @@ namespace NzbDrone.Core.Tags _seriesService = seriesService; _indexerService = indexerService; _autoTaggingService = autoTaggingService; + _downloadClientFactory = downloadClientFactory; } public Tag GetTag(int tagId) @@ -90,6 +94,7 @@ namespace NzbDrone.Core.Tags var series = _seriesService.AllForTag(tagId); var indexers = _indexerService.AllForTag(tagId); var autoTags = _autoTaggingService.AllForTag(tagId); + var downloadClients = _downloadClientFactory.AllForTag(tagId); return new TagDetails { @@ -101,7 +106,8 @@ namespace NzbDrone.Core.Tags RestrictionIds = restrictions.Select(c => c.Id).ToList(), SeriesIds = series.Select(c => c.Id).ToList(), IndexerIds = indexers.Select(c => c.Id).ToList(), - AutoTagIds = autoTags.Select(c => c.Id).ToList() + AutoTagIds = autoTags.Select(c => c.Id).ToList(), + DownloadClientIds = downloadClients.Select(c => c.Id).ToList() }; } @@ -115,6 +121,7 @@ namespace NzbDrone.Core.Tags var series = _seriesService.GetAllSeriesTags(); var indexers = _indexerService.All(); var autotags = _autoTaggingService.All(); + var downloadClients = _downloadClientFactory.All(); var details = new List(); @@ -131,6 +138,7 @@ namespace NzbDrone.Core.Tags SeriesIds = series.Where(c => c.Value.Contains(tag.Id)).Select(c => c.Key).ToList(), IndexerIds = indexers.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), AutoTagIds = autotags.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), + DownloadClientIds = downloadClients.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), }); } diff --git a/src/Sonarr.Api.V3/Tags/TagDetailsResource.cs b/src/Sonarr.Api.V3/Tags/TagDetailsResource.cs index 104cc31a3..0c40370c4 100644 --- a/src/Sonarr.Api.V3/Tags/TagDetailsResource.cs +++ b/src/Sonarr.Api.V3/Tags/TagDetailsResource.cs @@ -13,6 +13,7 @@ namespace Sonarr.Api.V3.Tags public List NotificationIds { get; set; } public List RestrictionIds { get; set; } public List IndexerIds { get; set; } + public List DownloadClientIds { get; set; } public List AutoTagIds { get; set; } public List SeriesIds { get; set; } } @@ -35,6 +36,7 @@ namespace Sonarr.Api.V3.Tags NotificationIds = model.NotificationIds, RestrictionIds = model.RestrictionIds, IndexerIds = model.IndexerIds, + DownloadClientIds = model.DownloadClientIds, AutoTagIds = model.AutoTagIds, SeriesIds = model.SeriesIds };