Compare commits

...

4 Commits

25 changed files with 484 additions and 139 deletions

View File

@ -16,4 +16,9 @@
color: #3a3f51; color: #3a3f51;
font-size: 21px; font-size: 21px;
line-height: inherit; line-height: inherit;
&.small {
color: #909293;
font-size: 18px;
}
} }

View File

@ -1,5 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import classNames from 'classnames';
import { sizes } from 'Helpers/Props';
import styles from './FieldSet.css'; import styles from './FieldSet.css';
class FieldSet extends Component { class FieldSet extends Component {
@ -9,13 +11,14 @@ class FieldSet extends Component {
render() { render() {
const { const {
size,
legend, legend,
children children
} = this.props; } = this.props;
return ( return (
<fieldset className={styles.fieldSet}> <fieldset className={styles.fieldSet}>
<legend className={styles.legend}> <legend className={classNames(styles.legend, (size === sizes.SMALL) && styles.small)}>
{legend} {legend}
</legend> </legend>
{children} {children}
@ -26,8 +29,13 @@ class FieldSet extends Component {
} }
FieldSet.propTypes = { FieldSet.propTypes = {
size: PropTypes.oneOf(sizes.all).isRequired,
legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
children: PropTypes.node children: PropTypes.node
}; };
FieldSet.defaultProps = {
size: sizes.MEDIUM
};
export default FieldSet; export default FieldSet;

View File

@ -1,6 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { inputTypes, kinds } from 'Helpers/Props'; import { inputTypes, kinds, sizes } from 'Helpers/Props';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
@ -9,6 +9,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel'; import FormLabel from 'Components/Form/FormLabel';
@ -44,7 +45,10 @@ class EditDownloadClientModalContent extends Component {
implementationName, implementationName,
name, name,
enable, enable,
protocol,
priority, priority,
removeCompletedDownloads,
removeFailedDownloads,
fields, fields,
message message
} = item; } = item;
@ -133,6 +137,37 @@ class EditDownloadClientModalContent extends Component {
/> />
</FormGroup> </FormGroup>
<FieldSet
size={sizes.SMALL}
legend="Completed Download Handling"
>
<FormGroup>
<FormLabel>Remove Completed</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="removeCompletedDownloads"
helpText="Remove imported downloads from download client history (when finished seeding for torrents)"
{...removeCompletedDownloads}
onChange={onInputChange}
/>
</FormGroup>
{
protocol.value !== 'torrent' &&
<FormGroup>
<FormLabel>Remove Failed</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="removeFailedDownloads"
helpText="Remove failed downloads from download client history"
{...removeFailedDownloads}
onChange={onInputChange}
/>
</FormGroup>
}
</FieldSet>
</Form> </Form>
} }
</ModalBody> </ModalBody>

View File

@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { inputTypes, sizes } from 'Helpers/Props'; import { inputTypes, kinds, sizes } from 'Helpers/Props';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet'; import FieldSet from 'Components/FieldSet';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
@ -31,11 +32,16 @@ function DownloadClientOptions(props) {
} }
{ {
hasSettings && !isFetching && !error && hasSettings && !isFetching && !error && advancedSettings &&
<div> <div>
<FieldSet legend="Completed Download Handling"> <FieldSet legend="Completed Download Handling">
<Form> <Form>
<FormGroup size={sizes.MEDIUM}> <FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>Enable</FormLabel> <FormLabel>Enable</FormLabel>
<FormInputGroup <FormInputGroup
@ -52,25 +58,7 @@ function DownloadClientOptions(props) {
isAdvanced={true} isAdvanced={true}
size={sizes.MEDIUM} size={sizes.MEDIUM}
> >
<FormLabel>Remove</FormLabel> <FormLabel>Redownload Failed</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="removeCompletedDownloads"
helpText="Remove imported downloads from download client history"
onChange={onInputChange}
{...settings.removeCompletedDownloads}
/>
</FormGroup>
</Form>
</FieldSet>
<FieldSet
legend="Failed Download Handling"
>
<Form>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>Redownload</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
@ -80,23 +68,11 @@ function DownloadClientOptions(props) {
{...settings.autoRedownloadFailed} {...settings.autoRedownloadFailed}
/> />
</FormGroup> </FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>Remove</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="removeFailedDownloads"
helpText="Remove failed downloads from download client history"
onChange={onInputChange}
{...settings.removeFailedDownloads}
/>
</FormGroup>
</Form> </Form>
<Alert kind={kinds.INFO}>
The Remove settings were moved to the individual Download Client settings in the table above.
</Alert>
</FieldSet> </FieldSet>
</div> </div>
} }

View File

@ -8,10 +8,7 @@ namespace NzbDrone.Api.Config
public string DownloadClientWorkingFolders { get; set; } public string DownloadClientWorkingFolders { get; set; }
public bool EnableCompletedDownloadHandling { get; set; } public bool EnableCompletedDownloadHandling { get; set; }
public bool RemoveCompletedDownloads { get; set; }
public bool AutoRedownloadFailed { get; set; } public bool AutoRedownloadFailed { get; set; }
public bool RemoveFailedDownloads { get; set; }
} }
public static class DownloadClientConfigResourceMapper public static class DownloadClientConfigResourceMapper
@ -23,10 +20,7 @@ namespace NzbDrone.Api.Config
DownloadClientWorkingFolders = model.DownloadClientWorkingFolders, DownloadClientWorkingFolders = model.DownloadClientWorkingFolders,
EnableCompletedDownloadHandling = model.EnableCompletedDownloadHandling, EnableCompletedDownloadHandling = model.EnableCompletedDownloadHandling,
RemoveCompletedDownloads = model.RemoveCompletedDownloads, AutoRedownloadFailed = model.AutoRedownloadFailed
AutoRedownloadFailed = model.AutoRedownloadFailed,
RemoveFailedDownloads = model.RemoveFailedDownloads
}; };
} }
} }

