1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-17 21:26:13 -04:00

Compare commits

..

39 Commits

Author SHA1 Message Date
Taloth Saldono
a200dd5f6d Bump package version 2021-03-27 22:14:03 +01:00
Taloth Saldono
f57efd30b8 Fixed data dir ownership in case of dpkg-reconfigure 2021-03-27 21:30:30 +01:00
bakerboy448
f2f1039c5e Fixed: Debatable typos in Naming Modal 2021-03-27 13:30:22 -07:00
bakerboy448
12ba4f73ed Updating the bug template yet again 2021-03-26 19:14:08 -07:00
Mark McDowall
370280b4bf Another wiki URL update
Close #4411
2021-03-26 19:08:04 -07:00
Mark McDowall
6c505937da Fixed: Interactive import modal horizontal scrolling on Firefox mobile
Closes #4401
2021-03-23 20:37:13 -07:00
Taloth Saldono
7272c5b7fc Added IsTorrentLoaded to tests 2021-03-20 00:33:51 +01:00
Mark McDowall
1d06c3fc15 Revert vswhere command 2021-03-19 15:21:04 -07:00
Mark McDowall
ec9f62285a Updated vswhere.exe 2021-03-19 14:40:17 -07:00
Mark McDowall
aae0d1c4ba Updated nuget.exe 2021-03-19 14:25:59 -07:00
Taloth Saldono
652d44722b Fixed: Qbittorrent api errors when only one of two seed criteria was configured
closes #4393
2021-03-19 21:32:42 +01:00
Taloth Saldono
5a69801877 Fixed: Unnecessary idle cpu usage
ref #4386
2021-03-19 02:48:09 +01:00
Taloth Saldono
fa8b2f48e7 Don't ignore original wal/journal during v3 migration 2021-03-18 23:43:16 +01:00
Taloth Saldono
34faa417c1 Fixed: Database migration failure when database was manually repaired in a certain way
fixes #4390
2021-03-18 01:25:21 +01:00
Mark McDowall
d6c0635a26 New: Improve message if Sonarr can't bind to IP/port during startup
Closes #4352
2021-03-17 17:21:30 -07:00
Mark McDowall
d4167d7169 New: Support for using parsed season number for some anime releases without aliases
Closes #4377
2021-03-17 17:14:54 -07:00
Taloth Saldono
eea6be459d Fixed post-install update check not running 2021-03-14 20:24:08 +01:00
Taloth Saldono
67e97f7aee Fixed: Setting seed criteria while torrent is still being loaded by qbittorrent
closes #4360
2021-03-14 00:46:28 +01:00
Mark McDowall
37e1c4f2eb New: Don't close interactive search with background click 2021-03-11 08:23:06 -08:00
Taloth Saldono
a9b8ec3505 Fixed failing tests 2021-03-10 23:38:59 +01:00
Taloth Saldono
f57cf1561b Log Skyhook connection failures with more info. 2021-03-10 23:05:19 +01:00
Taloth Saldono
6672650b6b Log Skyhook connection failures with more info. 2021-03-10 21:44:32 +01:00
Taloth Saldono
e4a064a1c0 Added comment to sonarr.service 2021-03-10 21:44:32 +01:00
Taloth Saldono
a848e575cd Make it clearer that Maximum size is the global limit. 2021-03-10 21:44:32 +01:00
Taloth Saldono
01995e686d New: Multiple Recipients on Email Notifications (Also CC, BCC)
Based on Qstick's Radarr commit of the same name
closes #4369

Signed-off-by: Taloth Saldono <Taloth@users.noreply.github.com>
2021-03-10 21:44:31 +01:00
Taloth Saldono
32058f1705 Fixed systemd unit search&replace issue and added umask to debconf 2021-03-10 21:44:31 +01:00
Mark McDowall
af3696af08 On Download -> On Import (again) 2021-03-10 11:25:29 -08:00
Mark McDowall
1477356cfc Update Discord link 2021-03-08 19:29:11 -08:00
Mark McDowall
aa19ddfbfd Fixed: Parsing of absolute episode numbers over 1000
Closes #4367
2021-03-08 19:22:41 -08:00
Mark McDowall
a697a69e88 Fixed: Some health check wiki links 2021-03-08 19:22:34 -08:00
Mark McDowall
3abb7e156a Fixed: Parsing of absolute episode number inside square brackets
Closes #4331
2021-03-07 17:31:43 -08:00
Mark McDowall
240791a7cd Fixed: Parsing of anime batch releases using a tilde instead of a dash
Closes #4330
2021-03-07 17:10:00 -08:00
Mark McDowall
0fe2453962 Fixed: Parsing similar series titles with common words at end 2021-03-07 16:53:56 -08:00
Robin Dadswell
85f4cbe94c Fix: Consistent SSL option for Download Clients
Closes #4323
2021-03-07 16:32:19 -08:00
Mark McDowall
e1f7bce14b New: Simplify Connection trigger settings
Closes #4351
2021-03-07 16:24:20 -08:00
Mark McDowall
675c72f02e Fixed: Set SameSite=Strict for SonarrAuth cookie
Closes #4365
2021-03-07 16:24:20 -08:00
Mark McDowall
6619350f87 Fixed: Don't set cookies for static resources
Closes #4356
2021-03-07 16:24:20 -08:00
Mark McDowall
efd9fe9ad0 Fixed: Cache headers for static resources
Towards #4356
2021-03-07 16:24:20 -08:00
Mark McDowall
ab502ffda4 Just one Application Version header 2021-03-07 16:24:20 -08:00
69 changed files with 1164 additions and 405 deletions

View File

@@ -32,5 +32,6 @@ assignees: ''
- Sonarr Branch: <!--[e.g. master, develop , phantom-develop]-->
**Trace Logs**
Turn on Trace logs under Settings -> General and wait for the bug to occur again.
**Upload the full log file here (or another site (e.g. pastebin) and link it). Issues will be closed, if they do not include this!**
Turn on Trace logs under Settings -> General and wait for the bug to occur again.
**Upload the full log file here (or another site (e.g. pastebin) and link it). Issues will be closed, if they do not include this!**
<!-- Trace logs are named Sonarr.trace.txt or Sonarr.trace.#.txt and will contain "trace" in them-->

View File

@@ -8,7 +8,10 @@ db_input high sonarr/owning_group || true
db_endblock
db_go
db_beginblock
db_input low sonarr/owning_umask || true
db_input low sonarr/config_directory || true
db_endblock
db_go
exit 0

View File

@@ -9,6 +9,8 @@ db_get sonarr/owning_user
USER="$RET"
db_get sonarr/owning_group
GROUP="$RET"
db_get sonarr/owning_umask
UMASK="$RET"
db_get sonarr/config_directory
CONFDIR="$RET"
@@ -64,9 +66,11 @@ fi
# Create data directory
if [ ! -d "$CONFDIR" ]; then
mkdir -p "$CONFDIR"
chown -R $USER:$GROUP "$CONFDIR"
fi
# Set permissions on data directory (always do this instead only on creation in case user was changed via dpkg-reconfigure)
chown -R $USER:$GROUP "$CONFDIR"
#BEGIN BUILTIN UPDATER
# Apply patch if present
if [ "$UPDATER" = "BuiltIn" ] && [ -f /usr/lib/sonarr/bin_patch/release_info ]; then
@@ -92,7 +96,7 @@ fi
chown -R $USER:$GROUP /usr/lib/sonarr
# Update sonarr.service file
sed -i "s:User=sonarr:User=$USER:g; s:Group=sonarr:Group=$GROUP:g; s:-data=/var/lib/sonarr:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
sed -i "s:User=\w*:User=$USER:g; s:Group=\w*:Group=$GROUP:g; s:UMask=[0-9]*:UMask=$UMASK:g; s:-data=.*$:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
#BEGIN BUILTIN UPDATER
if [ "$UPDATER" = "BuiltIn" ]; then

View File

@@ -1,3 +1,6 @@
# This file is owned by the sonarr package, DO NOT MODIFY MANUALLY
# Instead use 'dpkg-reconfigure -plow sonarr' to modify User/Group/UMask/-data
# Or use systemd built-in override functionality using 'systemctl edit sonarr'
[Unit]
Description=Sonarr Daemon
After=network.target

View File

@@ -14,6 +14,12 @@ Description: Sonarr group:
Any media files created by Sonarr will be writeable by this group.
It's advisable to keep the group the same between download client, Sonarr and media centers.
Template: sonarr/owning_umask
Type: string
Default: 0002
Description: Sonarr umask:
Specifies the umask of the files created by Sonarr. 0002 means the files will be created with 664 as permissions.
Template: sonarr/config_directory
Type: string
Default: /var/lib/sonarr

View File

