diff --git a/frontend/src/Components/FieldSet.css b/frontend/src/Components/FieldSet.css index c9a567c0e..9ea0dbab1 100644 --- a/frontend/src/Components/FieldSet.css +++ b/frontend/src/Components/FieldSet.css @@ -16,4 +16,9 @@ color: var(--textColor); font-size: 21px; line-height: inherit; + + &.small { + color: #909293; + font-size: 18px; + } } diff --git a/frontend/src/Components/FieldSet.css.d.ts b/frontend/src/Components/FieldSet.css.d.ts index b669ac6d0..74e99779a 100644 --- a/frontend/src/Components/FieldSet.css.d.ts +++ b/frontend/src/Components/FieldSet.css.d.ts @@ -3,6 +3,7 @@ interface CssExports { 'fieldSet': string; 'legend': string; + 'small': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Components/FieldSet.js b/frontend/src/Components/FieldSet.js index 76e68a934..8243fd00c 100644 --- a/frontend/src/Components/FieldSet.js +++ b/frontend/src/Components/FieldSet.js @@ -1,5 +1,7 @@ +import classNames from 'classnames'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { sizes } from 'Helpers/Props'; import styles from './FieldSet.css'; class FieldSet extends Component { @@ -9,13 +11,14 @@ class FieldSet extends Component { render() { const { + size, legend, children } = this.props; return (
- + {legend} {children} @@ -26,8 +29,13 @@ class FieldSet extends Component { } FieldSet.propTypes = { + size: PropTypes.oneOf(sizes.all).isRequired, legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), children: PropTypes.node }; +FieldSet.defaultProps = { + size: sizes.MEDIUM +}; + export default FieldSet; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js index efdbbb4bb..efc894d47 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Alert from 'Components/Alert'; +import FieldSet from 'Components/FieldSet'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -13,7 +14,7 @@ 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 } from 'Helpers/Props'; +import { inputTypes, kinds, sizes } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './EditDownloadClientModalContent.css'; @@ -45,7 +46,10 @@ class EditDownloadClientModalContent extends Component { implementationName, name, enable, + protocol, priority, + removeCompletedDownloads, + removeFailedDownloads, fields, message } = item; @@ -142,6 +146,37 @@ class EditDownloadClientModalContent extends Component { /> +
+ + {translate('RemoveCompleted')} + + + + + { + protocol.value !== 'torrent' && + + {translate('RemoveFailed')} + + + + } +
} diff --git a/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js index f921b5609..bf3850f95 100644 --- a/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js +++ b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js @@ -35,39 +35,23 @@ function DownloadClientOptions(props) { } { - hasSettings && !isFetching && !error && + hasSettings && !isFetching && !error && advancedSettings &&
- - - {translate('Enable')} - - - - - - - {translate('Remove')} - + {translate('Enable')}
@@ -78,9 +62,7 @@ function DownloadClientOptions(props) { >
- - {translate('Redownload')} - + {translate('RedownloadFailed')} - - - - {translate('Remove')} - - - -
+ + {translate('RemoveDownloadsAlert')} +
} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/034_cdh_per_downloadclientFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/034_cdh_per_downloadclientFixture.cs new file mode 100644 index 000000000..f78b5d71c --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/034_cdh_per_downloadclientFixture.cs @@ -0,0 +1,130 @@ +using System.Linq; +using FluentAssertions; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Download.Clients.RTorrent; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class cdh_per_downloadclientFixture : MigrationTest + { + [Test] + public void should_set_cdh_to_enabled() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("DownloadClients").Row(new + { + Enable = true, + Name = "Deluge", + Implementation = "Deluge", + Priority = 1, + Settings = new DelugeSettings34 + { + Host = "127.0.0.1", + TvCategory = "abc", + UrlBase = "/my/" + }.ToJson(), + ConfigContract = "DelugeSettings" + }); + }); + + var items = db.Query("SELECT * FROM \"DownloadClients\""); + + items.Should().HaveCount(1); + items.First().RemoveCompletedDownloads.Should().BeFalse(); + items.First().RemoveFailedDownloads.Should().BeTrue(); + } + + [Test] + public void should_set_cdh_to_disabled_when_globally_disabled() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Config").Row(new + { + Key = "removecompleteddownloads", + Value = "True" + }); + + c.Insert.IntoTable("DownloadClients").Row(new + { + Enable = true, + Name = "Deluge", + Implementation = "Deluge", + Priority = 1, + Settings = new DelugeSettings34 + { + Host = "127.0.0.1", + TvCategory = "abc", + UrlBase = "/my/" + }.ToJson(), + ConfigContract = "DelugeSettings" + }); + }); + + var items = db.Query("SELECT * FROM \"DownloadClients\""); + + items.Should().HaveCount(1); + items.First().RemoveCompletedDownloads.Should().BeTrue(); + items.First().RemoveFailedDownloads.Should().BeTrue(); + } + + [Test] + public void should_disable_remove_for_existing_rtorrent() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("DownloadClients").Row(new + { + Enable = true, + Name = "RTorrent", + Implementation = "RTorrent", + Priority = 1, + Settings = new RTorrentSettings + { + Host = "127.0.0.1", + MusicCategory = "abc", + UrlBase = "/my/" + }.ToJson(), + ConfigContract = "RTorrentSettings" + }); + }); + + var items = db.Query("SELECT * FROM \"DownloadClients\""); + + items.Should().HaveCount(1); + items.First().RemoveCompletedDownloads.Should().BeFalse(); + items.First().RemoveFailedDownloads.Should().BeTrue(); + } + } + + public class DelugeSettings34 + { + public string Host { get; set; } + public int Port { get; set; } + public string UrlBase { get; set; } + public string Password { get; set; } + public string TvCategory { get; set; } + public int RecentTvPriority { get; set; } + public int OlderTvPriority { get; set; } + public bool UseSsl { get; set; } + } + + public class DownloadClientDefinition34 + { + public int Id { get; set; } + public bool Enable { get; set; } + public int Priority { get; set; } + public string Name { get; set; } + public string Implementation { get; set; } + public JObject Settings { get; set; } + public string ConfigContract { get; set; } + public bool RemoveCompletedDownloads { get; set; } + public bool RemoveFailedDownloads { get; set; } + } +} diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 1fcabfdc1..994a33d93 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -137,13 +137,6 @@ namespace NzbDrone.Core.Configuration set { SetValue("EnableCompletedDownloadHandling", value); } } - public bool RemoveCompletedDownloads - { - get { return GetValueBoolean("RemoveCompletedDownloads", false); } - - set { SetValue("RemoveCompletedDownloads", value); } - } - public bool AutoRedownloadFailed { get { return GetValueBoolean("AutoRedownloadFailed", true); } @@ -151,13 +144,6 @@ namespace NzbDrone.Core.Configuration set { SetValue("AutoRedownloadFailed", value); } } - public bool RemoveFailedDownloads - { - get { return GetValueBoolean("RemoveFailedDownloads", true); } - - set { SetValue("RemoveFailedDownloads", value); } - } - public bool CreateEmptyAuthorFolders { get { return GetValueBoolean("CreateEmptyAuthorFolders", false); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index ba697283f..89a107a45 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -18,10 +18,7 @@ namespace NzbDrone.Core.Configuration //Completed/Failed Download Handling (Download client) bool EnableCompletedDownloadHandling { get; set; } - bool RemoveCompletedDownloads { get; set; } - bool AutoRedownloadFailed { get; set; } - bool RemoveFailedDownloads { get; set; } //Media Management bool AutoUnmonitorPreviouslyDownloadedBooks { get; set; } diff --git a/src/NzbDrone.Core/Datastore/Migration/034_cdh_per_downloadclient.cs b/src/NzbDrone.Core/Datastore/Migration/034_cdh_per_downloadclient.cs new file mode 100644 index 000000000..b88345e36 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/034_cdh_per_downloadclient.cs @@ -0,0 +1,48 @@ +using System.Data; +using Dapper; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(158)] + public class cdh_per_downloadclient : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("DownloadClients") + .AddColumn("RemoveCompletedDownloads").AsBoolean().NotNullable().WithDefaultValue(true) + .AddColumn("RemoveFailedDownloads").AsBoolean().NotNullable().WithDefaultValue(true); + + Execute.WithConnection(MoveRemoveSettings); + } + + private void MoveRemoveSettings(IDbConnection conn, IDbTransaction tran) + { + var removeCompletedDownloads = false; + var removeFailedDownloads = true; + + using (var removeCompletedDownloadsCmd = conn.CreateCommand(tran, "SELECT \"Value\" FROM \"Config\" WHERE \"Key\" = 'removecompleteddownloads'")) + { + if ((removeCompletedDownloadsCmd.ExecuteScalar() as string)?.ToLower() == "true") + { + removeCompletedDownloads = true; + } + } + + using (var removeFailedDownloadsCmd = conn.CreateCommand(tran, "SELECT \"Value\" FROM \"Config\" WHERE \"Key\" = 'removefaileddownloads'")) + { + if ((removeFailedDownloadsCmd.ExecuteScalar() as string)?.ToLower() == "false") + { + removeFailedDownloads = false; + } + } + + var parameters = new { RemoveFailedDownloads = removeFailedDownloads, RemoveCompletedDownloads = removeCompletedDownloads }; + var updateSql = $"UPDATE \"DownloadClients\" SET \"RemoveCompletedDownloads\" = (CASE WHEN \"Implementation\" IN ('RTorrent', 'Flood') THEN 'false' ELSE @RemoveCompletedDownloads END), \"RemoveFailedDownloads\" = @RemoveFailedDownloads"; + conn.Execute(updateSql, parameters, transaction: tran); + + conn.Execute("DELETE FROM \"Config\" WHERE \"Key\" IN ('removecompleteddownloads', 'removefaileddownloads')"); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationExtension.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationExtension.cs index 8d936463e..b321a7803 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationExtension.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationExtension.cs @@ -1,4 +1,5 @@ -using FluentMigrator; +using System.Data; +using FluentMigrator; using FluentMigrator.Builders.Create; using FluentMigrator.Builders.Create.Table; using FluentMigrator.Runner; @@ -16,7 +17,16 @@ namespace NzbDrone.Core.Datastore.Migration.Framework return expressionRoot.Table(name).WithColumn("Id").AsInt32().PrimaryKey().Identity(); } - public static void AddParameter(this System.Data.IDbCommand command, object value) + public static IDbCommand CreateCommand(this IDbConnection conn, IDbTransaction tran, string query) + { + var command = conn.CreateCommand(); + command.Transaction = tran; + command.CommandText = query; + + return command; + } + + public static void AddParameter(this IDbCommand command, object value) { var parameter = command.CreateParameter(); parameter.Value = value; diff --git a/src/NzbDrone.Core/Download/DownloadClientDefinition.cs b/src/NzbDrone.Core/Download/DownloadClientDefinition.cs index 1c0dfa927..6de6a4f14 100644 --- a/src/NzbDrone.Core/Download/DownloadClientDefinition.cs +++ b/src/NzbDrone.Core/Download/DownloadClientDefinition.cs @@ -7,5 +7,8 @@ namespace NzbDrone.Core.Download { public DownloadProtocol Protocol { get; set; } public int Priority { get; set; } = 1; + + public bool RemoveCompletedDownloads { get; set; } = true; + public bool RemoveFailedDownloads { get; set; } = true; } } diff --git a/src/NzbDrone.Core/Download/DownloadEventHub.cs b/src/NzbDrone.Core/Download/DownloadEventHub.cs index e0359d765..fd9de5125 100644 --- a/src/NzbDrone.Core/Download/DownloadEventHub.cs +++ b/src/NzbDrone.Core/Download/DownloadEventHub.cs @@ -27,38 +27,65 @@ namespace NzbDrone.Core.Download { var trackedDownload = message.TrackedDownload; - if (trackedDownload == null || !trackedDownload.DownloadItem.CanBeRemoved || _configService.RemoveFailedDownloads == false) + if (trackedDownload == null || + message.TrackedDownload.DownloadItem.Removed || + !trackedDownload.DownloadItem.CanBeRemoved) { return; } - RemoveFromDownloadClient(trackedDownload); + var downloadClient = _downloadClientProvider.Get(message.TrackedDownload.DownloadClient); + var definition = downloadClient.Definition as DownloadClientDefinition; + + if (!definition.RemoveFailedDownloads) + { + return; + } + + RemoveFromDownloadClient(trackedDownload, downloadClient); } public void Handle(DownloadCompletedEvent message) { - if (_configService.RemoveCompletedDownloads && - !message.TrackedDownload.DownloadItem.Removed && - message.TrackedDownload.DownloadItem.CanBeRemoved && - message.TrackedDownload.DownloadItem.Status != DownloadItemStatus.Downloading) + var trackedDownload = message.TrackedDownload; + var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); + var definition = downloadClient.Definition as DownloadClientDefinition; + + MarkItemAsImported(trackedDownload, downloadClient); + + if (trackedDownload.DownloadItem.Removed || + !trackedDownload.DownloadItem.CanBeRemoved || + trackedDownload.DownloadItem.Status == DownloadItemStatus.Downloading) { - RemoveFromDownloadClient(message.TrackedDownload); + return; } - else + + if (!definition.RemoveCompletedDownloads) { - MarkItemAsImported(message.TrackedDownload); + return; } + + RemoveFromDownloadClient(message.TrackedDownload, downloadClient); } public void Handle(DownloadCanBeRemovedEvent message) { - // Already verified that it can be removed, just needs to be removed - RemoveFromDownloadClient(message.TrackedDownload); + var trackedDownload = message.TrackedDownload; + var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); + var definition = downloadClient.Definition as DownloadClientDefinition; + + if (trackedDownload.DownloadItem.Removed || + !trackedDownload.DownloadItem.CanBeRemoved || + !definition.RemoveCompletedDownloads) + { + return; + } + + RemoveFromDownloadClient(message.TrackedDownload, downloadClient); } - private void RemoveFromDownloadClient(TrackedDownload trackedDownload) + private void RemoveFromDownloadClient(TrackedDownload trackedDownload, IDownloadClient downloadClient) { - var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); try { _logger.Debug("[{0}] Removing download from {1} history", trackedDownload.DownloadItem.Title, trackedDownload.DownloadItem.DownloadClientInfo.Name); @@ -75,9 +102,8 @@ namespace NzbDrone.Core.Download } } - private void MarkItemAsImported(TrackedDownload trackedDownload) + private void MarkItemAsImported(TrackedDownload trackedDownload, IDownloadClient downloadClient) { - var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); try { _logger.Debug("[{0}] Marking download as imported from {1}", trackedDownload.DownloadItem.Title, trackedDownload.DownloadItem.DownloadClientInfo.Name); diff --git a/src/NzbDrone.Core/Download/DownloadProcessingService.cs b/src/NzbDrone.Core/Download/DownloadProcessingService.cs index eb18a3184..504b2ceac 100644 --- a/src/NzbDrone.Core/Download/DownloadProcessingService.cs +++ b/src/NzbDrone.Core/Download/DownloadProcessingService.cs @@ -47,7 +47,6 @@ namespace NzbDrone.Core.Download public void Execute(ProcessMonitoredDownloadsCommand message) { var enableCompletedDownloadHandling = _configService.EnableCompletedDownloadHandling; - var removeCompletedDownloads = _configService.RemoveCompletedDownloads; var trackedDownloads = _trackedDownloadService.GetTrackedDownloads() .Where(t => t.IsTrackable) .ToList(); @@ -72,10 +71,7 @@ namespace NzbDrone.Core.Download } // Imported downloads are no longer trackable so process them after processing trackable downloads - if (removeCompletedDownloads) - { - RemoveCompletedDownloads(); - } + RemoveCompletedDownloads(); _eventAggregator.PublishEvent(new DownloadsProcessedEvent()); } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index ce2210206..ec9122884 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -625,6 +625,7 @@ "RecyclingBin": "Recycling Bin", "RecyclingBinCleanup": "Recycling Bin Cleanup", "Redownload": "Redownload", + "RedownloadFailed": "Redownload Failed", "Refresh": "Refresh", "RefreshAndScan": "Refresh & Scan", "RefreshAuthor": "Refresh Author", @@ -656,7 +657,10 @@ "RemotePathMappingCheckWrongOSPath": "Remote download client {0} places downloads in {1} but this is not a valid {2} path. Review your remote path mappings and download client settings.", "RemotePathMappings": "Remote Path Mappings", "Remove": "Remove", + "RemoveCompleted": "Remove Completed", "RemoveCompletedDownloadsHelpText": "Remove imported downloads from download client history", + "RemoveDownloadsAlert": "The Remove settings were moved to the individual Download Client settings in the table above.", + "RemoveFailed": "Remove Failed", "RemoveFailedDownloadsHelpText": "Remove failed downloads from download client history", "RemoveFilter": "Remove filter", "RemoveFromBlocklist": "Remove from blocklist", diff --git a/src/Readarr.Api.V1/Config/DownloadClientConfigResource.cs b/src/Readarr.Api.V1/Config/DownloadClientConfigResource.cs index 5de523239..23239fd3e 100644 --- a/src/Readarr.Api.V1/Config/DownloadClientConfigResource.cs +++ b/src/Readarr.Api.V1/Config/DownloadClientConfigResource.cs @@ -8,10 +8,7 @@ namespace Readarr.Api.V1.Config public string DownloadClientWorkingFolders { get; set; } public bool EnableCompletedDownloadHandling { get; set; } - public bool RemoveCompletedDownloads { get; set; } - public bool AutoRedownloadFailed { get; set; } - public bool RemoveFailedDownloads { get; set; } } public static class DownloadClientConfigResourceMapper @@ -23,10 +20,7 @@ namespace Readarr.Api.V1.Config DownloadClientWorkingFolders = model.DownloadClientWorkingFolders, EnableCompletedDownloadHandling = model.EnableCompletedDownloadHandling, - RemoveCompletedDownloads = model.RemoveCompletedDownloads, - - AutoRedownloadFailed = model.AutoRedownloadFailed, - RemoveFailedDownloads = model.RemoveFailedDownloads + AutoRedownloadFailed = model.AutoRedownloadFailed }; } } diff --git a/src/Readarr.Api.V1/DownloadClient/DownloadClientResource.cs b/src/Readarr.Api.V1/DownloadClient/DownloadClientResource.cs index 4a809f4a5..b9c552273 100644 --- a/src/Readarr.Api.V1/DownloadClient/DownloadClientResource.cs +++ b/src/Readarr.Api.V1/DownloadClient/DownloadClientResource.cs @@ -8,6 +8,8 @@ namespace Readarr.Api.V1.DownloadClient public bool Enable { get; set; } public DownloadProtocol Protocol { get; set; } public int Priority { get; set; } + public bool RemoveCompletedDownloads { get; set; } + public bool RemoveFailedDownloads { get; set; } } public class DownloadClientResourceMapper : ProviderResourceMapper @@ -24,6 +26,8 @@ namespace Readarr.Api.V1.DownloadClient resource.Enable = definition.Enable; resource.Protocol = definition.Protocol; resource.Priority = definition.Priority; + resource.RemoveCompletedDownloads = definition.RemoveCompletedDownloads; + resource.RemoveFailedDownloads = definition.RemoveFailedDownloads; return resource; } @@ -40,6 +44,8 @@ namespace Readarr.Api.V1.DownloadClient definition.Enable = resource.Enable; definition.Protocol = resource.Protocol; definition.Priority = resource.Priority; + definition.RemoveCompletedDownloads = resource.RemoveCompletedDownloads; + definition.RemoveFailedDownloads = resource.RemoveFailedDownloads; return definition; }