View File

@ -16,6 +16,8 @@ namespace NzbDrone.Api.DownloadClient
resource.Enable = definition.Enable; resource.Enable = definition.Enable;
resource.Protocol = definition.Protocol; resource.Protocol = definition.Protocol;
resource.Priority = definition.Priority; resource.Priority = definition.Priority;
resource.RemoveCompletedDownloads = definition.RemoveCompletedDownloads;
resource.RemoveFailedDownloads = definition.RemoveFailedDownloads;
} }
protected override void MapToModel(DownloadClientDefinition definition, DownloadClientResource resource) protected override void MapToModel(DownloadClientDefinition definition, DownloadClientResource resource)
@ -25,6 +27,8 @@ namespace NzbDrone.Api.DownloadClient
definition.Enable = resource.Enable; definition.Enable = resource.Enable;
definition.Protocol = resource.Protocol; definition.Protocol = resource.Protocol;
definition.Priority = resource.Priority; definition.Priority = resource.Priority;
definition.RemoveCompletedDownloads = resource.RemoveCompletedDownloads;
definition.RemoveFailedDownloads = resource.RemoveFailedDownloads;
} }
protected override void Validate(DownloadClientDefinition definition, bool includeWarnings) protected override void Validate(DownloadClientDefinition definition, bool includeWarnings)

View File

@ -7,5 +7,7 @@ namespace NzbDrone.Api.DownloadClient
public bool Enable { get; set; } public bool Enable { get; set; }
public DownloadProtocol Protocol { get; set; } public DownloadProtocol Protocol { get; set; }
public int Priority { get; set; } public int Priority { get; set; }
public bool RemoveCompletedDownloads { get; set; }
public bool RemoveFailedDownloads { get; set; }
} }
} }

View File