@@ -6,6 +6,24 @@ import EpisodeDetailsModalContentConnector from './EpisodeDetailsModalContentCon
class EpisodeDetailsModal extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
closeOnBackgroundClick: false
};
}
//
// Listeners
onTabChange = (isSearch) => {
this.setState({ closeOnBackgroundClick: !isSearch });
}
//
// Render
@@ -20,10 +38,12 @@ class EpisodeDetailsModal extends Component {
<Modal
isOpen={isOpen}
size={sizes.EXTRA_LARGE}
closeOnBackgroundClick={this.state.closeOnBackgroundClick}
onModalClose={onModalClose}
>
<EpisodeDetailsModalContentConnector
{...otherProps}
onTabChange={this.onTabChange}
onModalClose={onModalClose}
/>
</Modal>

View File

@@ -37,7 +37,9 @@ class EpisodeDetailsModalContent extends Component {
// Listeners
onTabSelect = (index, lastIndex) => {
this.setState({ selectedTab: tabs[index] });
const selectedTab = tabs[index];
this.props.onTabChange(selectedTab === 'search');
this.setState({ selectedTab });
}
//
@@ -206,6 +208,7 @@ EpisodeDetailsModalContent.propTypes = {
selectedTab: PropTypes.string.isRequired,
startInteractiveSearch: PropTypes.bool.isRequired,
onMonitorEpisodePress: PropTypes.func.isRequired,
onTabChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -300,7 +300,7 @@ class InteractiveImportModalContent extends Component {
isPopulated && !!items.length && !isFetching && !isFetching &&
<Table
columns={columns}
horizontalScroll={false}
horizontalScroll={true}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}

View File

@@ -23,8 +23,8 @@ const separatorOptions = [
const caseOptions = [
{ key: 'title', value: 'Default Case' },
{ key: 'lower', value: 'Lower Case' },
{ key: 'upper', value: 'Upper Case' }
{ key: 'lower', value: 'Lowercase' },
{ key: 'upper', value: 'Uppercase' }
];
const fileNameTokens = [

View File

@@ -9,3 +9,12 @@
margin-bottom: 30px;
}
.triggers {
margin-top: 3px;
}
.triggerEvents {
margin-top: 10px;
user-select: none;
}

View File

@@ -13,6 +13,7 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormInputHelpText from 'Components/Form/FormInputHelpText';
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import styles from './EditNotificationModalContent.css';
@@ -102,131 +103,110 @@ function EditNotificationModalContent(props) {
</FormGroup>
<FormGroup>
<FormLabel>On Grab</FormLabel>
<FormLabel>Triggers</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="onGrab"
helpText="Be notified when episodes are available for download and has been sent to a download client"
isDisabled={!supportsOnGrab.value}
{...onGrab}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>On Import</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="onDownload"
helpText="Be notified when episodes are successfully imported"
isDisabled={!supportsOnDownload.value}
{...onDownload}
onChange={onInputChange}
/>
</FormGroup>
{
onDownload.value &&
<FormGroup>
<FormLabel>On Upgrade</FormLabel>
<div className={styles.triggers}>
<FormInputHelpText
text="Select which events should trigger this conection"
link="https://wiki.servarr.com/Sonarr_Settings#Connections"
/>
<div className={styles.triggerEvents}>
<FormInputGroup
type={inputTypes.CHECK}
name="onUpgrade"
helpText="Be notified when episodes are upgraded to a better quality"
isDisabled={!supportsOnUpgrade.value}
{...onUpgrade}
name="onGrab"
helpText="On Grab"
isDisabled={!supportsOnGrab.value}
{...onGrab}
onChange={onInputChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>On Rename</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="onRename"
helpText="Be notified when episodes are renamed"
isDisabled={!supportsOnRename.value}
{...onRename}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>On Series Delete</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="onSeriesDelete"
helpText="Be notified when series are deleted"
isDisabled={!supportsOnSeriesDelete.value}
{...onSeriesDelete}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>On Episode File Delete</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="onEpisodeFileDelete"
helpText="Be notified when episode files are deleted"
isDisabled={!supportsOnEpisodeFileDelete.value}
{...onEpisodeFileDelete}
onChange={onInputChange}
/>
</FormGroup>
{
onEpisodeFileDelete.value ?
<FormGroup>
<FormLabel>On Episode File Delete For Upgrade</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="onEpisodeFileDeleteForUpgrade"
helpText="Be notified when episode files are deleted for upgrades"
isDisabled={!supportsOnEpisodeFileDeleteForUpgrade.value}
{...onEpisodeFileDeleteForUpgrade}
name="onDownload"
helpText="On Import"
isDisabled={!supportsOnDownload.value}
{...onDownload}
onChange={onInputChange}
/>
</FormGroup> :
null
}
<FormGroup>
<FormLabel>On Health Issue</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="onHealthIssue"
helpText="Be notified on health check failures"
isDisabled={!supportsOnHealthIssue.value}
{...onHealthIssue}
onChange={onInputChange}
/>
</FormGroup>
{
onHealthIssue.value &&
<FormGroup>
<FormLabel>Include Health Warnings</FormLabel>
{
onDownload.value ?
<FormInputGroup
type={inputTypes.CHECK}
name="onUpgrade"
helpText="On Upgrade"
isDisabled={!supportsOnUpgrade.value}
{...onUpgrade}
onChange={onInputChange}
/> :
null
}
<FormInputGroup
type={inputTypes.CHECK}
name="includeHealthWarnings"
helpText="Be notified on health warnings in addition to errors"
name="onRename"
helpText="On Rename"
isDisabled={!supportsOnRename.value}
{...onRename}
onChange={onInputChange}
/>
<FormInputGroup
type={inputTypes.CHECK}
name="onSeriesDelete"
helpText="On Series Delete"
isDisabled={!supportsOnSeriesDelete.value}
{...onSeriesDelete}
onChange={onInputChange}
/>
<FormInputGroup
type={inputTypes.CHECK}
name="onEpisodeFileDelete"
helpText="On Episode File Delete"
isDisabled={!supportsOnEpisodeFileDelete.value}
{...onEpisodeFileDelete}
onChange={onInputChange}
/>
{
onEpisodeFileDelete.value ?
<FormInputGroup
type={inputTypes.CHECK}
name="onEpisodeFileDeleteForUpgrade"
helpText="On Episode File Delete For Upgrade"
isDisabled={!supportsOnEpisodeFileDeleteForUpgrade.value}
{...onEpisodeFileDeleteForUpgrade}
onChange={onInputChange}
/> :
null
}
<FormInputGroup
type={inputTypes.CHECK}
name="onHealthIssue"
helpText="On Health Issue"
isDisabled={!supportsOnHealthIssue.value}
{...includeHealthWarnings}
{...onHealthIssue}
onChange={onInputChange}
/>
</FormGroup>
}
{
onHealthIssue.value ?
<FormInputGroup
type={inputTypes.CHECK}
name="includeHealthWarnings"
helpText="Include Health Warnings"
isDisabled={!supportsOnHealthIssue.value}
{...includeHealthWarnings}
onChange={onInputChange}
/> :
null
}
</div>
</div>
</FormGroup>
<FormGroup>
<FormLabel>Tags</FormLabel>

View File

@@ -36,7 +36,7 @@ class MoreInfo extends Component {
<DescriptionListItemTitle>Discord</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://discord.gg/M6BvZn5">discord.gg/M6BvZn5</Link>
<Link to="https://discord.gg/73QUuf3bgA">discord.gg/73QUuf3bgA</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>IRC</DescriptionListItemTitle>

View File

@@ -179,6 +179,38 @@ namespace NzbDrone.Common.Test.Http
ExceptionVerification.IgnoreWarns();
}
[Test]
public void should_not_throw_on_suppressed_status_codes()
{
var request = new HttpRequest($"http://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound };
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.IgnoreWarns();
}
[Test]
public void should_log_unsuccessful_status_codes()
{
var request = new HttpRequest($"http://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_not_log_unsuccessful_status_codes()
{
var request = new HttpRequest($"http://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.LogHttpError = false;
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.ExpectedWarns(0);
}
[Test]
public void should_not_follow_redirects_when_not_in_production()
{

View File

@@ -85,8 +85,7 @@ namespace NzbDrone.Common.EnvironmentInfo
if (_diskProvider.FileExists(_appFolderInfo.GetDatabase())) return;
if (!_diskProvider.FileExists(oldDbFile)) return;
_diskProvider.MoveFile(oldDbFile, _appFolderInfo.GetDatabase());
CleanupSqLiteRollbackFiles();
MoveSqliteDatabase(oldDbFile, _appFolderInfo.GetDatabase());
RemovePidFile();
}
@@ -108,12 +107,9 @@ namespace NzbDrone.Common.EnvironmentInfo
// Rename the DB file
if (_diskProvider.FileExists(oldDbFile))
{
_diskProvider.MoveFile(oldDbFile, _appFolderInfo.GetDatabase());
MoveSqliteDatabase(oldDbFile, _appFolderInfo.GetDatabase());
}
// Remove SQLite rollback files
CleanupSqLiteRollbackFiles();
// Remove Old PID file
RemovePidFile();
@@ -127,7 +123,6 @@ namespace NzbDrone.Common.EnvironmentInfo
}
}
private void InitializeMonoApplicationData()
{
if (OsInfo.IsWindows) return;
@@ -158,12 +153,37 @@ namespace NzbDrone.Common.EnvironmentInfo
}
}
private void CleanupSqLiteRollbackFiles()
private void MoveSqliteDatabase(string source, string destination)
{
_diskProvider.GetFiles(_appFolderInfo.AppDataFolder, SearchOption.TopDirectoryOnly)
.Where(f => Path.GetFileName(f).StartsWith("nzbdrone.db"))
.ToList()
.ForEach(_diskProvider.DeleteFile);
_logger.Info("Moving {0}* to {1}*", source, destination);
var dbSuffixes = new[] { "", "-shm", "-wal", "-journal" };
foreach (var suffix in dbSuffixes)
{
var sourceFile = source + suffix;
var destFile = destination + suffix;
if (_diskProvider.FileExists(destFile))
{
_diskProvider.DeleteFile(destFile);
}
if (_diskProvider.FileExists(sourceFile))
{
_diskProvider.CopyFile(sourceFile, destFile);
}
}
foreach (var suffix in dbSuffixes)
{
var sourceFile = source + suffix;
if (_diskProvider.FileExists(sourceFile))
{
_diskProvider.DeleteFile(sourceFile);
}
}
}
private void RemovePidFile()

View File

@@ -85,9 +85,12 @@ namespace NzbDrone.Common.Http
_logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]);
}
if (!request.SuppressHttpError && response.HasHttpError)
if (!request.SuppressHttpError && response.HasHttpError && (request.SuppressHttpErrorStatusCodes == null || !request.SuppressHttpErrorStatusCodes.Contains(response.StatusCode)))
{
_logger.Warn("HTTP Error - {0}", response);
if (request.LogHttpError)
{
_logger.Warn("HTTP Error - {0}", response);
}
if ((int)response.StatusCode == 429)
{

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
@@ -15,6 +16,7 @@ namespace NzbDrone.Common.Http
Headers = new HttpHeader();
AllowAutoRedirect = true;
StoreRequestCookie = true;
LogHttpError = true;
Cookies = new Dictionary<string, string>();
@@ -35,10 +37,12 @@ namespace NzbDrone.Common.Http
public byte[] ContentData { get; set; }
public string ContentSummary { get; set; }
public bool SuppressHttpError { get; set; }
public IEnumerable<HttpStatusCode> SuppressHttpErrorStatusCodes { get; set; }
public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; }
public bool LogResponseContent { get; set; }
public bool LogHttpError { get; set; }
public Dictionary<string, string> Cookies { get; private set; }
public bool StoreRequestCookie { get; set; }
public bool StoreResponseCookie { get; set; }

View File

@@ -19,6 +19,7 @@ namespace NzbDrone.Common.Http
public Dictionary<string, string> Segments { get; private set; }
public HttpHeader Headers { get; private set; }
public bool SuppressHttpError { get; set; }
public bool LogHttpError { get; set; }
public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; }
@@ -41,6 +42,7 @@ namespace NzbDrone.Common.Http
Headers = new HttpHeader();
Cookies = new Dictionary<string, string>();
FormData = new List<HttpFormData>();
LogHttpError = true;
}
public HttpRequestBuilder(bool useHttps, string host, int port, string urlBase = null)
@@ -101,6 +103,7 @@ namespace NzbDrone.Common.Http
{
request.Method = Method;
request.SuppressHttpError = SuppressHttpError;
request.LogHttpError = LogHttpError;
request.UseSimplifiedUserAgent = UseSimplifiedUserAgent;
request.AllowAutoRedirect = AllowAutoRedirect;
request.ConnectionKeepAlive = ConnectionKeepAlive;

View File

@@ -0,0 +1,100 @@
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.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class email_multiple_addressesFixture : MigrationTest<email_multiple_addresses>
{
[Test]
public void should_convert_to_list_on_email_lists()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Notifications").Row(new
{
OnGrab = true,
OnDownload = true,
OnUpgrade = true,
OnHealthIssue = true,
IncludeHealthWarnings = true,
OnRename = true,
Name = "Mail Sonarr",
Implementation = "Email",
Tags = "[]",
Settings = new EmailSettings173
{
Server = "smtp.gmail.com",
Port = 563,
To = "dont@email.me"
}.ToJson(),
ConfigContract = "EmailSettings"
});
});
var items = db.Query<NotificationDefinition173>("SELECT * FROM Notifications");
items.Should().HaveCount(1);
items.First().Implementation.Should().Be("Email");
items.First().ConfigContract.Should().Be("EmailSettings");
items.First().Settings.To.Count().Should().Be(1);
}
}
public class NotificationDefinition173
{
public int Id { get; set; }
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public EmailSettings174 Settings { get; set; }
public string Name { get; set; }
public bool OnGrab { get; set; }
public bool OnDownload { get; set; }
public bool OnUpgrade { get; set; }
public bool OnRename { get; set; }
public bool OnSeriesDelete { get; set; }
public bool OnEpisodeFileDelete { get; set; }
public bool OnEpisodeFileDeleteForUpgrade { get; set; }
public bool OnHealthIssue { get; set; }
public bool SupportsOnGrab { get; set; }
public bool SupportsOnDownload { get; set; }
public bool SupportsOnUpgrade { get; set; }
public bool SupportsOnRename { get; set; }
public bool SupportsOnSeriesDelete { get; set; }
public bool SupportsOnEpisodeFileDelete { get; set; }
public bool SupportsOnEpisodeFileDeleteForUpgrade { get; set; }
public bool SupportsOnHealthIssue { get; set; }
public bool IncludeHealthWarnings { get; set; }
public List<int> Tags { get; set; }
}
public class EmailSettings173
{
public string Server { get; set; }
public int Port { get; set; }
public bool RequireEncryption { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string From { get; set; }
public string To { get; set; }
}
public class EmailSettings174
{
public string Server { get; set; }
public int Port { get; set; }
public bool RequireEncryption { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string From { get; set; }
public IEnumerable<string> To { get; set; }
public IEnumerable<string> Cc { get; set; }
public IEnumerable<string> Bcc { get; set; }
}
}

View File

@@ -25,6 +25,7 @@ namespace NzbDrone.Core.Test.Datastore.SqliteSchemaDumperTests
[TestCase(@"CREATE TABLE ""Test """"Table"" (""My""""Id"" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT)", "Test \"Table", "My\"Id")]
[TestCase(@"CREATE TABLE [Test Table] ([My Id] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT)", "Test Table", "My Id")]
[TestCase(@" CREATE TABLE `Test ``Table` ( `My`` Id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) ", "Test `Table", "My` Id")]
[TestCase(@"CREATE TABLE TestTable (MyId INTEGER NOT NULL, PRIMARY KEY(""MyId"" AUTOINCREMENT))", "TestTable", "MyId")]
public void should_parse_table_language_flavors(string sql, string tableName, string columnName)
{
var result = Subject.ReadTableSchema(sql);

View File

@@ -12,6 +12,7 @@ using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.QBittorrent;
using NzbDrone.Test.Common;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Download.Clients;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
{
@@ -71,19 +72,23 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
protected void GivenFailedDownload()
{
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()))
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<TorrentSeedConfiguration>(), It.IsAny<QBittorrentSettings>()))
.Throws<InvalidOperationException>();
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.AddTorrentFromFile(It.IsAny<string>(), It.IsAny<byte[]>(), It.IsAny<TorrentSeedConfiguration>(), It.IsAny<QBittorrentSettings>()))
.Throws<InvalidOperationException>();
}
protected void GivenSuccessfulDownload()
{
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()))
.Setup(s => s.AddTorrentFromFile(It.IsAny<string>(), It.IsAny<byte[]>(), It.IsAny<TorrentSeedConfiguration>(), It.IsAny<QBittorrentSettings>()))
.Callback(() =>
{
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Hash = "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951",
Name = _title,
Size = 1000,
Progress = 1.0,
@@ -135,6 +140,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
.Setup(s => s.GetTorrentFiles(torrent.Hash.ToLower(), It.IsAny<QBittorrentSettings>()))
.Returns(new List<QBittorrentTorrentFile> { new QBittorrentTorrentFile { Name = torrent.Name } });
}
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.IsTorrentLoaded(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()))
.Returns<string, QBittorrentSettings>((hash, s) => torrents.Any(v => v.Hash.ToLower() == hash));
}
private void GivenTorrentFiles(string hash, List<QBittorrentTorrentFile> files)
@@ -466,7 +475,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Assert.DoesNotThrow(() => Subject.Download(remoteEpisode));
Mocker.GetMock<IQBittorrentProxy>()
.Verify(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once());
.Verify(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<TorrentSeedConfiguration>(), It.IsAny<QBittorrentSettings>()), Times.Once());
}
[Test]

