mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-17 21:26:13 -04:00
Compare commits
39 Commits
v3.0.5.114
...
v3.0.6.119
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a200dd5f6d | ||
|
|
f57efd30b8 | ||
|
|
f2f1039c5e | ||
|
|
12ba4f73ed | ||
|
|
370280b4bf | ||
|
|
6c505937da | ||
|
|
7272c5b7fc | ||
|
|
1d06c3fc15 | ||
|
|
ec9f62285a | ||
|
|
aae0d1c4ba | ||
|
|
652d44722b | ||
|
|
5a69801877 | ||
|
|
fa8b2f48e7 | ||
|
|
34faa417c1 | ||
|
|
d6c0635a26 | ||
|
|
d4167d7169 | ||
|
|
eea6be459d | ||
|
|
67e97f7aee | ||
|
|
37e1c4f2eb | ||
|
|
a9b8ec3505 | ||
|
|
f57cf1561b | ||
|
|
6672650b6b | ||
|
|
e4a064a1c0 | ||
|
|
a848e575cd | ||
|
|
01995e686d | ||
|
|
32058f1705 | ||
|
|
af3696af08 | ||
|
|
1477356cfc | ||
|
|
aa19ddfbfd | ||
|
|
a697a69e88 | ||
|
|
3abb7e156a | ||
|
|
240791a7cd | ||
|
|
0fe2453962 | ||
|
|
85f4cbe94c | ||
|
|
e1f7bce14b | ||
|
|
675c72f02e | ||
|
|
6619350f87 | ||
|
|
efd9fe9ad0 | ||
|
|
ab502ffda4 |
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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-->
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -9,3 +9,12 @@
|
||||
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.triggers {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.triggerEvents {
|
||||
margin-top: 10px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
33
src/Sonarr.Http/Authentication/SonarrNancyCookie.cs
Normal file
33
src/Sonarr.Http/Authentication/SonarrNancyCookie.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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.
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user