@ -0,0 +1,119 @@
using System.Collections.Generic;
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<cdh_per_downloadclient>
{
[Test]
public void should_set_cdh_to_enabled()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("DownloadClients").Row(new
{
Enable = 1,
Name = "Deluge",
Implementation = "Deluge",
Priority = 1,
Settings = new DelugeSettings85
{
Host = "127.0.0.1",
TvCategory = "abc",
UrlBase = "/my/"
}.ToJson(),
ConfigContract = "DelugeSettings"
});
});
var items = db.Query<DownloadClientDefinition158>("SELECT * FROM DownloadClients");
items.Should().HaveCount(1);
items.First().RemoveCompletedDownloads.Should().BeTrue();
items.First().RemoveFailedDownloads.Should().BeFalse();
}
[Test]
public void should_set_cdh_to_disabled_when_globally_disabled()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Config").Row(new
{
Key = "removecompleteddownloads",
Value = "False"
});
c.Insert.IntoTable("DownloadClients").Row(new
{
Enable = 1,
Name = "Deluge",
Implementation = "Deluge",
Priority = 1,
Settings = new DelugeSettings85
{
Host = "127.0.0.1",
TvCategory = "abc",
UrlBase = "/my/"
}.ToJson(),
ConfigContract = "DelugeSettings"
});
});
var items = db.Query<DownloadClientDefinition158>("SELECT * FROM DownloadClients");
items.Should().HaveCount(1);
items.First().RemoveCompletedDownloads.Should().BeFalse();
items.First().RemoveFailedDownloads.Should().BeFalse();
}
[Test]
public void should_disable_remove_for_existing_rtorrent()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("DownloadClients").Row(new
{
Enable = 1,
Name = "RTorrent",
Implementation = "RTorrent",
Priority = 1,
Settings = new RTorrentSettings
{
Host = "127.0.0.1",
TvCategory = "abc",
UrlBase = "/my/"
}.ToJson(),
ConfigContract = "RTorrentSettings"
});
});
var items = db.Query<DownloadClientDefinition158>("SELECT * FROM DownloadClients");
items.Should().HaveCount(1);
items.First().RemoveCompletedDownloads.Should().BeFalse();
items.First().RemoveFailedDownloads.Should().BeFalse();
}
}
public class DownloadClientDefinition158
{
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; }
}
}

View File

@ -134,13 +134,6 @@ namespace NzbDrone.Core.Configuration
set { SetValue("EnableCompletedDownloadHandling", value); } set { SetValue("EnableCompletedDownloadHandling", value); }
} }
public bool RemoveCompletedDownloads
{
get { return GetValueBoolean("RemoveCompletedDownloads", false); }
set { SetValue("RemoveCompletedDownloads", value); }
}
public bool AutoRedownloadFailed public bool AutoRedownloadFailed
{ {
get { return GetValueBoolean("AutoRedownloadFailed", true); } get { return GetValueBoolean("AutoRedownloadFailed", true); }
@ -148,13 +141,6 @@ namespace NzbDrone.Core.Configuration
set { SetValue("AutoRedownloadFailed", value); } set { SetValue("AutoRedownloadFailed", value); }
} }
public bool RemoveFailedDownloads
{
get { return GetValueBoolean("RemoveFailedDownloads", true); }
set { SetValue("RemoveFailedDownloads", value); }
}
public bool CreateEmptySeriesFolders public bool CreateEmptySeriesFolders
{ {
get { return GetValueBoolean("CreateEmptySeriesFolders", false); } get { return GetValueBoolean("CreateEmptySeriesFolders", false); }

View File

@ -19,10 +19,7 @@ namespace NzbDrone.Core.Configuration
//Completed/Failed Download Handling (Download client) //Completed/Failed Download Handling (Download client)
bool EnableCompletedDownloadHandling { get; set; } bool EnableCompletedDownloadHandling { get; set; }
bool RemoveCompletedDownloads { get; set; }
bool AutoRedownloadFailed { get; set; } bool AutoRedownloadFailed { get; set; }
bool RemoveFailedDownloads { get; set; }
//Media Management //Media Management
bool AutoUnmonitorPreviouslyDownloadedEpisodes { get; set; } bool AutoUnmonitorPreviouslyDownloadedEpisodes { get; set; }

View File

@ -0,0 +1,51 @@
using System.Data;
using System.Linq;
using FluentMigrator;
using Newtonsoft.Json.Linq;
using NzbDrone.Common.Serializer;
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 = true;
var removeFailedDownloads = false;
using (var removeCompletedDownloadsCmd = conn.CreateCommand(tran, "SELECT Value FROM Config WHERE Key = 'removecompleteddownloads'"))
{
if ("False" == (removeCompletedDownloadsCmd.ExecuteScalar() as string))
removeCompletedDownloads = false;
}
using (var removeFailedDownloadsCmd = conn.CreateCommand(tran, "SELECT Value FROM Config WHERE Key = 'removefaileddownloads'"))
{
if ("True" == (removeFailedDownloadsCmd.ExecuteScalar() as string))
removeFailedDownloads = true;
}
using (var updateClientCmd = conn.CreateCommand(tran, $"UPDATE DownloadClients SET RemoveCompletedDownloads = (CASE WHEN Implementation = \"RTorrent\" THEN 0 ELSE ? END), RemoveFailedDownloads = ?"))
{
updateClientCmd.AddParameter(removeCompletedDownloads ? 1 : 0);
updateClientCmd.AddParameter(removeFailedDownloads ? 1 : 0);
updateClientCmd.ExecuteNonQuery();
}
using (var removeConfigCmd = conn.CreateCommand(tran, $"DELETE FROM Config WHERE Key IN ('removecompleteddownloads', 'removefaileddownloads')"))
{
removeConfigCmd.ExecuteNonQuery();
}
}
}
}