View File

@@ -0,0 +1,114 @@
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Notifications.Email;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.NotificationTests.EmailTests
{
[TestFixture]
public class EmailSettingsValidatorFixture : CoreTest<EmailSettingsValidator>
{
private EmailSettings _emailSettings;
private TestValidator<EmailSettings> _validator;
[SetUp]
public void Setup()
{
_validator = new TestValidator<EmailSettings>
{
v => v.RuleFor(s => s).SetValidator(Subject)
};
_emailSettings = Builder<EmailSettings>.CreateNew()
.With(s => s.Server = "someserver")
.With(s => s.Port = 567)
.With(s => s.RequireEncryption = true)
.With(s => s.From = "dont@email.me")
.With(s => s.To = new string[] { "dont@email.me" })
.Build();
}
[Test]
public void should_be_valid_if_all_settings_valid()
{
_validator.Validate(_emailSettings).IsValid.Should().BeTrue();
}
[Test]
public void should_not_be_valid_if_port_is_out_of_range()
{
_emailSettings.Port = 900000;
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[Test]
public void should_not_be_valid_if_server_is_empty()
{
_emailSettings.Server = "";
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[Test]
public void should_not_be_valid_if_from_is_empty()
{
_emailSettings.From = "";
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[TestCase("sonarr")]
[TestCase("sonarr@sonarr")]
[TestCase("email.me")]
[Ignore("Allowed coz some email servers allow arbitrary source, we probably need to support 'Name <email>' syntax")]
public void should_not_be_valid_if_from_is_invalid(string email)
{
_emailSettings.From = email;
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[TestCase("sonarr")]
[TestCase("sonarr@sonarr")]
[TestCase("email.me")]
public void should_not_be_valid_if_to_is_invalid(string email)
{
_emailSettings.To = new string[] { email };
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[TestCase("sonarr")]
[TestCase("sonarr@sonarr")]
[TestCase("email.me")]
public void should_not_be_valid_if_cc_is_invalid(string email)
{
_emailSettings.Cc = new string[] { email };
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[TestCase("sonarr")]
[TestCase("sonarr@sonarr")]
[TestCase("email.me")]
public void should_not_be_valid_if_bcc_is_invalid(string email)
{
_emailSettings.Bcc = new string[] { email };
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[Test]
public void should_not_be_valid_if_to_bcc_cc_are_all_empty()
{
_emailSettings.To = new string[] { };
_emailSettings.Cc = new string[] { };
_emailSettings.Bcc = new string[] { };
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
}
}

View File

@@ -100,6 +100,9 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[HorribleSubs] Series 100 - 07 [1080p].mkv", "Series 100", 7, 0, 0)]
[TestCase("[HorribleSubs] Series 100 S2 - 07 [1080p].mkv", "Series 100 S2", 7, 0, 0)]
[TestCase("[abc] Adventure Series: 30 [Web][MKV][h264][720p][AAC 2.0][abc]", "Adventure Series:", 30, 0, 0)]
[TestCase("[XKsub] Series Title S2 [05][HEVC-10bit 1080p AAC][CHS&CHT&JPN]", "Series Title S2", 5, 0, 0)]
[TestCase("[Cheetah-Raws] Super Long Anime - 1000 (YTV 1280x720 x264 AAC)", "Super Long Anime", 1000, 0, 0)]
[TestCase("[DameDesuYo] ReZero kara Hajimeru Isekai Seikatsu (Season 2) - 33 (1280x720 10bit EAC3) [42A12A76].mkv", "ReZero kara Hajimeru Isekai Seikatsu", 33, 2, 0)]
//[TestCase("", "", 0, 0, 0)]
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
{
@@ -140,6 +143,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[Judas] Some Anime Show 091-123 [1080p][HEVC x265 10bit][Dual-Audio][Multi-Subs]", "Some Anime Show", 91, 123 )]
[TestCase("[Judas] Some Anime Show - 091-123 [1080p][HEVC x265 10bit][Dual-Audio][Multi-Subs]", "Some Anime Show", 91, 123)]
[TestCase("[HorribleSubs] Some Anime Show 01 - 119 [1080p] [Batch]", "Some Anime Show", 1, 119)]
[TestCase("[Erai-raws] Series Title! - 01~10 [1080p][Multiple Subtitle]", "Series Title!", 1, 10)]
[TestCase("[Erai-raws] Series Title! 2 - 01~10 [1080p][Multiple Subtitle]", "Series Title! 2", 1, 10)]
// [TestCase("", "", 1, 2)]
public void should_parse_multi_episode_absolute_numbers(string postTitle, string title, int firstAbsoluteEpisodeNumber, int lastAbsoluteEpisodeNumber)
{

View File

@@ -36,16 +36,13 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("or")]
[TestCase("an")]
[TestCase("of")]
public void should_remove_common_words(string word)
public void should_remove_common_words_from_middle_of_title(string word)
{
var dirtyFormat = new[]
{
"word.{0}.word",
"word {0} word",
"word-{0}-word",
"word.word.{0}",
"word-word-{0}",
"word-word {0}",
"word-{0}-word"
};
foreach (var s in dirtyFormat)
@@ -55,6 +52,27 @@ namespace NzbDrone.Core.Test.ParserTests
}
}
[TestCase("the")]
[TestCase("and")]
[TestCase("or")]
[TestCase("an")]
[TestCase("of")]
public void should_not_remove_common_words_from_end_of_title(string word)
{
var dirtyFormat = new[]
{
"word.word.{0}",
"word-word-{0}",
"word-word {0}"
};
foreach (var s in dirtyFormat)
{
var dirty = string.Format(s, word);
dirty.CleanSeriesTitle().Should().Be("wordword" + word.ToLower());
}
}
[Test]
public void should_remove_a_from_middle_of_title()
{

View File

@@ -268,6 +268,26 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
.Verify(v => v.FindEpisode(It.IsAny<int>(), seasonNumber, It.IsAny<int>()), Times.Never());
}
[TestCase(2)]
[TestCase(20)]
public void should_find_episode_by_parsed_season_and_absolute_episode_number_when_season_number_is_2_or_higher(int seasonNumber)
{
GivenAbsoluteNumberingSeries();
_parsedEpisodeInfo.SeasonNumber = seasonNumber;
Mocker.GetMock<IEpisodeService>()
.Setup(s => s.FindEpisodesBySceneNumbering(It.IsAny<int>(), seasonNumber, It.IsAny<int>()))
.Returns(new List<Episode> { _episodes.First() });
Subject.GetEpisodes(_parsedEpisodeInfo, _series, true, null);
Mocker.GetMock<IEpisodeService>()
.Verify(v => v.FindEpisodesBySceneNumbering(It.IsAny<int>(), seasonNumber, It.IsAny<int>()), Times.Once());
Mocker.GetMock<IEpisodeService>()
.Verify(v => v.FindEpisode(It.IsAny<int>(), seasonNumber, It.IsAny<int>()), Times.Never());
}
[TestCase(0)]
[TestCase(1)]
[TestCase(2)]

View File

@@ -142,6 +142,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("tvs-amgo-dd51-dl-7p-azhd-x264-103", "tvs-amgo-dd51-dl-7p-azhd", 1, 3)]
[TestCase("Series Title - S01E01 [AC3 5.1 Castellano][www.descargas2020.org]", "Series Title", 1, 1)]
[TestCase("Series Title - [02x01] - Episode 1", "Series Title", 2, 1)]
[TestCase("Series.Title.Of.S01E01.xyz", "Series Title Of", 1, 1)]
//[TestCase("", "", 0, 0)]
public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber)
{

View File

@@ -0,0 +1,50 @@
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(157)]
public class email_multiple_addresses : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.WithConnection(ChangeEmailAddressType);
}
private void ChangeEmailAddressType(IDbConnection conn, IDbTransaction tran)
{
using (var getEmailCmd = conn.CreateCommand())
{
getEmailCmd.Transaction = tran;
getEmailCmd.CommandText = "SELECT Id, Settings FROM Notifications WHERE Implementation = 'Email'";
using (var reader = getEmailCmd.ExecuteReader())
{
while (reader.Read())
{
var id = reader.GetInt32(0);
var settings = Json.Deserialize<JObject>(reader.GetString(1));
// "To" was changed from string to array
settings["to"] = new JArray(settings["to"].ToObject<string>().Split(',').Select(v => v.Trim()).ToArray());
using (var updateCmd = conn.CreateCommand())
{
updateCmd.Transaction = tran;
updateCmd.CommandText = "UPDATE Notifications SET Settings = ? WHERE Id = ?";
updateCmd.AddParameter(settings.ToJson());
updateCmd.AddParameter(id);
updateCmd.ExecuteNonQuery();
}
}
}
}
}
}
}

View File

@@ -24,7 +24,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
{
var sw = Stopwatch.StartNew();
_announcer.Heading("Migrating " + connectionString);
_announcer.Heading("Checking database for required migrations " + connectionString);
var assembly = Assembly.GetExecutingAssembly();

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Data;
using System.Linq;
using FluentMigrator.Model;
using FluentMigrator.Runner;
using FluentMigrator.Runner.Processors.SQLite;
@@ -67,6 +68,23 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
if (columnReader.Read() == SqliteSyntaxReader.TokenType.StringToken)
{
if (columnReader.ValueToUpper == "PRIMARY")
{
columnReader.SkipTillToken(SqliteSyntaxReader.TokenType.ListStart);
if (columnReader.Read() == SqliteSyntaxReader.TokenType.Identifier)
{
var pk = table.Columns.First(v => v.Name == columnReader.Value);
pk.IsPrimaryKey = true;
pk.IsNullable = true;
pk.IsUnique = true;
if (columnReader.Buffer.ToUpperInvariant().Contains("AUTOINCREMENT"))
{
pk.IsIdentity = true;
}
continue;
}
}
if (columnReader.ValueToUpper == "CONSTRAINT" ||
columnReader.ValueToUpper == "PRIMARY" || columnReader.ValueToUpper == "UNIQUE" ||
columnReader.ValueToUpper == "CHECK" || columnReader.ValueToUpper == "FOREIGN")

View File

@@ -148,9 +148,9 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
{
var start = Index;
var end = start + 1;
while (end < Buffer.Length && (char.IsLetter(Buffer[end]) || char.IsNumber(Buffer[end]) || Buffer[end] == '_' || Buffer[end] == '(')) end++;
while (end < Buffer.Length && (char.IsLetter(Buffer[end]) || char.IsNumber(Buffer[end]) || Buffer[end] == '_')) end++;
if (end >= Buffer.Length || Buffer[end] == ',' || Buffer[end] == ')' || char.IsWhiteSpace(Buffer[end]))
if (end >= Buffer.Length || Buffer[end] == ',' || Buffer[end] == '(' || Buffer[end] == ')' || char.IsWhiteSpace(Buffer[end]))
{
Index = end;
}

View File

@@ -41,7 +41,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
if (size > maximumSize)
{
var message = $"{size.SizeSuffix()} is too big, maximum size is {maximumSize.SizeSuffix()}";
var message = $"{size.SizeSuffix()} is too big, maximum size is {maximumSize.SizeSuffix()} (Settings->Indexers->Maximum Size)";
_logger.Debug(message);
return Decision.Reject(message);

View File

@@ -35,30 +35,30 @@ namespace NzbDrone.Core.Download.Clients.Deluge
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the deluge json url, see http://[host]:[port]/[urlBase]/json")]
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Deluge")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the deluge json url, see http://[host]:[port]/[urlBase]/json")]
public string UrlBase { get; set; }
[FieldDefinition(3, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
[FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
public string TvCategory { get; set; }
[FieldDefinition(5, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Sonarr to set after it has imported the download. Sonarr will not remove torrents in that category even if seeding finished. Leave blank to keep same category.")]
[FieldDefinition(6, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Sonarr to set after it has imported the download. Sonarr will not remove torrents in that category even if seeding finished. Leave blank to keep same category.")]
public string TvImportedCategory { get; set; }
[FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
[FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public int RecentTvPriority { get; set; }
[FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
[FieldDefinition(8, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public int OlderTvPriority { get; set; }
[FieldDefinition(8, Label = "Add Paused", Type = FieldType.Checkbox)]
[FieldDefinition(9, Label = "Add Paused", Type = FieldType.Checkbox)]
public bool AddPaused { get; set; }
[FieldDefinition(9, Label = "Use SSL", Type = FieldType.Checkbox)]
public bool UseSsl { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -36,21 +36,21 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(2, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Download Station")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(3, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
[FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")]
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")]
public string TvCategory { get; set; }
[FieldDefinition(5, Label = "Directory", Type = FieldType.Textbox, HelpText = "Optional shared folder to put downloads into, leave blank to use the default Download Station location")]
[FieldDefinition(6, Label = "Directory", Type = FieldType.Textbox, HelpText = "Optional shared folder to put downloads into, leave blank to use the default Download Station location")]
public string TvDirectory { get; set; }
[FieldDefinition(6, Label = "Use SSL", Type = FieldType.Checkbox)]
public bool UseSsl { get; set; }
public DownloadStationSettings()
{
this.Host = "127.0.0.1";

View File

@@ -34,15 +34,15 @@ namespace NzbDrone.Core.Download.Clients.Flood
StartOnAdd = true;
}
[FieldDefinition(0, Label = "Use SSL", Type = FieldType.Checkbox)]
public bool UseSsl { get; set; }
[FieldDefinition(1, Label = "Host", Type = FieldType.Textbox)]
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)]
public string Host { get; set; }
[FieldDefinition(2, Label = "Port", Type = FieldType.Textbox)]
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Flood")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, HelpText = "Optionally adds a prefix to Flood API, such as [protocol]://[host]:[port]/[urlBase]api")]
public string UrlBase { get; set; }

View File

@@ -39,21 +39,21 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the Hadouken url, e.g. http://[host]:[port]/[urlBase]/api")]
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Hadouken")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the Hadouken url, e.g. http://[host]:[port]/[urlBase]/api")]
public string UrlBase { get; set; }
[FieldDefinition(3, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox)]
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox)]
public string Category { get; set; }
[FieldDefinition(6, Label = "Use SSL", Type = FieldType.Checkbox, Advanced = true)]
public bool UseSsl { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -40,30 +40,30 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the nzbget url, e.g. http://[host]:[port]/[urlBase]/jsonrpc")]
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Sabnzbd")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the nzbget url, e.g. http://[host]:[port]/[urlBase]/jsonrpc")]
public string UrlBase { get; set; }
[FieldDefinition(3, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
public string TvCategory { get; set; }
[FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
[FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public int RecentTvPriority { get; set; }
[FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
[FieldDefinition(8, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public int OlderTvPriority { get; set; }
[FieldDefinition(8, Label = "Add Paused", Type = FieldType.Checkbox, HelpText = "This option requires at least NzbGet version 16.0")]
[FieldDefinition(9, Label = "Add Paused", Type = FieldType.Checkbox, HelpText = "This option requires at least NzbGet version 16.0")]
public bool AddPaused { get; set; }
[FieldDefinition(9, Label = "Use SSL", Type = FieldType.Checkbox)]
public bool UseSsl { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -43,6 +43,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
private IQBittorrentProxy Proxy => _proxySelector.GetProxy(Settings);
private Version ProxyApiVersion => _proxySelector.GetApiVersion(Settings);
public override void MarkItemAsImported(DownloadClientItem downloadClientItem)
{
@@ -69,21 +70,57 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
throw new NotSupportedException("Magnet Links without trackers not supported if DHT is disabled");
}
Proxy.AddTorrentFromUrl(magnetLink, Settings);
var setShareLimits = remoteEpisode.SeedConfiguration != null && (remoteEpisode.SeedConfiguration.Ratio.HasValue || remoteEpisode.SeedConfiguration.SeedTime.HasValue);
var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1);
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
var moveToTop = (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First || !isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First);
var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart;
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
Proxy.AddTorrentFromUrl(magnetLink, addHasSetShareLimits && setShareLimits ? remoteEpisode.SeedConfiguration : null, Settings);
if (!addHasSetShareLimits && setShareLimits || moveToTop || forceStart)
{
Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
}
SetInitialState(hash.ToLower());
if (!WaitForTorrent(hash))
{
return hash;
}
if (remoteEpisode.SeedConfiguration != null && (remoteEpisode.SeedConfiguration.Ratio.HasValue || remoteEpisode.SeedConfiguration.SeedTime.HasValue))
{
Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings);
if (!addHasSetShareLimits && setShareLimits)
{
try
{
Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings);
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to set the torrent seed criteria for {0}.", hash);
}
}
if (moveToTop)
{
try
{
Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to set the torrent priority for {0}.", hash);
}
}
if (forceStart)
{
try
{
Proxy.SetForceStart(hash.ToLower(), true, Settings);
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to set ForceStart for {0}.", hash);
}
}
}
return hash;
@@ -91,33 +128,88 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, Byte[] fileContent)
{
Proxy.AddTorrentFromFile(filename, fileContent, Settings);
var setShareLimits = remoteEpisode.SeedConfiguration != null && (remoteEpisode.SeedConfiguration.Ratio.HasValue || remoteEpisode.SeedConfiguration.SeedTime.HasValue);
var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1);
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
var moveToTop = (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First || !isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First);
var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart;
try
Proxy.AddTorrentFromFile(filename, fileContent, addHasSetShareLimits ? remoteEpisode.SeedConfiguration : null, Settings);
if (!addHasSetShareLimits && setShareLimits || moveToTop || forceStart)
{
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
if (!WaitForTorrent(hash))
{
Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
return hash;
}
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to set the torrent priority for {0}.", filename);
if (!addHasSetShareLimits && setShareLimits)
{
try
{
Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings);
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to set the torrent seed criteria for {0}.", hash);
}
}
SetInitialState(hash.ToLower());
if (moveToTop)
{
try
{
Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to set the torrent priority for {0}.", hash);
}
}
if (remoteEpisode.SeedConfiguration != null && (remoteEpisode.SeedConfiguration.Ratio.HasValue || remoteEpisode.SeedConfiguration.SeedTime.HasValue))
{
Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings);
if (forceStart)
{
try
{
Proxy.SetForceStart(hash.ToLower(), true, Settings);
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to set ForceStart for {0}.", hash);
}
}
}
return hash;
}
protected bool WaitForTorrent(string hash)
{
var count = 10;
while (count != 0)
{
try
{
if (Proxy.IsTorrentLoaded(hash.ToLower(), Settings))
{
return true;
}
}
catch
{
}
_logger.Trace("Torrent '{0}' not yet visible in qbit, waiting 100ms.", hash);
System.Threading.Thread.Sleep(100);
count--;
}
_logger.Warn("Failed to load torrent '{0}' within 500 ms, skipping additional parameters.", hash);
return false;
}
public override string Name => "qBittorrent";
public override IEnumerable<DownloadClientItem> GetItems()
@@ -456,29 +548,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return null;
}
private void SetInitialState(string hash)
{
try
{
switch ((QBittorrentState)Settings.InitialState)
{
case QBittorrentState.ForceStart:
Proxy.SetForceStart(hash, true, Settings);
break;
case QBittorrentState.Start:
Proxy.ResumeTorrent(hash, Settings);
break;
case QBittorrentState.Pause:
Proxy.PauseTorrent(hash, Settings);
break;
}
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to set inital state for {0}.", hash);
}
}
protected TimeSpan? GetRemainingTime(QBittorrentTorrent torrent)
{
if (torrent.Eta < 0 || torrent.Eta > 365 * 24 * 3600)

View File

@@ -16,11 +16,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
string GetVersion(QBittorrentSettings settings);
QBittorrentPreferences GetConfig(QBittorrentSettings settings);
List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings);
bool IsTorrentLoaded(string hash, QBittorrentSettings settings);
QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings);
List<QBittorrentTorrentFile> GetTorrentFiles(string hash, QBittorrentSettings settings);
void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings);
void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings);
void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
void AddTorrentFromFile(string fileName, Byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings);
void SetTorrentLabel(string hash, string label, QBittorrentSettings settings);
@@ -36,12 +37,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public interface IQBittorrentProxySelector
{
IQBittorrentProxy GetProxy(QBittorrentSettings settings, bool force = false);
Version GetApiVersion(QBittorrentSettings settings, bool force = false);
}
public class QBittorrentProxySelector : IQBittorrentProxySelector
{
private readonly IHttpClient _httpClient;
private readonly ICached<IQBittorrentProxy> _proxyCache;
private readonly ICached<Tuple<IQBittorrentProxy, Version>> _proxyCache;
private readonly Logger _logger;
private readonly IQBittorrentProxy _proxyV1;
@@ -49,12 +50,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public QBittorrentProxySelector(QBittorrentProxyV1 proxyV1,
QBittorrentProxyV2 proxyV2,
IHttpClient httpClient,
ICacheManager cacheManager,
Logger logger)
{
_httpClient = httpClient;
_proxyCache = cacheManager.GetCache<IQBittorrentProxy>(GetType());
_proxyCache = cacheManager.GetCache<Tuple<IQBittorrentProxy, Version>>(GetType());
_logger = logger;
_proxyV1 = proxyV1;
@@ -62,6 +61,16 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
public IQBittorrentProxy GetProxy(QBittorrentSettings settings, bool force)
{
return GetProxyCache(settings, force).Item1;
}
public Version GetApiVersion(QBittorrentSettings settings, bool force)
{
return GetProxyCache(settings, force).Item2;
}
private Tuple<IQBittorrentProxy, Version> GetProxyCache(QBittorrentSettings settings, bool force)
{
var proxyKey = $"{settings.Host}_{settings.Port}";
@@ -70,21 +79,21 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
_proxyCache.Remove(proxyKey);
}
return _proxyCache.Get(proxyKey, () => FetchProxy(settings), TimeSpan.FromMinutes(10.0));
return _proxyCache.Get(proxyKey, () => FetchProxy(settings), TimeSpan.FromMinutes(10.0));
}
private IQBittorrentProxy FetchProxy(QBittorrentSettings settings)
private Tuple<IQBittorrentProxy, Version> FetchProxy(QBittorrentSettings settings)
{
if (_proxyV2.IsApiSupported(settings))
{
_logger.Trace("Using qbitTorrent API v2");
return _proxyV2;
return Tuple.Create(_proxyV2, _proxyV2.GetApiVersion(settings));
}
if (_proxyV1.IsApiSupported(settings))
{
_logger.Trace("Using qbitTorrent API v1");
return _proxyV1;
return Tuple.Create(_proxyV1, _proxyV1.GetApiVersion(settings));
}
throw new DownloadClientException("Unable to determine qBittorrent API version");

View File

@@ -98,6 +98,23 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return response;
}
public bool IsTorrentLoaded(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource($"/query/propertiesGeneral/{hash}");
request.LogHttpError = false;
try
{
ProcessRequest(request, settings);
return true;
}
catch
{
return false;
}
}
public QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource($"/query/propertiesGeneral/{hash}");
@@ -114,7 +131,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return response;
}
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
public void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/command/download")
.Post()
@@ -125,7 +142,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
request.AddFormParameter("category", settings.TvCategory);
}
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
// Note: ForceStart is handled by separate api call
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
{
request.AddFormParameter("paused", false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{
request.AddFormParameter("paused", true);
}
@@ -139,7 +161,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
}
public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings)
public void AddTorrentFromFile(string fileName, Byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/command/upload")
.Post()
@@ -150,9 +172,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
request.AddFormParameter("category", settings.TvCategory);
}
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
// Note: ForceStart is handled by separate api call
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
{
request.AddFormParameter("paused", "true");
request.AddFormParameter("paused", false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{
request.AddFormParameter("paused", true);
}
var result = ProcessRequest(request, settings);
@@ -287,15 +314,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var request = requestBuilder.Build();
request.LogResponseContent = true;
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.Forbidden };
HttpResponse response;
try
{
response = _httpClient.Execute(request);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
if (response.StatusCode == HttpStatusCode.Forbidden)
{
_logger.Debug("Authentication required, logging in.");
@@ -305,10 +331,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
response = _httpClient.Execute(request);
}
else
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
}
}
catch (HttpException ex)
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
}
catch (WebException ex)
{

View File

@@ -101,6 +101,24 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return response;
}
public bool IsTorrentLoaded(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/properties")
.AddQueryParam("hash", hash);
request.LogHttpError = false;
try
{
ProcessRequest(request, settings);
return true;
}
catch
{
return false;
}
}
public QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/properties")
@@ -119,7 +137,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return response;
}
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
public void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
.Post()
@@ -130,11 +148,21 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
request.AddFormParameter("category", settings.TvCategory);
}
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
// Note: ForceStart is handled by separate api call
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
{
request.AddFormParameter("paused", false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{
request.AddFormParameter("paused", true);
}
if (seedConfiguration != null)
{
AddTorrentSeedingFormParameters(request, seedConfiguration);
}
var result = ProcessRequest(request, settings);
// Note: Older qbit versions returned nothing, so we can't do != "Ok." here.
@@ -144,7 +172,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
}
public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings)
public void AddTorrentFromFile(string fileName, Byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
.Post()
@@ -155,9 +183,19 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
request.AddFormParameter("category", settings.TvCategory);
}
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
// Note: ForceStart is handled by separate api call
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
{
request.AddFormParameter("paused", "true");
request.AddFormParameter("paused", false);
}
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{
request.AddFormParameter("paused", true);
}
if (seedConfiguration != null)
{
AddTorrentSeedingFormParameters(request, seedConfiguration);
}
var result = ProcessRequest(request, settings);
@@ -206,16 +244,29 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return Json.Deserialize<Dictionary<string, QBittorrentLabel>>(ProcessRequest(request, settings));
}
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
private void AddTorrentSeedingFormParameters(HttpRequestBuilder request, TorrentSeedConfiguration seedConfiguration, bool always = false)
{
var ratioLimit = seedConfiguration.Ratio.HasValue ? seedConfiguration.Ratio : -2;
var seedingTimeLimit = seedConfiguration.SeedTime.HasValue ? (long)seedConfiguration.SeedTime.Value.TotalMinutes : -2;
if (ratioLimit != -2 || always)
{
request.AddFormParameter("ratioLimit", ratioLimit);
}
if (seedingTimeLimit != -2 || always)
{
request.AddFormParameter("seedingTimeLimit", seedingTimeLimit);
}
}
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/setShareLimits")
.Post()
.AddFormParameter("hashes", hash)
.AddFormParameter("ratioLimit", ratioLimit)
.AddFormParameter("seedingTimeLimit", seedingTimeLimit);
.AddFormParameter("hashes", hash);
AddTorrentSeedingFormParameters(request, seedConfiguration, true);
try
{
@@ -305,15 +356,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var request = requestBuilder.Build();
request.LogResponseContent = true;
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.Forbidden };
HttpResponse response;
try
{
response = _httpClient.Execute(request);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
if (response.StatusCode == HttpStatusCode.Forbidden)
{
_logger.Debug("Authentication required, logging in.");
@@ -323,10 +373,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
response = _httpClient.Execute(request);
}
else
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
}
}
catch (HttpException ex)
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
}
catch (WebException ex)
{

View File

@@ -36,33 +36,33 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the qBittorrent url, e.g. http://[host]:[port]/[urlBase]/api")]
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the qBittorrent url, e.g. http://[host]:[port]/[urlBase]/api")]
public string UrlBase { get; set; }
[FieldDefinition(3, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
public string TvCategory { get; set; }
[FieldDefinition(6, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Sonarr to set after it has imported the download. Sonarr will not remove the torrent if seeding has finished. Leave blank to keep same category.")]
[FieldDefinition(7, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Sonarr to set after it has imported the download. Sonarr will not remove the torrent if seeding has finished. Leave blank to keep same category.")]
public string TvImportedCategory { get; set; }
[FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
[FieldDefinition(8, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public int RecentTvPriority { get; set; }
[FieldDefinition(8, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
[FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public int OlderTvPriority { get; set; }
[FieldDefinition(9, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "Initial state for torrents added to qBittorrent")]
[FieldDefinition(10, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "Initial state for torrents added to qBittorrent")]
public int InitialState { get; set; }
[FieldDefinition(10, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.")]
public bool UseSsl { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -51,30 +51,30 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the Sabnzbd url, e.g. http://[host]:[port]/[urlBase]/api")]
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Sabnzbd")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the Sabnzbd url, e.g. http://[host]:[port]/[urlBase]/api")]
public string UrlBase { get; set; }
[FieldDefinition(3, Label = "API Key", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)]
[FieldDefinition(4, Label = "API Key", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)]
public string ApiKey { get; set; }
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
[FieldDefinition(5, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
[FieldDefinition(6, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
[FieldDefinition(7, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
public string TvCategory { get; set; }
[FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
[FieldDefinition(8, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public int RecentTvPriority { get; set; }
[FieldDefinition(8, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
[FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public int OlderTvPriority { get; set; }
[FieldDefinition(9, Label = "Use SSL", Type = FieldType.Checkbox)]
public bool UseSsl { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -41,33 +41,33 @@ namespace NzbDrone.Core.Download.Clients.Transmission
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the transmission rpc url, eg http://[host]:[port]/[urlBase]/rpc, defaults to '/transmission/'")]
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Transmission")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the transmission rpc url, eg http://[host]:[port]/[urlBase]/rpc, defaults to '/transmission/'")]
public string UrlBase { get; set; }
[FieldDefinition(3, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")]
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")]
public string TvCategory { get; set; }
[FieldDefinition(6, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default Transmission location")]
[FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default Transmission location")]
public string TvDirectory { get; set; }
[FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
[FieldDefinition(8, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public int RecentTvPriority { get; set; }
[FieldDefinition(8, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
[FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public int OlderTvPriority { get; set; }
[FieldDefinition(9, Label = "Add Paused", Type = FieldType.Checkbox)]
[FieldDefinition(10, Label = "Add Paused", Type = FieldType.Checkbox)]
public bool AddPaused { get; set; }
[FieldDefinition(10, Label = "Use SSL", Type = FieldType.Checkbox)]
public bool UseSsl { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -37,12 +37,12 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(2, Label = "Url Path", Type = FieldType.Textbox, HelpText = "Path to the XMLRPC endpoint, see http(s)://[host]:[port]/[urlPath]. When using ruTorrent this usually is RPC2 or (path to ruTorrent)/plugins/rpc/rpc.php")]
public string UrlBase { get; set; }
[FieldDefinition(3, Label = "Use SSL", Type = FieldType.Checkbox)]
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to ruTorrent")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "Url Path", Type = FieldType.Textbox, HelpText = "Path to the XMLRPC endpoint, see http(s)://[host]:[port]/[urlPath]. When using ruTorrent this usually is RPC2 or (path to ruTorrent)/plugins/rpc/rpc.php")]
public string UrlBase { get; set; }
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }

View File

@@ -186,7 +186,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
private HttpRequestBuilder BuildRequest(UTorrentSettings settings)
{
var requestBuilder = new HttpRequestBuilder(false, settings.Host, settings.Port, settings.UrlBase)
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
.Resource("/gui/")
.KeepAlive()
.SetHeader("Cache-Control", "no-cache")

View File

@@ -34,28 +34,31 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the uTorrent url, e.g. http://[host]:[port]/[urlBase]/api")]
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to uTorrent")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the uTorrent url, e.g. http://[host]:[port]/[urlBase]/api")]
public string UrlBase { get; set; }
[FieldDefinition(3, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
public string TvCategory { get; set; }
[FieldDefinition(6, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Sonarr to set after it has imported the download. Sonarr will not remove the torrent if seeding has finished. Leave blank to keep same category.")]
[FieldDefinition(7, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Sonarr to set after it has imported the download. Sonarr will not remove the torrent if seeding has finished. Leave blank to keep same category.")]
public string TvImportedCategory { get; set; }
[FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
[FieldDefinition(8, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public int RecentTvPriority { get; set; }
[FieldDefinition(8, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
[FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public int OlderTvPriority { get; set; }
[FieldDefinition(9, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(UTorrentState), HelpText = "Initial state for torrents added to uTorrent")]
[FieldDefinition(10, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(UTorrentState), HelpText = "Initial state for torrents added to uTorrent")]
public int IntialState { get; set; }
public NzbDroneValidationResult Validate()

View File

@@ -1,4 +1,5 @@
using System.Net;
using System;
using System.Net;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Exceptions
@@ -7,7 +8,13 @@ namespace NzbDrone.Core.Exceptions
{
public HttpStatusCode StatusCode { get; private set; }
public NzbDroneClientException(HttpStatusCode statusCode, string message, params object[] args) : base(message, args)
public NzbDroneClientException(HttpStatusCode statusCode, string message, params object[] args)
: base(message, args)
{
StatusCode = statusCode;
}
public NzbDroneClientException(HttpStatusCode statusCode, string message, Exception innerException, params object[] args)
: base(message, innerException, args)
{
StatusCode = statusCode;
}

View File

@@ -50,20 +50,20 @@ namespace NzbDrone.Core.HealthCheck.Checks
// Migration helper logic
if (!downloadClientIsLocalHost)
{
return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Multi-Computer unsupported)", "Migrating_to_Completed_Download_Handling#Unsupported_download_client_on_different_computer");
return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Multi-Computer unsupported)", "#completed_failed_download_handling");
}
if (downloadClients.All(v => v.DownloadClient is Sabnzbd))
{
return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Sabnzbd)", "Migrating_to_Completed_Download_Handling#sabnzbd_enable_completed_download_handling");
return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Sabnzbd)", "#completed/failed_download_handling");
}
if (downloadClients.All(v => v.DownloadClient is Nzbget))
{
return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Nzbget)", "Migrating_to_Completed_Download_Handling#nzbget_enable_completed_download_handling");
return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Nzbget)", "#completed/failed_download_handling");
}
return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible", "Migrating_to_Completed_Download_Handling");
return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible", "#completed/failed_download_handling");
}
if (!_configService.EnableCompletedDownloadHandling)

View File

@@ -40,21 +40,21 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
return new HealthCheck(GetType(), HealthCheckResult.Error,
string.Format("Cannot install update because startup folder '{0}' is in an App Translocation folder.", startupFolder),
"Cannot install update because startup folder is in an App Translocation folder.");
"#cannot_install_update_because_startup_folder_is_in_an_App_Translocation_folder");
}
if (!_diskProvider.FolderWritable(startupFolder))
{
return new HealthCheck(GetType(), HealthCheckResult.Error,
string.Format("Cannot install update because startup folder '{0}' is not writable by the user '{1}'.", startupFolder, Environment.UserName),
"Cannot install update because startup folder is not writable by the user");
"#cannot_install_update_because_startup_folder_is_not_writable_by_the_user");
}
if (!_diskProvider.FolderWritable(uiFolder))
{
return new HealthCheck(GetType(), HealthCheckResult.Error,
string.Format("Cannot install update because UI folder '{0}' is not writable by the user '{1}'.", uiFolder, Environment.UserName),
"Cannot install update because UI folder is not writable by the user");
"#cannot_install_update_because_UI_folder_is_not_writable_by_the_user");
}
}

View File

@@ -23,6 +23,8 @@ namespace NzbDrone.Core.Messaging.Commands
lock (_mutex)
{
_items.Add(item);
Monitor.PulseAll(_mutex);
}
}
@@ -68,6 +70,8 @@ namespace NzbDrone.Core.Messaging.Commands
{
_items.Remove(command);
}
Monitor.PulseAll(_mutex);
}
}
public bool RemoveIfQueued(int id)
@@ -82,6 +86,8 @@ namespace NzbDrone.Core.Messaging.Commands
{
_items.Remove(command);
rval = true;
Monitor.PulseAll(_mutex);
}
}
@@ -101,18 +107,43 @@ namespace NzbDrone.Core.Messaging.Commands
public IEnumerable<CommandModel> GetConsumingEnumerable(CancellationToken cancellationToken)
{
cancellationToken.Register(PulseAllConsumers);
while (!cancellationToken.IsCancellationRequested)
{
if (TryGet(out var command))
CommandModel command = null;
lock (_mutex)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
if (!TryGet(out command))
{
Monitor.Wait(_mutex);
continue;
}
}
if (command != null)
{
yield return command;
}
Thread.Sleep(10);
}
}
public bool TryGet(out CommandModel item)
public void PulseAllConsumers()
{
// Signal all consumers to reevaluate cancellation token
lock (_mutex)
{
Monitor.PulseAll(_mutex);
}
}
private bool TryGet(out CommandModel item)
{
var rval = true;
item = default(CommandModel);

View File

@@ -183,6 +183,8 @@ namespace NzbDrone.Core.Messaging.Commands
public void Complete(CommandModel command, string message)
{
Update(command, CommandStatus.Completed, message);
_commandQueue.PulseAllConsumers();
}
public void Fail(CommandModel command, string message, Exception e)
@@ -190,6 +192,8 @@ namespace NzbDrone.Core.Messaging.Commands
command.Exception = e.ToString();
Update(command, CommandStatus.Failed, message);
_commandQueue.PulseAllConsumers();
}
public void Requeue()

View File

@@ -1,4 +1,5 @@
using System.Net;
using System;
using System.Net;
using NzbDrone.Core.Exceptions;
namespace NzbDrone.Core.MetadataSource.SkyHook
@@ -13,5 +14,10 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
: base(HttpStatusCode.ServiceUnavailable, message, args)
{
}
public SkyHookException(string message, Exception innerException, params object[] args)
: base(HttpStatusCode.ServiceUnavailable, message, innerException, args)
{
}
}
}

View File

@@ -109,14 +109,20 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
return httpResponse.Resource.SelectList(MapSearchResult);
}
catch (HttpException)
catch (HttpException ex)
{
throw new SkyHookException("Search for '{0}' failed. Unable to communicate with SkyHook.", title);
_logger.Warn(ex);
throw new SkyHookException("Search for '{0}' failed. Unable to communicate with SkyHook.", ex, title);
}
catch (WebException ex)
{
_logger.Warn(ex);
throw new SkyHookException("Search for '{0}' failed. Unable to communicate with SkyHook.", ex, title, ex.Message);
}
catch (Exception ex)
{
_logger.Warn(ex, ex.Message);
throw new SkyHookException("Search for '{0}' failed. Invalid response received from SkyHook.", title);
_logger.Warn(ex);
throw new SkyHookException("Search for '{0}' failed. Invalid response received from SkyHook.", ex, title);
}
}

View File

@@ -26,25 +26,11 @@ namespace NzbDrone.Core.Notifications.Email
public void SendEmail(EmailSettings settings, string subject, string body, bool htmlBody = false)
{
var email = new MimeMessage();
try
{
email.From.Add(MailboxAddress.Parse(settings.From));
}
catch (Exception ex)
{
_logger.Error(ex, "From email address '{0}' invalid", settings.From);
}
try
{
email.To.Add(MailboxAddress.Parse(settings.To));
}
catch (Exception ex)
{
_logger.Error(ex, "To email address '{0}' invalid", settings.To);
}
email.From.Add(ParseAddress("From", settings.From));
email.To.AddRange(settings.To.Select(x => ParseAddress("To", x)));
email.Cc.AddRange(settings.Cc.Select(x => ParseAddress("CC", x)));
email.Bcc.AddRange(settings.Bcc.Select(x => ParseAddress("BCC", x)));
email.Subject = subject;
email.Body = new TextPart(htmlBody ? "html" : "plain")
{
@@ -113,5 +99,18 @@ namespace NzbDrone.Core.Notifications.Email
return null;
}
private MailboxAddress ParseAddress(string type, string address)
{
try
{
return MailboxAddress.Parse(address);
}
catch (Exception ex)
{
_logger.Error(ex, "{0} email address '{1}' invalid", type, address);
throw;
}
}
}
}

View File

@@ -1,4 +1,7 @@
using FluentValidation;
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
@@ -12,7 +15,14 @@ namespace NzbDrone.Core.Notifications.Email
RuleFor(c => c.Server).NotEmpty();
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.From).NotEmpty();
RuleFor(c => c.To).NotEmpty();
RuleForEach(c => c.To).EmailAddress();
RuleForEach(c => c.Cc).EmailAddress();
RuleForEach(c => c.Bcc).EmailAddress();
// Only require one of three send fields to be set
RuleFor(c => c.To).NotEmpty().Unless(c => c.Bcc.Any() || c.Cc.Any());
RuleFor(c => c.Cc).NotEmpty().Unless(c => c.To.Any() || c.Bcc.Any());
RuleFor(c => c.Bcc).NotEmpty().Unless(c => c.To.Any() || c.Cc.Any());
}
}
@@ -23,6 +33,10 @@ namespace NzbDrone.Core.Notifications.Email
public EmailSettings()
{
Port = 587;
To = Array.Empty<string>();
Cc = Array.Empty<string>();
Bcc = Array.Empty<string>();
}
[FieldDefinition(0, Label = "Server", HelpText = "Hostname or IP of Email server")]
@@ -43,8 +57,14 @@ namespace NzbDrone.Core.Notifications.Email
[FieldDefinition(5, Label = "From Address")]
public string From { get; set; }
[FieldDefinition(6, Label = "Recipient Address")]
public string To { get; set; }
[FieldDefinition(6, Label = "Recipient Address(es)", HelpText = "Comma seperated list of email recipients")]
public IEnumerable<string> To { get; set; }
[FieldDefinition(7, Label = "CC Address(es)", HelpText = "Comma seperated list of email cc recipients", Advanced = true)]
public IEnumerable<string> Cc { get; set; }
[FieldDefinition(8, Label = "BCC Address(es)", HelpText = "Comma seperated list of email bcc recipients", Advanced = true)]
public IEnumerable<string> Bcc { get; set; }
public NzbDroneValidationResult Validate()
{

View File

@@ -68,6 +68,14 @@ namespace NzbDrone.Core.Parser
//Anime - [SubGroup] Title Season+Episode
new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:\s|\.).*?(?<hash>\[\w{8}\])?(?:$|\.)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Anime - [SubGroup] Title with trailing number Absolute Episode Number - Batch separated with tilde
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)(?:(?<![-_. ]|\b[0]\d+) - )[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+))~(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+))(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Anime - [SubGroup] Title with season number in brackets Absolute Episode Number
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)[_. ]+?\(Season[_. ](?<season>\d+)\)[-_. ]+?(?:[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Anime - [SubGroup] Title with trailing number Absolute Episode Number
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)(?:(?<![-_. ]|\b[0]\d+) - )(?:[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
@@ -223,6 +231,14 @@ namespace NzbDrone.Core.Parser
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[-_. ]+?(?:Episode[-_. ]+?)(?<absoluteepisode>\d{1}(\.\d{1,2})?(?!\d+))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - 4 digit absolute episode number
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ]+?(?<absoluteepisode>\d{4}(\.\d{1,2})?(?!\d+))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - Absolute episode number in square brackets
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ]+?\[(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+))\]",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Season only releases
new Regex(@"^(?<title>.+?)\W(?:S|Season|Saison)\W?(?<season>\d{1,2}(?!\d+))(\W+|_|$)(?<extras>EXTRAS|SUBPACK)?(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
@@ -368,7 +384,7 @@ namespace NzbDrone.Core.Parser
//Regex to detect whether the title was reversed.
private static readonly Regex ReversedTitleRegex = new Regex(@"(?:^|[-._ ])(p027|p0801|\d{2,3}E\d{2}S)[-._ ]", RegexOptions.Compiled);
private static readonly RegexReplace NormalizeRegex = new RegexReplace(@"((?:\b|_)(?<!^)(a(?!$)|an|the|and|or|of)(?:\b|_))|\W|_",
private static readonly RegexReplace NormalizeRegex = new RegexReplace(@"((?:\b|_)(?<!^)(a(?!$)|an|the|and|or|of)(?!$)(?:\b|_))|\W|_",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.Compiled);
@@ -736,36 +752,12 @@ namespace NzbDrone.Core.Parser
if (airYear < 1900)
{
var seasons = new List<int>();
foreach (Capture seasonCapture in matchCollection[0].Groups["season"].Captures)
{
int parsedSeason;
if (int.TryParse(seasonCapture.Value, out parsedSeason))
{
seasons.Add(parsedSeason);
lastSeasonEpisodeStringIndex = Math.Max(lastSeasonEpisodeStringIndex, seasonCapture.EndIndex());
}
}
//If no season was found it should be treated as a mini series and season 1
if (seasons.Count == 0) seasons.Add(1);
result = new ParsedEpisodeInfo
{
ReleaseTitle = releaseTitle,
SeasonNumber = seasons.First(),
EpisodeNumbers = new int[0],
AbsoluteEpisodeNumbers = new int[0]
};
//If more than 1 season was parsed set IsMultiSeason to true so it can be rejected later
if (seasons.Distinct().Count() > 1)
{
result.IsMultiSeason = true;
}
{
ReleaseTitle = releaseTitle,
EpisodeNumbers = new int[0],
AbsoluteEpisodeNumbers = new int[0]
};
foreach (Match matchGroup in matchCollection)
{
@@ -848,9 +840,34 @@ namespace NzbDrone.Core.Parser
}
}
if (result.AbsoluteEpisodeNumbers.Any() && !result.EpisodeNumbers.Any())
var seasons = new List<int>();
foreach (Capture seasonCapture in matchCollection[0].Groups["season"].Captures)
{
result.SeasonNumber = 0;
int parsedSeason;
if (int.TryParse(seasonCapture.Value, out parsedSeason))
{
seasons.Add(parsedSeason);
lastSeasonEpisodeStringIndex = Math.Max(lastSeasonEpisodeStringIndex, seasonCapture.EndIndex());
}
}
//If more than 1 season was parsed set IsMultiSeason to true so it can be rejected later
if (seasons.Distinct().Count() > 1)
{
result.IsMultiSeason = true;
}
if (seasons.Any())
{
// If at least one season was parsed use the first season as the season
result.SeasonNumber = seasons.First();
}
else if (!result.AbsoluteEpisodeNumbers.Any() && result.EpisodeNumbers.Any())
{
// If no season was found and it's not an absolute only release it should be treated as a mini series and season 1
result.SeasonNumber = 1;
}
}

View File

@@ -465,6 +465,16 @@ namespace NzbDrone.Core.Parser
episodes.AddIfNotNull(episode);
}
}
else if (parsedEpisodeInfo.SeasonNumber > 1)
{
episodes = _episodeService.FindEpisodesBySceneNumbering(series.Id, parsedEpisodeInfo.SeasonNumber, absoluteEpisodeNumber);
if (episodes.Empty())
{
var episode = _episodeService.FindEpisode(series.Id, sceneSeasonNumber.Value, absoluteEpisodeNumber);
episodes.AddIfNotNull(episode);
}
}
else
{
episodes = _episodeService.FindEpisodesBySceneNumbering(series.Id, absoluteEpisodeNumber);

View File

@@ -293,14 +293,6 @@ namespace NzbDrone.Core.Update
try
{
// Don't do a prestartup update check unless BuiltIn update is enabled
if (_configFileProvider.UpdateAutomatically ||
_configFileProvider.UpdateMechanism != UpdateMechanism.BuiltIn ||
_deploymentInfoProvider.IsExternalUpdateMechanism)
{
return;
}
var updateMarker = Path.Combine(_appFolderInfo.AppDataFolder, "update_required");
if (!_diskProvider.FileExists(updateMarker))
{
@@ -309,6 +301,15 @@ namespace NzbDrone.Core.Update
_logger.Debug("Post-install update check requested");
// Don't do a prestartup update check unless BuiltIn update is enabled
if (!_configFileProvider.UpdateAutomatically ||
_configFileProvider.UpdateMechanism != UpdateMechanism.BuiltIn ||
_deploymentInfoProvider.IsExternalUpdateMechanism)
{
_logger.Debug("Built-in updater disabled, skipping post-install update check");
return;
}
var latestAvailable = _checkUpdateService.AvailableUpdate();
if (latestAvailable == null)
{

View File

@@ -61,7 +61,7 @@ namespace NzbDrone.Host.Owin
if (ex.InnerException is HttpListenerException)
{
throw new PortInUseException("Port {0} is already in use, please ensure NzbDrone is not already running.", ex, _configFileProvider.Port);
throw new PortInUseException("Unable to bind to the designated IP Address/Port ({0}:{1}). Please ensure Sonarr is not already running, the bind address is correct (or is set to'*') and the port is not used", ex, _configFileProvider.BindAddress, _configFileProvider.Port);
}
throw ex.InnerException;

View File

@@ -6,6 +6,7 @@ using System.Security.Principal;
using Nancy;
using Nancy.Authentication.Basic;
using Nancy.Authentication.Forms;
using Nancy.Routing.Trie.Nodes;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Authentication;
@@ -161,6 +162,11 @@ namespace Sonarr.Http.Authentication
return true;
}
if (context.Request.IsBundledJsRequest())
{
return true;
}
if (ValidUser(context))
{
return true;

View File

@@ -4,7 +4,6 @@ using Nancy;
using Nancy.Authentication.Basic;
using Nancy.Authentication.Forms;
using Nancy.Bootstrapper;
using Nancy.Cookies;
using Nancy.Cryptography;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Authentication;
@@ -120,7 +119,7 @@ namespace Sonarr.Http.Authentication
if (FormsAuthentication.DecryptAndValidateAuthenticationCookie(formsAuthCookieValue, FormsAuthConfig).IsNotNullOrWhiteSpace())
{
var formsAuthCookie = new NancyCookie(formsAuthCookieName, formsAuthCookieValue, true, false, DateTime.UtcNow.AddDays(7))
var formsAuthCookie = new SonarrNancyCookie(formsAuthCookieName, formsAuthCookieValue, true, false, DateTime.UtcNow.AddDays(7))
{
Path = GetCookiePath()
};

View File

@@ -0,0 +1,33 @@
using System;
using Nancy.Cookies;
namespace Sonarr.Http.Authentication
{
public class SonarrNancyCookie : NancyCookie
{
public SonarrNancyCookie(string name, string value) : base(name, value)
{
}
public SonarrNancyCookie(string name, string value, DateTime expires) : base(name, value, expires)
{
}
public SonarrNancyCookie(string name, string value, bool httpOnly) : base(name, value, httpOnly)
{
}
public SonarrNancyCookie(string name, string value, bool httpOnly, bool secure) : base(name, value, httpOnly, secure)
{
}
public SonarrNancyCookie(string name, string value, bool httpOnly, bool secure, DateTime? expires) : base(name, value, httpOnly, secure, expires)
{
}
public override string ToString()
{
return base.ToString() + "; SameSite=Strict";
}
}
}

View File

@@ -246,7 +246,7 @@ namespace Sonarr.Http.ClientSchema
}
else
{
return fieldValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
return fieldValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => v.Trim());
}
};
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Linq;
using Nancy;
using Nancy.Bootstrapper;
namespace Sonarr.Http.Extensions.Pipelines
{
public class SetCookieHeaderPipeline : IRegisterNancyPipeline
{
public int Order => 99;
public void Register(IPipelines pipelines)
{
pipelines.AfterRequest.AddItemToEndOfPipeline((Action<NancyContext>) Handle);
}
private void Handle(NancyContext context)
{
if (context.Request.IsContentRequest() || context.Request.IsBundledJsRequest())
{
var authCookie = context.Response.Cookies.FirstOrDefault(c => c.Name == "SonarrAuth");
if (authCookie != null)
{
context.Response.Cookies.Remove(authCookie);
}
}
}
}
}

View File

@@ -16,9 +16,9 @@ namespace Sonarr.Http.Extensions.Pipelines
private void Handle(NancyContext context)
{
if (!context.Response.Headers.ContainsKey("X-ApplicationVersion"))
if (!context.Response.Headers.ContainsKey("X-Application-Version"))
{
context.Response.Headers.Add("X-ApplicationVersion", BuildInfo.Version.ToString());
context.Response.Headers.Add("X-Application-Version", BuildInfo.Version.ToString());
}
}
}

View File

@@ -11,6 +11,7 @@ namespace Sonarr.Http.Extensions
public static class ReqResExtensions
{
private static readonly NancyJsonSerializer NancySerializer = new NancyJsonSerializer();
private static readonly string Expires = DateTime.UtcNow.AddYears(1).ToString("r");
public static readonly string LastModified = BuildInfo.BuildDateTime.ToString("r");
@@ -51,8 +52,8 @@ namespace Sonarr.Http.Extensions
public static IDictionary<string, string> EnableCache(this IDictionary<string, string> headers)
{
headers["Cache-Control"] = "max-age=31536000 , public";
headers["Expires"] = "Sat, 29 Jun 2020 00:00:00 GMT";
headers["Cache-Control"] = "max-age=31536000, public";
headers["Expires"] = Expires;
headers["Last-Modified"] = LastModified;
headers["Age"] = "193266";

View File

@@ -40,6 +40,11 @@ namespace Sonarr.Http.Extensions
return request.Path.StartsWith("/Content/", StringComparison.InvariantCultureIgnoreCase);
}
public static bool IsBundledJsRequest(this Request request)
{
return !request.Path.EqualsIgnoreCase("/initialize.js") && request.Path.EndsWith(".js", StringComparison.InvariantCultureIgnoreCase);
}
public static bool IsSharedContentRequest(this Request request)
{
return request.Path.StartsWith("/MediaCover/", StringComparison.InvariantCultureIgnoreCase) ||

Binary file not shown.

Binary file not shown.

View File

@@ -1,7 +1,7 @@
#! /bin/bash
# Increment packageVersion when package scripts change
packageVersion='3.0.5'
packageVersion='3.0.6'
# For now we keep the build version and package version the same
buildVersion=$packageVersion