View File

@ -1,4 +1,5 @@
using FluentMigrator.Builders.Create; using System.Data;
using FluentMigrator.Builders.Create;
using FluentMigrator.Builders.Create.Table; using FluentMigrator.Builders.Create.Table;
namespace NzbDrone.Core.Datastore.Migration.Framework namespace NzbDrone.Core.Datastore.Migration.Framework
@ -10,7 +11,16 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
return expressionRoot.Table(name).WithColumn("Id").AsInt32().PrimaryKey().Identity(); 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(); var parameter = command.CreateParameter();
parameter.Value = value; parameter.Value = value;

View File

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Threading; using System.Threading;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.MediaFiles.TorrentInfo;
@ -22,6 +23,8 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
{ {
private readonly IRTorrentProxy _proxy; private readonly IRTorrentProxy _proxy;
private readonly IRTorrentDirectoryValidator _rTorrentDirectoryValidator; private readonly IRTorrentDirectoryValidator _rTorrentDirectoryValidator;
private readonly IDownloadSeedConfigProvider _downloadSeedConfigProvider;
private readonly string _imported_view = String.Concat(BuildInfo.AppName.ToLower(), "_imported");
public RTorrent(IRTorrentProxy proxy, public RTorrent(IRTorrentProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader, ITorrentFileInfoReader torrentFileInfoReader,
@ -29,17 +32,19 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
IConfigService configService, IConfigService configService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IDownloadSeedConfigProvider downloadSeedConfigProvider,
IRTorrentDirectoryValidator rTorrentDirectoryValidator, IRTorrentDirectoryValidator rTorrentDirectoryValidator,
Logger logger) Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
{ {
_proxy = proxy; _proxy = proxy;
_rTorrentDirectoryValidator = rTorrentDirectoryValidator; _rTorrentDirectoryValidator = rTorrentDirectoryValidator;
_downloadSeedConfigProvider = downloadSeedConfigProvider;
} }
public override void MarkItemAsImported(DownloadClientItem downloadClientItem) public override void MarkItemAsImported(DownloadClientItem downloadClientItem)
{ {
// set post-import category // Set post-import label
if (Settings.TvImportedCategory.IsNotNullOrWhiteSpace() && if (Settings.TvImportedCategory.IsNotNullOrWhiteSpace() &&
Settings.TvImportedCategory != Settings.TvCategory) Settings.TvImportedCategory != Settings.TvCategory)
{ {
@ -53,6 +58,17 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
Settings.TvImportedCategory, downloadClientItem.Title); Settings.TvImportedCategory, downloadClientItem.Title);
} }
} }
// Set post-import view
try
{
_proxy.PushTorrentUniqueView(downloadClientItem.DownloadId.ToLower(), _imported_view, Settings);
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to set torrent post-import view \"{0}\" for {1} in rTorrent.",
_imported_view, downloadClientItem.Title);
}
} }
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink) protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
@ -95,7 +111,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public override string Name => "rTorrent"; public override string Name => "rTorrent";
public override ProviderMessage Message => new ProviderMessage("Sonarr is unable to remove torrents that have finished seeding when using rTorrent", ProviderMessageType.Warning); public override ProviderMessage Message => new ProviderMessage($"Sonarr will handle automatic removal of torrents based on the current seed criteria in Settings->Indexers. After importing it will also set \"{_imported_view}\" as an rTorrent view, which can be used in rTorrent scripts to customize behavior.", ProviderMessageType.Info);
public override IEnumerable<DownloadClientItem> GetItems() public override IEnumerable<DownloadClientItem> GetItems()
{ {
@ -147,8 +163,16 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
item.Status = DownloadItemStatus.Paused; item.Status = DownloadItemStatus.Paused;
} }
// No stop ratio data is present, so do not delete // Grab cached seedConfig
item.CanMoveFiles = item.CanBeRemoved = false; var seedConfig = _downloadSeedConfigProvider.GetSeedConfiguration(torrent.Hash);
// Check if torrent is finished and if it exceeds cached seedConfig
item.CanMoveFiles = item.CanBeRemoved =
torrent.IsFinished && seedConfig != null &&
(
(torrent.Ratio / 1000.0) >= seedConfig.Ratio ||
(DateTimeOffset.Now - DateTimeOffset.FromUnixTimeSeconds(torrent.FinishedTime)) >= seedConfig.SeedTime
);
items.Add(item); items.Add(item);
} }

View File

@ -20,6 +20,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
void RemoveTorrent(string hash, RTorrentSettings settings); void RemoveTorrent(string hash, RTorrentSettings settings);
void SetTorrentLabel(string hash, string label, RTorrentSettings settings); void SetTorrentLabel(string hash, string label, RTorrentSettings settings);
bool HasHashTorrent(string hash, RTorrentSettings settings); bool HasHashTorrent(string hash, RTorrentSettings settings);
void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings);
} }
public interface IRTorrent : IXmlRpcProxy public interface IRTorrent : IXmlRpcProxy
@ -48,6 +49,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
[XmlRpcMethod("d.custom1.set")] [XmlRpcMethod("d.custom1.set")]
string SetLabel(string hash, string label); string SetLabel(string hash, string label);
[XmlRpcMethod("d.views.push_back_unique")]
int PushUniqueView(string hash, string view);
[XmlRpcMethod("system.client_version")] [XmlRpcMethod("system.client_version")]
string GetVersion(); string GetVersion();
} }
@ -87,7 +91,8 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
"d.ratio=", // long "d.ratio=", // long
"d.is_open=", // long "d.is_open=", // long
"d.is_active=", // long "d.is_active=", // long
"d.complete=") //long "d.complete=", // long
"d.timestamp.finished=") // long (unix timestamp)
); );
var items = new List<RTorrentTorrent>(); var items = new List<RTorrentTorrent>();
@ -108,6 +113,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
item.IsOpen = Convert.ToBoolean((long)torrent[8]); item.IsOpen = Convert.ToBoolean((long)torrent[8]);
item.IsActive = Convert.ToBoolean((long)torrent[9]); item.IsActive = Convert.ToBoolean((long)torrent[9]);
item.IsFinished = Convert.ToBoolean((long)torrent[10]); item.IsFinished = Convert.ToBoolean((long)torrent[10]);
item.FinishedTime = (long)torrent[11];
items.Add(item); items.Add(item);
} }
@ -174,6 +180,18 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
} }
} }
public void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings)
{
_logger.Debug("Executing remote method: d.views.push_back_unique");
var client = BuildClient(settings);
var response = ExecuteRequest(() => client.PushUniqueView(hash, view));
if (response != 0)
{
throw new DownloadClientException("Could not push unique view {0} for torrent: {1}.", view, hash);
}
}
public void RemoveTorrent(string hash, RTorrentSettings settings) public void RemoveTorrent(string hash, RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: d.erase"); _logger.Debug("Executing remote method: d.erase");

View File

@ -10,6 +10,7 @@
public long RemainingSize { get; set; } public long RemainingSize { get; set; }
public long DownRate { get; set; } public long DownRate { get; set; }
public long Ratio { get; set; } public long Ratio { get; set; }
public long FinishedTime { get; set; }
public bool IsFinished { get; set; } public bool IsFinished { get; set; }
public bool IsOpen { get; set; } public bool IsOpen { get; set; }
public bool IsActive { get; set; } public bool IsActive { get; set; }

View File

@ -7,5 +7,8 @@ namespace NzbDrone.Core.Download
{ {
public DownloadProtocol Protocol { get; set; } public DownloadProtocol Protocol { get; set; }
public int Priority { get; set; } = 1; public int Priority { get; set; } = 1;
public bool RemoveCompletedDownloads { get; set; } = true;
public bool RemoveFailedDownloads { get; set; } = true;
} }
} }

View File

@ -27,38 +27,65 @@ namespace NzbDrone.Core.Download
{ {
var trackedDownload = message.TrackedDownload; var trackedDownload = message.TrackedDownload;
if (trackedDownload == null || !trackedDownload.DownloadItem.CanBeRemoved || _configService.RemoveFailedDownloads == false) if (trackedDownload == null ||
message.TrackedDownload.DownloadItem.Removed ||
!trackedDownload.DownloadItem.CanBeRemoved)
{ {
return; 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) public void Handle(DownloadCompletedEvent message)
{ {
if (_configService.RemoveCompletedDownloads && var trackedDownload = message.TrackedDownload;
!message.TrackedDownload.DownloadItem.Removed && var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient);
message.TrackedDownload.DownloadItem.CanBeRemoved && var definition = downloadClient.Definition as DownloadClientDefinition;
message.TrackedDownload.DownloadItem.Status != DownloadItemStatus.Downloading)
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) public void Handle(DownloadCanBeRemovedEvent message)
{ {
// Already verified that it can be removed, just needs to be removed var trackedDownload = message.TrackedDownload;
RemoveFromDownloadClient(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 try
{ {
_logger.Debug("[{0}] Removing download from {1} history", trackedDownload.DownloadItem.Title, trackedDownload.DownloadItem.DownloadClientInfo.Name); _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 try
{ {
_logger.Debug("[{0}] Marking download as imported from {1}", trackedDownload.DownloadItem.Title, trackedDownload.DownloadItem.DownloadClientInfo.Name); _logger.Debug("[{0}] Marking download as imported from {1}", trackedDownload.DownloadItem.Title, trackedDownload.DownloadItem.DownloadClientInfo.Name);

View File

@ -48,7 +48,6 @@ namespace NzbDrone.Core.Download
public void Execute(ProcessMonitoredDownloadsCommand message) public void Execute(ProcessMonitoredDownloadsCommand message)
{ {
var enableCompletedDownloadHandling = _configService.EnableCompletedDownloadHandling; var enableCompletedDownloadHandling = _configService.EnableCompletedDownloadHandling;
var removeCompletedDownloads = _configService.RemoveCompletedDownloads;
var trackedDownloads = _trackedDownloadService.GetTrackedDownloads() var trackedDownloads = _trackedDownloadService.GetTrackedDownloads()
.Where(t => t.IsTrackable) .Where(t => t.IsTrackable)
.ToList(); .ToList();
@ -73,10 +72,7 @@ namespace NzbDrone.Core.Download
} }
// Imported downloads are no longer trackable so process them after processing trackable downloads // Imported downloads are no longer trackable so process them after processing trackable downloads
if (removeCompletedDownloads) RemoveCompletedDownloads();
{
RemoveCompletedDownloads();
}
_eventAggregator.PublishEvent(new DownloadsProcessedEvent()); _eventAggregator.PublishEvent(new DownloadsProcessedEvent());
} }

View File

@ -0,0 +1,84 @@
using System;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Download.History;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Download
{
public interface IDownloadSeedConfigProvider
{
TorrentSeedConfiguration GetSeedConfiguration(string infoHash);
}
public class DownloadSeedConfigProvider : IDownloadSeedConfigProvider
{
private readonly Logger _logger;
private readonly ISeedConfigProvider _indexerSeedConfigProvider;
private readonly IDownloadHistoryService _downloadHistoryService;
class CachedSeedConfiguration
{
public int IndexerId { get; set; }
public bool FullSeason { get; set; }
}
private readonly ICached<CachedSeedConfiguration> _cacheDownloads;
public DownloadSeedConfigProvider(IDownloadHistoryService downloadHistoryService, ISeedConfigProvider indexerSeedConfigProvider, ICacheManager cacheManager, Logger logger)
{
_logger = logger;
_indexerSeedConfigProvider = indexerSeedConfigProvider;
_downloadHistoryService = downloadHistoryService;
_cacheDownloads = cacheManager.GetRollingCache<CachedSeedConfiguration>(GetType(), "indexerByHash", TimeSpan.FromHours(1));
}
public TorrentSeedConfiguration GetSeedConfiguration(string infoHash)
{
if (infoHash.IsNullOrWhiteSpace()) return null;
infoHash = infoHash.ToUpper();
var cachedConfig = _cacheDownloads.Get(infoHash, () => FetchIndexer(infoHash));
if (cachedConfig == null) return null;
var seedConfig = _indexerSeedConfigProvider.GetSeedConfiguration(cachedConfig.IndexerId, cachedConfig.FullSeason);
return seedConfig;
}
private CachedSeedConfiguration FetchIndexer(string infoHash)
{
var historyItem = _downloadHistoryService.GetLatestGrab(infoHash);
if (historyItem == null)
{
_logger.Debug("No download history item for infohash {0}, unable to provide seed configuration", infoHash);
return null;
}
ParsedEpisodeInfo parsedEpisodeInfo = null;
if (historyItem.Release != null)
{
parsedEpisodeInfo = Parser.Parser.ParseTitle(historyItem.Release.Title);
}
if (parsedEpisodeInfo == null)
{
_logger.Debug("No parsed title in download history item for infohash {0}, unable to provide seed configuration", infoHash);
return null;
}
return new CachedSeedConfiguration
{
IndexerId = historyItem.IndexerId,
FullSeason = parsedEpisodeInfo.FullSeason
};
}
}
}

View File

@ -1,17 +0,0 @@
using NzbDrone.Common.Messaging;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Indexers
{
public class IndexerSettingUpdatedEvent : IEvent
{
public string IndexerName { get; private set; }
public IProviderConfig IndexerSetting { get; private set; }
public IndexerSettingUpdatedEvent(string indexerName, IProviderConfig indexerSetting)
{
IndexerName = indexerName;
IndexerSetting = indexerSetting;
}
}
}

View File

@ -1,25 +1,30 @@
using System; using System;
using System.Linq; using System.Linq;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Cache;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Download.Clients; using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.Indexers namespace NzbDrone.Core.Indexers
{ {
public interface ISeedConfigProvider public interface ISeedConfigProvider
{ {
TorrentSeedConfiguration GetSeedConfiguration(RemoteEpisode release); TorrentSeedConfiguration GetSeedConfiguration(RemoteEpisode release);
TorrentSeedConfiguration GetSeedConfiguration(int indexerId, bool fullSeason);
} }
public class SeedConfigProvider : ISeedConfigProvider public class SeedConfigProvider : ISeedConfigProvider, IHandle<ProviderUpdatedEvent<IIndexer>>
{ {
private readonly IIndexerFactory _indexerFactory; private readonly IIndexerFactory _indexerFactory;
private readonly ICached<SeedCriteriaSettings> _cache;
public SeedConfigProvider(IIndexerFactory indexerFactory) public SeedConfigProvider(IIndexerFactory indexerFactory, ICacheManager cacheManager)
{ {
_indexerFactory = indexerFactory; _indexerFactory = indexerFactory;
_cache = cacheManager.GetRollingCache<SeedCriteriaSettings>(GetType(), "criteriaByIndexer", TimeSpan.FromHours(1));
} }
public TorrentSeedConfiguration GetSeedConfiguration(RemoteEpisode remoteEpisode) public TorrentSeedConfiguration GetSeedConfiguration(RemoteEpisode remoteEpisode)
@ -27,33 +32,49 @@ namespace NzbDrone.Core.Indexers
if (remoteEpisode.Release.DownloadProtocol != DownloadProtocol.Torrent) return null; if (remoteEpisode.Release.DownloadProtocol != DownloadProtocol.Torrent) return null;
if (remoteEpisode.Release.IndexerId == 0) return null; if (remoteEpisode.Release.IndexerId == 0) return null;
return GetSeedConfiguration(remoteEpisode.Release.IndexerId, remoteEpisode.ParsedEpisodeInfo.FullSeason);
}
public TorrentSeedConfiguration GetSeedConfiguration(int indexerId, bool fullSeason)
{
if (indexerId == 0) return null;
var seedCriteria = _cache.Get(indexerId.ToString(), () => FetchSeedCriteria(indexerId));
if (seedCriteria == null) return null;
var seedConfig = new TorrentSeedConfiguration
{
Ratio = seedCriteria.SeedRatio
};
var seedTime = fullSeason ? seedCriteria.SeasonPackSeedTime : seedCriteria.SeedTime;
if (seedTime.HasValue)
{
seedConfig.SeedTime = TimeSpan.FromMinutes(seedTime.Value);
}
return seedConfig;
}
private SeedCriteriaSettings FetchSeedCriteria(int indexerId)
{
try try
{ {
var indexer = _indexerFactory.Get(remoteEpisode.Release.IndexerId); var indexer = _indexerFactory.Get(indexerId);
var torrentIndexerSettings = indexer.Settings as ITorrentIndexerSettings; var torrentIndexerSettings = indexer.Settings as ITorrentIndexerSettings;
if (torrentIndexerSettings != null && torrentIndexerSettings.SeedCriteria != null) return torrentIndexerSettings?.SeedCriteria;
{
var seedConfig = new TorrentSeedConfiguration
{
Ratio = torrentIndexerSettings.SeedCriteria.SeedRatio
};
var seedTime = remoteEpisode.ParsedEpisodeInfo.FullSeason ? torrentIndexerSettings.SeedCriteria.SeasonPackSeedTime : torrentIndexerSettings.SeedCriteria.SeedTime;
if (seedTime.HasValue)
{
seedConfig.SeedTime = TimeSpan.FromMinutes(seedTime.Value);
}
return seedConfig;
}
} }
catch (ModelNotFoundException) catch (ModelNotFoundException)
{ {
return null; return null;
} }
}
return null; public void Handle(ProviderUpdatedEvent<IIndexer> message)
{
_cache.Clear();
} }
} }
} }

View File

@ -103,7 +103,10 @@ namespace NzbDrone.Core.ThingiProvider
public virtual TProviderDefinition Create(TProviderDefinition definition) public virtual TProviderDefinition Create(TProviderDefinition definition)
{ {
return _providerRepository.Insert(definition); var result = _providerRepository.Insert(definition);
_eventAggregator.PublishEvent(new ProviderUpdatedEvent<TProvider>(result));
return result;
} }
public virtual void Update(TProviderDefinition definition) public virtual void Update(TProviderDefinition definition)

View File

@ -8,10 +8,7 @@ namespace Sonarr.Api.V3.Config
public string DownloadClientWorkingFolders { get; set; } public string DownloadClientWorkingFolders { get; set; }
public bool EnableCompletedDownloadHandling { get; set; } public bool EnableCompletedDownloadHandling { get; set; }
public bool RemoveCompletedDownloads { get; set; }
public bool AutoRedownloadFailed { get; set; } public bool AutoRedownloadFailed { get; set; }
public bool RemoveFailedDownloads { get; set; }
} }
public static class DownloadClientConfigResourceMapper public static class DownloadClientConfigResourceMapper
@ -23,10 +20,7 @@ namespace Sonarr.Api.V3.Config
DownloadClientWorkingFolders = model.DownloadClientWorkingFolders, DownloadClientWorkingFolders = model.DownloadClientWorkingFolders,
EnableCompletedDownloadHandling = model.EnableCompletedDownloadHandling, EnableCompletedDownloadHandling = model.EnableCompletedDownloadHandling,
RemoveCompletedDownloads = model.RemoveCompletedDownloads, AutoRedownloadFailed = model.AutoRedownloadFailed
AutoRedownloadFailed = model.AutoRedownloadFailed,
RemoveFailedDownloads = model.RemoveFailedDownloads
}; };
} }
} }

View File

@ -8,6 +8,8 @@ namespace Sonarr.Api.V3.DownloadClient
public bool Enable { get; set; } public bool Enable { get; set; }
public DownloadProtocol Protocol { get; set; } public DownloadProtocol Protocol { get; set; }
public int Priority { get; set; } public int Priority { get; set; }
public bool RemoveCompletedDownloads { get; set; }
public bool RemoveFailedDownloads { get; set; }
} }
public class DownloadClientResourceMapper : ProviderResourceMapper<DownloadClientResource, DownloadClientDefinition> public class DownloadClientResourceMapper : ProviderResourceMapper<DownloadClientResource, DownloadClientDefinition>
@ -21,6 +23,8 @@ namespace Sonarr.Api.V3.DownloadClient
resource.Enable = definition.Enable; resource.Enable = definition.Enable;
resource.Protocol = definition.Protocol; resource.Protocol = definition.Protocol;
resource.Priority = definition.Priority; resource.Priority = definition.Priority;
resource.RemoveCompletedDownloads = definition.RemoveCompletedDownloads;
resource.RemoveFailedDownloads = definition.RemoveFailedDownloads;
return resource; return resource;
} }
@ -34,6 +38,8 @@ namespace Sonarr.Api.V3.DownloadClient
definition.Enable = resource.Enable; definition.Enable = resource.Enable;
definition.Protocol = resource.Protocol; definition.Protocol = resource.Protocol;
definition.Priority = resource.Priority; definition.Priority = resource.Priority;
definition.RemoveCompletedDownloads = resource.RemoveCompletedDownloads;
definition.RemoveFailedDownloads = resource.RemoveFailedDownloads;
return definition; return definition;
} }

View File

@ -172,8 +172,7 @@ namespace Sonarr.Http.ClientSchema
{ {
Value = value, Value = value,
Name = name, Name = name,
Order = value, Order = value
Hint = $"({value})"
}; };
} }
}); });