mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-05 13:21:25 -05:00
Compare commits
28 Commits
v2.0.0.437
...
v2.0.0.442
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c99e92e6af | ||
|
|
3f64c01d5b | ||
|
|
f022dae1fa | ||
|
|
52ad8cf37f | ||
|
|
3d20fd8f96 | ||
|
|
cf662291d5 | ||
|
|
43d85bf59d | ||
|
|
4a149c356b | ||
|
|
740fc9154f | ||
|
|
0a657302f7 | ||
|
|
7b09b259a8 | ||
|
|
b093be3f4e | ||
|
|
3c8b263694 | ||
|
|
43c5d03f9a | ||
|
|
9fbe06ad68 | ||
|
|
db899a9bb8 | ||
|
|
d3890bd712 | ||
|
|
1a61796092 | ||
|
|
1251e294cd | ||
|
|
0411b82e65 | ||
|
|
9519f3137c | ||
|
|
f8d97cac7d | ||
|
|
f2ecbe776b | ||
|
|
1ac442d0e6 | ||
|
|
5f2aeb0cea | ||
|
|
2ece05cd1e | ||
|
|
25a3f83ebc | ||
|
|
cdce65a922 |
@@ -8,8 +8,8 @@ Setup guides, FAQ, the more information we have on the wiki the better.
|
||||
## Development ##
|
||||
|
||||
### Tools required ###
|
||||
- Visual Studio 2013
|
||||
- HTML/Javascript editor of choice (Sublime Text/Webstorm/etc)
|
||||
- Visual Studio 2015
|
||||
- HTML/Javascript editor of choice (Sublime Text/Webstorm/Atom/etc)
|
||||
- npm (node package manager)
|
||||
- git
|
||||
|
||||
|
||||
@@ -44,7 +44,8 @@ namespace NzbDrone.Api.Config
|
||||
|
||||
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,
|
||||
CopyUsingHardlinks = model.CopyUsingHardlinks,
|
||||
EnableMediaInfo = model.EnableMediaInfo,
|
||||
ExtraFileExtensions = model.ExtraFileExtensions,
|
||||
EnableMediaInfo = model.EnableMediaInfo
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ namespace NzbDrone.Common.Test
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Sanbox()
|
||||
public void Sandbox()
|
||||
{
|
||||
GetIAppDirectoryInfo().GetUpdateSandboxFolder().Should().BeEquivalentTo(@"C:\Temp\nzbdrone_update\".AsOsAgnostic());
|
||||
}
|
||||
@@ -271,5 +271,33 @@ namespace NzbDrone.Common.Test
|
||||
{
|
||||
GetIAppDirectoryInfo().GetUpdateLogFolder().Should().BeEquivalentTo(@"C:\NzbDrone\UpdateLogs\".AsOsAgnostic());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAncestorFolders_should_return_all_ancestors_in_path_Windows()
|
||||
{
|
||||
WindowsOnly();
|
||||
var path = @"C:\Test\TV\Series Title";
|
||||
var result = path.GetAncestorFolders();
|
||||
|
||||
result.Count.Should().Be(4);
|
||||
result[0].Should().Be(@"C:\");
|
||||
result[1].Should().Be(@"Test");
|
||||
result[2].Should().Be(@"TV");
|
||||
result[3].Should().Be(@"Series Title");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAncestorFolders_should_return_all_ancestors_in_path_Linux()
|
||||
{
|
||||
MonoOnly();
|
||||
var path = @"/Test/TV/Series Title";
|
||||
var result = path.GetAncestorFolders();
|
||||
|
||||
result.Count.Should().Be(4);
|
||||
result[0].Should().Be(@"/");
|
||||
result[1].Should().Be(@"Test");
|
||||
result[2].Should().Be(@"TV");
|
||||
result[3].Should().Be(@"Series Title");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ namespace NzbDrone.Common.Disk
|
||||
.Select(d => new FileSystemModel
|
||||
{
|
||||
Type = FileSystemEntityType.Drive,
|
||||
Name = d.VolumeLabel,
|
||||
Name = d.VolumeName,
|
||||
Path = d.RootDirectory,
|
||||
LastModified = null
|
||||
})
|
||||
@@ -163,7 +163,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
|
||||
private string GetParent(string path)
|
||||
{
|
||||
var di = new DirectoryInfo(path);
|
||||
|
||||
@@ -13,5 +13,6 @@ namespace NzbDrone.Common.Disk
|
||||
long TotalFreeSpace { get; }
|
||||
long TotalSize { get; }
|
||||
string VolumeLabel { get; }
|
||||
string VolumeName { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
@@ -174,6 +175,21 @@ namespace NzbDrone.Common.Extensions
|
||||
return Path.Combine(GetProperCapitalization(dirInfo), fileName);
|
||||
}
|
||||
|
||||
public static List<string> GetAncestorFolders(this string path)
|
||||
{
|
||||
var directory = new DirectoryInfo(path);
|
||||
var directories = new List<string>();
|
||||
|
||||
while (directory != null)
|
||||
{
|
||||
directories.Insert(0, directory.Name);
|
||||
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
return directories;
|
||||
}
|
||||
|
||||
public static string GetAppDataPath(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return appFolderInfo.AppDataFolder;
|
||||
|
||||
@@ -48,11 +48,11 @@
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Org.Mentalis, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DotNet4.SocksProxy.1.1.0.0\lib\net40\Org.Mentalis.dll</HintPath>
|
||||
<HintPath>..\packages\DotNet4.SocksProxy.1.3.2.0\lib\net40\Org.Mentalis.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="SocksWebProxy, Version=1.1.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DotNet4.SocksProxy.1.1.0.0\lib\net40\SocksWebProxy.dll</HintPath>
|
||||
<Reference Include="SocksWebProxy, Version=1.3.2.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DotNet4.SocksProxy.1.3.2.0\lib\net40\SocksWebProxy.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="DotNet4.SocksProxy" version="1.1.0.0" targetFramework="net40" />
|
||||
<package id="DotNet4.SocksProxy" version="1.3.2.0" targetFramework="net40" />
|
||||
<package id="ICSharpCode.SharpZipLib.Patched" version="0.86.5" targetFramework="net40" />
|
||||
<package id="Newtonsoft.Json" version="6.0.6" targetFramework="net40" />
|
||||
<package id="NLog" version="4.3.4" targetFramework="net40" />
|
||||
|
||||
@@ -108,5 +108,19 @@ namespace NzbDrone.Core.Test.Configuration
|
||||
|
||||
keys.Should().OnlyHaveUniqueItems();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_ignore_null_properties()
|
||||
{
|
||||
Mocker.GetMock<IConfigRepository>()
|
||||
.Setup(v => v.Get("downloadedepisodesfolder"))
|
||||
.Returns(new Config { Id = 1, Key = "DownloadedEpisodesFolder", Value = @"C:\test".AsOsAgnostic() });
|
||||
|
||||
var dict = new Dictionary<string, object>();
|
||||
dict.Add("DownloadedEpisodesFolder", null);
|
||||
Subject.SaveConfigDictionary(dict);
|
||||
|
||||
Mocker.GetMock<IConfigRepository>().Verify(c => c.Upsert("downloadedepisodesfolder", It.IsAny<string>()), Times.Never());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,15 @@ namespace NzbDrone.Core.Test.Datastore.Migration
|
||||
Type = 5,
|
||||
Consumer = "XbmcMetadata"
|
||||
});
|
||||
|
||||
c.Insert.IntoTable("MetadataFiles").Row(new
|
||||
{
|
||||
SeriesId = 1,
|
||||
RelativePath = "Series Title",
|
||||
LastUpdated = "2016-05-30 20:23:02.3725923",
|
||||
Type = 3,
|
||||
Consumer = "RoksboxMetadata"
|
||||
});
|
||||
});
|
||||
|
||||
var items = db.Query<MetadataFile99>("SELECT * FROM MetadataFiles");
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
{
|
||||
Series = new Series(),
|
||||
Episodes = new List<Episode> { new Episode { Id = 1 } }
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -77,11 +77,11 @@ namespace NzbDrone.Core.Test.Download
|
||||
.Setup(s => s.MostRecentForDownloadId(_trackedDownload.DownloadItem.DownloadId))
|
||||
.Returns((History.History)null);
|
||||
}
|
||||
|
||||
|
||||
private void GivenSuccessfulImport()
|
||||
{
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(new ImportDecision(new LocalEpisode() { Path = @"C:\TestPath\Droned.S01E01.mkv" }))
|
||||
@@ -145,7 +145,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
GivenNoGrabbedHistory();
|
||||
GivenSeriesMatch();
|
||||
GivenSuccessfulImport();
|
||||
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
|
||||
AssertCompletedDownload();
|
||||
@@ -179,13 +179,13 @@ namespace NzbDrone.Core.Test.Download
|
||||
public void should_mark_as_imported_if_all_episodes_were_imported()
|
||||
{
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(
|
||||
new ImportDecision(
|
||||
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"})),
|
||||
|
||||
|
||||
new ImportResult(
|
||||
new ImportDecision(
|
||||
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv"}))
|
||||
@@ -200,13 +200,13 @@ namespace NzbDrone.Core.Test.Download
|
||||
public void should_not_mark_as_imported_if_all_files_were_rejected()
|
||||
{
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(
|
||||
new ImportDecision(
|
||||
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}, new Rejection("Rejected!")), "Test Failure"),
|
||||
|
||||
|
||||
new ImportResult(
|
||||
new ImportDecision(
|
||||
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv"},new Rejection("Rejected!")), "Test Failure")
|
||||
@@ -224,13 +224,13 @@ namespace NzbDrone.Core.Test.Download
|
||||
public void should_not_mark_as_imported_if_no_episodes_were_parsed()
|
||||
{
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(
|
||||
new ImportDecision(
|
||||
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}, new Rejection("Rejected!")), "Test Failure"),
|
||||
|
||||
|
||||
new ImportResult(
|
||||
new ImportDecision(
|
||||
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv"},new Rejection("Rejected!")), "Test Failure")
|
||||
@@ -247,7 +247,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
public void should_not_mark_as_imported_if_all_files_were_skipped()
|
||||
{
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}),"Test Failure"),
|
||||
@@ -271,7 +271,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
};
|
||||
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"})),
|
||||
@@ -294,7 +294,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
};
|
||||
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"})),
|
||||
@@ -314,7 +314,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
GivenABadlyNamedDownload();
|
||||
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}))
|
||||
@@ -323,7 +323,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Setup(v => v.GetSeries(It.IsAny<int>()))
|
||||
.Returns(BuildRemoteEpisode().Series);
|
||||
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
|
||||
AssertCompletedDownload();
|
||||
@@ -335,7 +335,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
GivenABadlyNamedDownload();
|
||||
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}))
|
||||
@@ -370,7 +370,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
};
|
||||
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}))
|
||||
@@ -408,7 +408,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
private void AssertNoAttemptedImport()
|
||||
{
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>()
|
||||
.Verify(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()), Times.Never());
|
||||
.Verify(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()), Times.Never());
|
||||
|
||||
AssertNoCompletedDownload();
|
||||
}
|
||||
@@ -424,7 +424,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
private void AssertCompletedDownload()
|
||||
{
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>()
|
||||
.Verify(v => v.ProcessPath(_trackedDownload.DownloadItem.OutputPath.FullPath, _trackedDownload.RemoteEpisode.Series, _trackedDownload.DownloadItem), Times.Once());
|
||||
.Verify(v => v.ProcessPath(_trackedDownload.DownloadItem.OutputPath.FullPath, ImportMode.Auto, _trackedDownload.RemoteEpisode.Series, _trackedDownload.DownloadItem), Times.Once());
|
||||
|
||||
Mocker.GetMock<IEventAggregator>()
|
||||
.Verify(v => v.PublishEvent(It.IsAny<DownloadCompletedEvent>()), Times.Once());
|
||||
|
||||
@@ -35,6 +35,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
|
||||
|
||||
Mocker.GetMock<IQBittorrentProxy>()
|
||||
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
|
||||
.Returns(new QBittorrentPreferences());
|
||||
}
|
||||
|
||||
protected void GivenRedirectToMagnet()
|
||||
@@ -85,6 +89,17 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
});
|
||||
}
|
||||
|
||||
protected void GivenMaxRatio(float maxRatio, bool removeOnMaxRatio = true)
|
||||
{
|
||||
Mocker.GetMock<IQBittorrentProxy>()
|
||||
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
|
||||
.Returns(new QBittorrentPreferences
|
||||
{
|
||||
RemoveOnMaxRatio = removeOnMaxRatio,
|
||||
MaxRatio = maxRatio
|
||||
});
|
||||
}
|
||||
|
||||
protected virtual void GivenTorrents(List<QBittorrentTorrent> torrents)
|
||||
{
|
||||
if (torrents == null)
|
||||
@@ -253,13 +268,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
[Test]
|
||||
public void should_return_status_with_outputdirs()
|
||||
{
|
||||
var configItems = new Dictionary<string, Object>();
|
||||
|
||||
configItems.Add("save_path", @"C:\Downloads\Finished\QBittorrent".AsOsAgnostic());
|
||||
var config = new QBittorrentPreferences
|
||||
{
|
||||
SavePath = @"C:\Downloads\Finished\QBittorrent".AsOsAgnostic()
|
||||
};
|
||||
|
||||
Mocker.GetMock<IQBittorrentProxy>()
|
||||
.Setup(v => v.GetConfig(It.IsAny<QBittorrentSettings>()))
|
||||
.Returns(configItems);
|
||||
.Returns(config);
|
||||
|
||||
var result = Subject.GetStatus();
|
||||
|
||||
@@ -293,5 +309,97 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
|
||||
id.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_read_only_if_max_ratio_not_reached()
|
||||
{
|
||||
GivenMaxRatio(1.0f);
|
||||
|
||||
var torrent = new QBittorrentTorrent
|
||||
{
|
||||
Hash = "HASH",
|
||||
Name = _title,
|
||||
Size = 1000,
|
||||
Progress = 1.0,
|
||||
Eta = 8640000,
|
||||
State = "uploading",
|
||||
Label = "",
|
||||
SavePath = "",
|
||||
Ratio = 0.5f
|
||||
};
|
||||
GivenTorrents(new List<QBittorrentTorrent> { torrent });
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.IsReadOnly.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_read_only_if_max_ratio_reached_and_not_paused()
|
||||
{
|
||||
GivenMaxRatio(1.0f);
|
||||
|
||||
var torrent = new QBittorrentTorrent
|
||||
{
|
||||
Hash = "HASH",
|
||||
Name = _title,
|
||||
Size = 1000,
|
||||
Progress = 1.0,
|
||||
Eta = 8640000,
|
||||
State = "uploading",
|
||||
Label = "",
|
||||
SavePath = "",
|
||||
Ratio = 1.0f
|
||||
};
|
||||
GivenTorrents(new List<QBittorrentTorrent> { torrent });
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.IsReadOnly.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_read_only_if_max_ratio_is_not_set()
|
||||
{
|
||||
GivenMaxRatio(1.0f, false);
|
||||
|
||||
var torrent = new QBittorrentTorrent
|
||||
{
|
||||
Hash = "HASH",
|
||||
Name = _title,
|
||||
Size = 1000,
|
||||
Progress = 1.0,
|
||||
Eta = 8640000,
|
||||
State = "uploading",
|
||||
Label = "",
|
||||
SavePath = "",
|
||||
Ratio = 1.0f
|
||||
};
|
||||
GivenTorrents(new List<QBittorrentTorrent> { torrent });
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.IsReadOnly.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_be_read_only_if_max_ratio_reached_and_paused()
|
||||
{
|
||||
GivenMaxRatio(1.0f);
|
||||
|
||||
var torrent = new QBittorrentTorrent
|
||||
{
|
||||
Hash = "HASH",
|
||||
Name = _title,
|
||||
Size = 1000,
|
||||
Progress = 1.0,
|
||||
Eta = 8640000,
|
||||
State = "pausedUP",
|
||||
Label = "",
|
||||
SavePath = "",
|
||||
Ratio = 1.0f
|
||||
};
|
||||
GivenTorrents(new List<QBittorrentTorrent> { torrent });
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.IsReadOnly.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
[TestCase("4.0.0.0")]
|
||||
[TestCase("4.2")]
|
||||
[TestCase("4.6")]
|
||||
[TestCase("4.4.2")]
|
||||
public void should_return_ok(string version)
|
||||
{
|
||||
GivenOutput(version);
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
.Returns(new List<ImportResult>());
|
||||
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>());
|
||||
|
||||
var downloadItem = Builder<DownloadClientItem>.CreateNew()
|
||||
@@ -113,7 +113,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
|
||||
Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFolder });
|
||||
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), null, null), Times.Once());
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), ImportMode.Auto, null, null), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -123,7 +123,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
|
||||
Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFile });
|
||||
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), null, null), Times.Once());
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), ImportMode.Auto, null, null), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -134,7 +134,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
|
||||
Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFolder, DownloadClientId = "sab1" });
|
||||
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(_downloadFolder, _trackedDownload.RemoteEpisode.Series, _trackedDownload.DownloadItem), Times.Once());
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(_downloadFolder, ImportMode.Auto, _trackedDownload.RemoteEpisode.Series, _trackedDownload.DownloadItem), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -144,7 +144,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
|
||||
Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFolder, DownloadClientId = "sab1" });
|
||||
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(_downloadFolder, null, null), Times.Once());
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(_downloadFolder, ImportMode.Auto, null, null), Times.Once());
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
}
|
||||
@@ -154,9 +154,19 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
{
|
||||
Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFolder });
|
||||
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), null, null), Times.Never());
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), ImportMode.Auto, null, null), Times.Never());
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_override_import_mode()
|
||||
{
|
||||
GivenExistingFile(_downloadFile);
|
||||
|
||||
Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFile, ImportMode = ImportMode.Copy });
|
||||
|
||||
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), ImportMode.Copy, null, null), Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
.Returns(true);
|
||||
|
||||
Mocker.GetMock<IImportApprovedEpisodes>()
|
||||
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null))
|
||||
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
|
||||
.Returns(new List<ImportResult>());
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
.Returns(true);
|
||||
|
||||
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
|
||||
|
||||
|
||||
VerifyNoImport();
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
public void should_not_delete_folder_if_no_files_were_imported()
|
||||
{
|
||||
Mocker.GetMock<IImportApprovedEpisodes>()
|
||||
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), false, null))
|
||||
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), false, null, ImportMode.Auto))
|
||||
.Returns(new List<ImportResult>());
|
||||
|
||||
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
|
||||
@@ -133,7 +133,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
.Returns(imported);
|
||||
|
||||
Mocker.GetMock<IImportApprovedEpisodes>()
|
||||
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null))
|
||||
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
|
||||
.Returns(imported.Select(i => new ImportResult(i)).ToList());
|
||||
|
||||
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
|
||||
@@ -159,7 +159,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
.Returns(imported);
|
||||
|
||||
Mocker.GetMock<IImportApprovedEpisodes>()
|
||||
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null))
|
||||
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
|
||||
.Returns(imported.Select(i => new ImportResult(i)).ToList());
|
||||
|
||||
Mocker.GetMock<IDetectSample>()
|
||||
@@ -231,7 +231,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
.Returns(imported);
|
||||
|
||||
Mocker.GetMock<IImportApprovedEpisodes>()
|
||||
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null))
|
||||
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
|
||||
.Returns(imported.Select(i => new ImportResult(i)).ToList());
|
||||
|
||||
Mocker.GetMock<IDetectSample>()
|
||||
@@ -342,7 +342,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
.Returns(imported);
|
||||
|
||||
Mocker.GetMock<IImportApprovedEpisodes>()
|
||||
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null))
|
||||
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
|
||||
.Returns(new List<ImportResult>());
|
||||
|
||||
Mocker.GetMock<IDetectSample>()
|
||||
@@ -365,14 +365,14 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
|
||||
private void VerifyNoImport()
|
||||
{
|
||||
Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null),
|
||||
Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto),
|
||||
Times.Never());
|
||||
}
|
||||
|
||||
private void VerifyImport()
|
||||
{
|
||||
Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null),
|
||||
Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto),
|
||||
Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
}
|
||||
|
||||
Mocker.GetMock<IUpgradeMediaFiles>()
|
||||
.Setup(s => s.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), It.IsAny<LocalEpisode>(), false))
|
||||
.Setup(s => s.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
|
||||
.Returns(new EpisodeFileMoveResult());
|
||||
|
||||
_downloadClientItem = Builder<DownloadClientItem>.CreateNew().Build();
|
||||
@@ -95,7 +95,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
all.AddRange(_approvedDecisions);
|
||||
|
||||
var result = Subject.Import(all, false);
|
||||
|
||||
|
||||
result.Should().HaveCount(all.Count);
|
||||
result.Where(i => i.Result == ImportResultType.Imported).Should().HaveCount(_approvedDecisions.Count);
|
||||
}
|
||||
@@ -223,5 +223,23 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||
results.Should().ContainSingle(d => d.Result == ImportResultType.Imported);
|
||||
results.Should().ContainSingle(d => d.Result == ImportResultType.Imported && d.ImportDecision.LocalEpisode.Size == fileDecision.LocalEpisode.Size);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_copy_readonly_downloads()
|
||||
{
|
||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "30.Rock.S01E01", IsReadOnly = true });
|
||||
|
||||
Mocker.GetMock<IUpgradeMediaFiles>()
|
||||
.Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_override_importmode()
|
||||
{
|
||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "30.Rock.S01E01", IsReadOnly = true }, ImportMode.Move);
|
||||
|
||||
Mocker.GetMock<IUpgradeMediaFiles>()
|
||||
.Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, false), Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Configuration
|
||||
{
|
||||
object currentValue;
|
||||
allWithDefaults.TryGetValue(configValue.Key, out currentValue);
|
||||
if (currentValue == null) continue;
|
||||
if (currentValue == null || configValue.Value == null) continue;
|
||||
|
||||
var equal = configValue.Value.ToString().Equals(currentValue.ToString());
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||
.AddColumn("Added").AsDateTime().Nullable()
|
||||
.AddColumn("Extension").AsString().Nullable();
|
||||
|
||||
// Remove Metadata files that don't have an extension
|
||||
Execute.Sql("DELETE FROM MetadataFiles WHERE RelativePath NOT LIKE '%.%'");
|
||||
|
||||
// Set Extension using the extension from RelativePath
|
||||
Execute.WithConnection(SetMetadataFileExtension);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
Paused,
|
||||
Checking,
|
||||
Downloading,
|
||||
ToPP,
|
||||
ToPP, // TODO: Remove in v3
|
||||
QuickCheck,
|
||||
Verifying,
|
||||
Repairing,
|
||||
|
||||
@@ -12,7 +12,6 @@ using FluentValidation.Results;
|
||||
using System.Net;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.RemotePathMappings;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
@@ -80,20 +79,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
}
|
||||
}
|
||||
|
||||
public override ProviderMessage Message
|
||||
{
|
||||
get
|
||||
{
|
||||
return new ProviderMessage("Sonarr is unable to remove torrents that have finished seeding when using qBittorrent", ProviderMessageType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<DownloadClientItem> GetItems()
|
||||
{
|
||||
QBittorrentPreferences config;
|
||||
List<QBittorrentTorrent> torrents;
|
||||
|
||||
try
|
||||
{
|
||||
config = _proxy.GetConfig(Settings);
|
||||
torrents = _proxy.GetTorrents(Settings);
|
||||
}
|
||||
catch (DownloadClientException ex)
|
||||
@@ -116,11 +109,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta);
|
||||
|
||||
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath));
|
||||
|
||||
// At the moment there isn't an easy way to detect if the torrent has
|
||||
// reached the seeding limit, We would need to check the preferences
|
||||
// and still not be completely sure if that torrent has a limit set for it
|
||||
item.IsReadOnly = true;
|
||||
|
||||
// Avoid removing torrents that haven't reached the global max ratio.
|
||||
// Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api).
|
||||
item.IsReadOnly = (config.MaxRatioEnabled && config.MaxRatio > torrent.Ratio) || torrent.State != "pausedUP";
|
||||
|
||||
if (!item.OutputPath.IsEmpty && item.OutputPath.FileName != torrent.Name)
|
||||
{
|
||||
@@ -178,7 +170,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
var config = _proxy.GetConfig(Settings);
|
||||
|
||||
var destDir = new OsPath((string)config.GetValueOrDefault("save_path"));
|
||||
var destDir = new OsPath(config.SavePath);
|
||||
|
||||
return new DownloadClientStatus
|
||||
{
|
||||
@@ -227,6 +219,16 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
DetailedDescription = "Sonarr will not attempt to import completed downloads without a category."
|
||||
};
|
||||
}
|
||||
|
||||
// Complain if qBittorrent is configured to remove torrents on max ratio
|
||||
var config = _proxy.GetConfig(Settings);
|
||||
if (config.MaxRatioEnabled && config.RemoveOnMaxRatio)
|
||||
{
|
||||
return new NzbDroneValidationFailure(String.Empty, "QBittorrent is configured to remove torrents when they reach their Share Ratio Limit")
|
||||
{
|
||||
DetailedDescription = "Sonarr will be unable to perform Completed Download Handling as configured. You can fix this in qBittorrent ('Tools -> Options...' in the menu) by changing 'Options -> BitTorrent -> Share Ratio Limiting' from 'Remove them' to 'Pause them'."
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (DownloadClientAuthenticationException ex)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
// qbittorrent settings from the list returned by /query/preferences
|
||||
public class QBittorrentPreferences
|
||||
{
|
||||
[JsonProperty(PropertyName = "save_path")]
|
||||
public string SavePath { get; set; } // Default save path for torrents, separated by slashes
|
||||
|
||||
[JsonProperty(PropertyName = "max_ratio_enabled")]
|
||||
public bool MaxRatioEnabled { get; set; } // True if share ratio limit is enabled
|
||||
|
||||
[JsonProperty(PropertyName = "max_ratio")]
|
||||
public float MaxRatio { get; set; } // Get the global share ratio limit
|
||||
|
||||
[JsonProperty(PropertyName = "max_ratio_act")]
|
||||
public bool RemoveOnMaxRatio { get; set; } // Action performed when a torrent reaches the maximum share ratio. [false = pause, true = remove]
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
public interface IQBittorrentProxy
|
||||
{
|
||||
int GetVersion(QBittorrentSettings settings);
|
||||
Dictionary<string, Object> GetConfig(QBittorrentSettings settings);
|
||||
QBittorrentPreferences GetConfig(QBittorrentSettings settings);
|
||||
List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings);
|
||||
|
||||
void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings);
|
||||
@@ -47,10 +47,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
return response;
|
||||
}
|
||||
|
||||
public Dictionary<string, object> GetConfig(QBittorrentSettings settings)
|
||||
public QBittorrentPreferences GetConfig(QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/query/preferences");
|
||||
var response = ProcessRequest<Dictionary<string, object>>(request, settings);
|
||||
var response = ProcessRequest<QBittorrentPreferences>(request, settings);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -21,5 +21,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
|
||||
[JsonProperty(PropertyName = "save_path")]
|
||||
public string SavePath { get; set; } // Torrent save path
|
||||
|
||||
public float Ratio { get; set; } // Torrent share ratio
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace NzbDrone.Core.Download
|
||||
private void Import(TrackedDownload trackedDownload)
|
||||
{
|
||||
var outputPath = trackedDownload.DownloadItem.OutputPath.FullPath;
|
||||
var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
|
||||
var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
|
||||
|
||||
if (importResults.Empty())
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Tv;
|
||||
@@ -31,19 +32,30 @@ namespace NzbDrone.Core.Extras.Files
|
||||
private readonly IExtraFileRepository<TExtraFile> _repository;
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IRecycleBinProvider _recycleBinProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ExtraFileService(IExtraFileRepository<TExtraFile> repository,
|
||||
ISeriesService seriesService,
|
||||
IDiskProvider diskProvider,
|
||||
IRecycleBinProvider recycleBinProvider,
|
||||
Logger logger)
|
||||
{
|
||||
_repository = repository;
|
||||
_seriesService = seriesService;
|
||||
_diskProvider = diskProvider;
|
||||
_recycleBinProvider = recycleBinProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public virtual bool PermanentlyDelete
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public List<TExtraFile> GetFilesBySeries(int seriesId)
|
||||
{
|
||||
return _repository.GetFilesBySeries(seriesId);
|
||||
@@ -99,15 +111,33 @@ namespace NzbDrone.Core.Extras.Files
|
||||
public void HandleAsync(EpisodeFileDeletedEvent message)
|
||||
{
|
||||
var episodeFile = message.EpisodeFile;
|
||||
var series = _seriesService.GetSeries(message.EpisodeFile.SeriesId);
|
||||
|
||||
foreach (var extra in _repository.GetFilesByEpisodeFile(episodeFile.Id))
|
||||
if (message.Reason == DeleteMediaFileReason.NoLinkedEpisodes)
|
||||
{
|
||||
var path = Path.Combine(series.Path, extra.RelativePath);
|
||||
_logger.Debug("Removing episode file from DB as part of cleanup routine, not deleting extra files from disk.");
|
||||
}
|
||||
|
||||
if (_diskProvider.FileExists(path))
|
||||
else
|
||||
{
|
||||
var series = _seriesService.GetSeries(message.EpisodeFile.SeriesId);
|
||||
|
||||
foreach (var extra in _repository.GetFilesByEpisodeFile(episodeFile.Id))
|
||||
{
|
||||
_diskProvider.DeleteFile(path);
|
||||
var path = Path.Combine(series.Path, extra.RelativePath);
|
||||
|
||||
if (_diskProvider.FileExists(path))
|
||||
{
|
||||
if (PermanentlyDelete)
|
||||
{
|
||||
_diskProvider.DeleteFile(path);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
// Send extra files to the recycling bin so they can be recovered if necessary
|
||||
_recycleBinProvider.DeleteFile(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
|
||||
}
|
||||
|
||||
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
|
||||
var destination = Path.Combine(series.Path, Path.GetFileName(series.Path) + Path.GetExtension(source));
|
||||
var destination = Path.GetFileName(series.Path) + Path.GetExtension(source);
|
||||
|
||||
return new List<ImageFileResult>{ new ImageFileResult(destination, source) };
|
||||
}
|
||||
@@ -220,7 +220,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
|
||||
}
|
||||
|
||||
var filename = Path.GetFileName(seasonFolder) + ".jpg";
|
||||
var path = Path.Combine(series.Path, seasonFolder, filename);
|
||||
var path = series.Path.GetRelativePath(Path.Combine(series.Path, seasonFolder, filename));
|
||||
|
||||
return new List<ImageFileResult> { new ImageFileResult(path, image.Url) };
|
||||
}
|
||||
|
||||
@@ -20,15 +20,12 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
|
||||
public class XbmcMetadata : MetadataBase<XbmcMetadataSettings>
|
||||
{
|
||||
private readonly IMapCoversToLocal _mediaCoverService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public XbmcMetadata(IMapCoversToLocal mediaCoverService,
|
||||
IDiskProvider diskProvider,
|
||||
Logger logger)
|
||||
{
|
||||
_mediaCoverService = mediaCoverService;
|
||||
_diskProvider = diskProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata.Files
|
||||
@@ -11,9 +12,17 @@ namespace NzbDrone.Core.Extras.Metadata.Files
|
||||
|
||||
public class MetadataFileService : ExtraFileService<MetadataFile>, IMetadataFileService
|
||||
{
|
||||
public MetadataFileService(IExtraFileRepository<MetadataFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, Logger logger)
|
||||
: base(repository, seriesService, diskProvider, logger)
|
||||
public MetadataFileService(IExtraFileRepository<MetadataFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger)
|
||||
: base(repository, seriesService, diskProvider, recycleBinProvider, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool PermanentlyDelete
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Others
|
||||
@@ -11,8 +12,8 @@ namespace NzbDrone.Core.Extras.Others
|
||||
|
||||
public class OtherExtraFileService : ExtraFileService<OtherExtraFile>, IOtherExtraFileService
|
||||
{
|
||||
public OtherExtraFileService(IExtraFileRepository<OtherExtraFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, Logger logger)
|
||||
: base(repository, seriesService, diskProvider, logger)
|
||||
public OtherExtraFileService(IExtraFileRepository<OtherExtraFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger)
|
||||
: base(repository, seriesService, diskProvider, recycleBinProvider, logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Subtitles
|
||||
@@ -11,8 +12,8 @@ namespace NzbDrone.Core.Extras.Subtitles
|
||||
|
||||
public class SubtitleFileService : ExtraFileService<SubtitleFile>, ISubtitleFileService
|
||||
{
|
||||
public SubtitleFileService(IExtraFileRepository<SubtitleFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, Logger logger)
|
||||
: base(repository, seriesService, diskProvider, logger)
|
||||
public SubtitleFileService(IExtraFileRepository<SubtitleFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger)
|
||||
: base(repository, seriesService, diskProvider, recycleBinProvider, logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, "your mono version 3.4.0 has a critical bug, you should upgrade to a higher version");
|
||||
}
|
||||
|
||||
if (version >= new Version(4, 4, 0) && version < new Version(4, 5))
|
||||
if (version == new Version(4, 4, 0) || version == new Version(4, 4, 1))
|
||||
{
|
||||
_logger.Debug("mono version {0}", version);
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"your mono version {version} has a bug that causes issues connecting to indexers/download clients");
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.IO;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Update;
|
||||
|
||||
@@ -33,6 +34,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
if ((OsInfo.IsWindows || _configFileProvider.UpdateAutomatically) &&
|
||||
_configFileProvider.UpdateMechanism == UpdateMechanism.BuiltIn)
|
||||
{
|
||||
if (OsInfo.IsOsx && startupFolder.GetAncestorFolders().Contains("AppTranslocation"))
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
if (!_diskProvider.FolderWritable(startupFolder))
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error,
|
||||
|
||||
@@ -236,7 +236,7 @@ namespace NzbDrone.Core.History
|
||||
{
|
||||
if (message.Reason == DeleteMediaFileReason.NoLinkedEpisodes)
|
||||
{
|
||||
_logger.Debug("Removing episode file from DB as part of cleanup routine.");
|
||||
_logger.Debug("Removing episode file from DB as part of cleanup routine, not creating history event.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
48
src/NzbDrone.Core/MediaCover/GdiPlusInterop.cs
Normal file
48
src/NzbDrone.Core/MediaCover/GdiPlusInterop.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
namespace NzbDrone.Core.MediaCover
|
||||
{
|
||||
public static class GdiPlusInterop
|
||||
{
|
||||
private static Exception _gdiPlusException;
|
||||
|
||||
static GdiPlusInterop()
|
||||
{
|
||||
TestLibrary();
|
||||
}
|
||||
|
||||
private static void TestLibrary()
|
||||
{
|
||||
if (OsInfo.IsWindows)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// We use StringFormat as test coz it gets properly cleaned up by the finalizer even if gdiplus is absent and is relatively non-invasive.
|
||||
var strFormat = new StringFormat();
|
||||
|
||||
strFormat.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_gdiPlusException = ex;
|
||||
}
|
||||
}
|
||||
|
||||
public static void CheckGdiPlus()
|
||||
{
|
||||
if (_gdiPlusException != null)
|
||||
{
|
||||
throw new DllNotFoundException("Couldn't load GDIPlus library", _gdiPlusException);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ namespace NzbDrone.Core.MediaCover
|
||||
{
|
||||
try
|
||||
{
|
||||
GdiPlusInterop.CheckGdiPlus();
|
||||
|
||||
using (var sourceStream = _diskProvider.OpenReadStream(source))
|
||||
{
|
||||
using (var outputStream = _diskProvider.OpenWriteStream(destination))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using NzbDrone.Core.MediaFiles.EpisodeImport;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles.Commands
|
||||
@@ -18,5 +19,6 @@ namespace NzbDrone.Core.MediaFiles.Commands
|
||||
// Properties used by third-party apps, do not modify.
|
||||
public string Path { get; set; }
|
||||
public string DownloadClientId { get; set; }
|
||||
public ImportMode ImportMode { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,17 +70,17 @@ namespace NzbDrone.Core.MediaFiles
|
||||
{
|
||||
_logger.Debug("External directory scan request for known download {0}. [{1}]", message.DownloadClientId, message.Path);
|
||||
|
||||
return _downloadedEpisodesImportService.ProcessPath(message.Path, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
|
||||
return _downloadedEpisodesImportService.ProcessPath(message.Path, message.ImportMode, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("External directory scan request for unknown download {0}, attempting normal import. [{1}]", message.DownloadClientId, message.Path);
|
||||
|
||||
return _downloadedEpisodesImportService.ProcessPath(message.Path);
|
||||
return _downloadedEpisodesImportService.ProcessPath(message.Path, message.ImportMode);
|
||||
}
|
||||
}
|
||||
|
||||
return _downloadedEpisodesImportService.ProcessPath(message.Path);
|
||||
return _downloadedEpisodesImportService.ProcessPath(message.Path, message.ImportMode);
|
||||
}
|
||||
|
||||
public void Execute(DownloadedEpisodesScanCommand message)
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||
public interface IDownloadedEpisodesImportService
|
||||
{
|
||||
List<ImportResult> ProcessRootFolder(DirectoryInfo directoryInfo);
|
||||
List<ImportResult> ProcessPath(string path, Series series = null, DownloadClientItem downloadClientItem = null);
|
||||
List<ImportResult> ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Series series = null, DownloadClientItem downloadClientItem = null);
|
||||
bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Series series);
|
||||
}
|
||||
|
||||
@@ -56,20 +56,20 @@ namespace NzbDrone.Core.MediaFiles
|
||||
|
||||
foreach (var subFolder in _diskProvider.GetDirectories(directoryInfo.FullName))
|
||||
{
|
||||
var folderResults = ProcessFolder(new DirectoryInfo(subFolder));
|
||||
var folderResults = ProcessFolder(new DirectoryInfo(subFolder), ImportMode.Auto, null);
|
||||
results.AddRange(folderResults);
|
||||
}
|
||||
|
||||
foreach (var videoFile in _diskScanService.GetVideoFiles(directoryInfo.FullName, false))
|
||||
{
|
||||
var fileResults = ProcessFile(new FileInfo(videoFile));
|
||||
var fileResults = ProcessFile(new FileInfo(videoFile), ImportMode.Auto, null);
|
||||
results.AddRange(fileResults);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public List<ImportResult> ProcessPath(string path, Series series = null, DownloadClientItem downloadClientItem = null)
|
||||
public List<ImportResult> ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Series series = null, DownloadClientItem downloadClientItem = null)
|
||||
{
|
||||
if (_diskProvider.FolderExists(path))
|
||||
{
|
||||
@@ -77,10 +77,10 @@ namespace NzbDrone.Core.MediaFiles
|
||||
|
||||
if (series == null)
|
||||
{
|
||||
return ProcessFolder(directoryInfo, downloadClientItem);
|
||||
return ProcessFolder(directoryInfo, importMode, downloadClientItem);
|
||||
}
|
||||
|
||||
return ProcessFolder(directoryInfo, series, downloadClientItem);
|
||||
return ProcessFolder(directoryInfo, importMode, series, downloadClientItem);
|
||||
}
|
||||
|
||||
if (_diskProvider.FileExists(path))
|
||||
@@ -89,10 +89,10 @@ namespace NzbDrone.Core.MediaFiles
|
||||
|
||||
if (series == null)
|
||||
{
|
||||
return ProcessFile(fileInfo, downloadClientItem);
|
||||
return ProcessFile(fileInfo, importMode, downloadClientItem);
|
||||
}
|
||||
|
||||
return ProcessFile(fileInfo, series, downloadClientItem);
|
||||
return ProcessFile(fileInfo, importMode, series, downloadClientItem);
|
||||
}
|
||||
|
||||
_logger.Error("Import failed, path does not exist or is not accessible by Sonarr: {0}", path);
|
||||
@@ -133,7 +133,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem = null)
|
||||
private List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, ImportMode importMode, DownloadClientItem downloadClientItem)
|
||||
{
|
||||
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
|
||||
var series = _parsingService.GetSeries(cleanedUpName);
|
||||
@@ -148,11 +148,10 @@ namespace NzbDrone.Core.MediaFiles
|
||||
};
|
||||
}
|
||||
|
||||
return ProcessFolder(directoryInfo, series, downloadClientItem);
|
||||
return ProcessFolder(directoryInfo, importMode, series, downloadClientItem);
|
||||
}
|
||||
|
||||
private List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, Series series,
|
||||
DownloadClientItem downloadClientItem = null)
|
||||
private List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, ImportMode importMode, Series series, DownloadClientItem downloadClientItem)
|
||||
{
|
||||
if (_seriesService.SeriesPathExists(directoryInfo.FullName))
|
||||
{
|
||||
@@ -185,7 +184,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||
}
|
||||
|
||||
var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, folderInfo, true);
|
||||
var importResults = _importApprovedEpisodes.Import(decisions, true, downloadClientItem);
|
||||
var importResults = _importApprovedEpisodes.Import(decisions, true, downloadClientItem, importMode);
|
||||
|
||||
if ((downloadClientItem == null || !downloadClientItem.IsReadOnly) &&
|
||||
importResults.Any(i => i.Result == ImportResultType.Imported) &&
|
||||
@@ -198,7 +197,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||
return importResults;
|
||||
}
|
||||
|
||||
private List<ImportResult> ProcessFile(FileInfo fileInfo, DownloadClientItem downloadClientItem = null)
|
||||
private List<ImportResult> ProcessFile(FileInfo fileInfo, ImportMode importMode, DownloadClientItem downloadClientItem)
|
||||
{
|
||||
var series = _parsingService.GetSeries(Path.GetFileNameWithoutExtension(fileInfo.Name));
|
||||
|
||||
@@ -212,10 +211,10 @@ namespace NzbDrone.Core.MediaFiles
|
||||
};
|
||||
}
|
||||
|
||||
return ProcessFile(fileInfo, series, downloadClientItem);
|
||||
return ProcessFile(fileInfo, importMode, series, downloadClientItem);
|
||||
}
|
||||
|
||||
private List<ImportResult> ProcessFile(FileInfo fileInfo, Series series, DownloadClientItem downloadClientItem = null)
|
||||
private List<ImportResult> ProcessFile(FileInfo fileInfo, ImportMode importMode, Series series, DownloadClientItem downloadClientItem)
|
||||
{
|
||||
if (Path.GetFileNameWithoutExtension(fileInfo.Name).StartsWith("._"))
|
||||
{
|
||||
@@ -240,7 +239,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||
|
||||
var decisions = _importDecisionMaker.GetImportDecisions(new List<string>() { fileInfo.FullName }, series, null, true);
|
||||
|
||||
return _importApprovedEpisodes.Import(decisions, true, downloadClientItem);
|
||||
return _importApprovedEpisodes.Import(decisions, true, downloadClientItem, importMode);
|
||||
}
|
||||
|
||||
private string GetCleanedUpFolderName(string folder)
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
||||
{
|
||||
public interface IImportApprovedEpisodes
|
||||
{
|
||||
List<ImportResult> Import(List<ImportDecision> decisions, bool newDownload, DownloadClientItem downloadClientItem = null);
|
||||
List<ImportResult> Import(List<ImportDecision> decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto);
|
||||
}
|
||||
|
||||
public class ImportApprovedEpisodes : IImportApprovedEpisodes
|
||||
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<ImportResult> Import(List<ImportDecision> decisions, bool newDownload, DownloadClientItem downloadClientItem = null)
|
||||
public List<ImportResult> Import(List<ImportDecision> decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto)
|
||||
{
|
||||
var qualifiedImports = decisions.Where(c => c.Approved)
|
||||
.GroupBy(c => c.LocalEpisode.Series.Id, (i, s) => s
|
||||
@@ -85,10 +85,23 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
||||
episodeFile.Episodes = localEpisode.Episodes;
|
||||
episodeFile.ReleaseGroup = localEpisode.ParsedEpisodeInfo.ReleaseGroup;
|
||||
|
||||
bool copyOnly;
|
||||
switch (importMode)
|
||||
{
|
||||
default:
|
||||
case ImportMode.Auto:
|
||||
copyOnly = downloadClientItem != null && downloadClientItem.IsReadOnly;
|
||||
break;
|
||||
case ImportMode.Move:
|
||||
copyOnly = false;
|
||||
break;
|
||||
case ImportMode.Copy:
|
||||
copyOnly = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (newDownload)
|
||||
{
|
||||
bool copyOnly = downloadClientItem != null && downloadClientItem.IsReadOnly;
|
||||
|
||||
episodeFile.SceneName = GetSceneName(downloadClientItem, localEpisode);
|
||||
|
||||
var moveResult = _episodeFileUpgrader.UpgradeEpisodeFile(episodeFile, localEpisode, copyOnly);
|
||||
@@ -104,7 +117,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
||||
|
||||
if (newDownload)
|
||||
{
|
||||
_extraService.ImportExtraFiles(localEpisode, episodeFile, downloadClientItem != null && downloadClientItem.IsReadOnly);
|
||||
_extraService.ImportExtraFiles(localEpisode, episodeFile, copyOnly);
|
||||
}
|
||||
|
||||
if (downloadClientItem != null)
|
||||
|
||||
11
src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportMode.cs
Normal file
11
src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportMode.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
||||
{
|
||||
public enum ImportMode
|
||||
{
|
||||
Auto = 0,
|
||||
Move = 1,
|
||||
Copy = 2
|
||||
}
|
||||
}
|
||||
@@ -14,5 +14,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public ImportMode ImportMode { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
||||
|
||||
public void Execute(ManualImportCommand message)
|
||||
{
|
||||
_logger.ProgressTrace("Manually importing {0} files", message.Files.Count);
|
||||
_logger.ProgressTrace("Manually importing {0} files using mode {1}", message.Files.Count, message.ImportMode);
|
||||
|
||||
var imported = new List<ImportResult>();
|
||||
var importedTrackedDownload = new List<ManuallyImportedFile>();
|
||||
@@ -217,20 +217,19 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
||||
Size = 0
|
||||
};
|
||||
|
||||
//TODO: Option to copy instead of import
|
||||
//TODO: Cleanup non-tracked downloads
|
||||
|
||||
var importDecision = new ImportDecision(localEpisode);
|
||||
|
||||
if (file.DownloadId.IsNullOrWhiteSpace())
|
||||
{
|
||||
imported.AddRange(_importApprovedEpisodes.Import(new List<ImportDecision> { importDecision }, !existingFile));
|
||||
imported.AddRange(_importApprovedEpisodes.Import(new List<ImportDecision> { importDecision }, !existingFile, null, message.ImportMode));
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
var trackedDownload = _trackedDownloadService.Find(file.DownloadId);
|
||||
var importResult = _importApprovedEpisodes.Import(new List<ImportDecision> { importDecision }, true, trackedDownload.DownloadItem).First();
|
||||
var importResult = _importApprovedEpisodes.Import(new List<ImportDecision> { importDecision }, true, trackedDownload.DownloadItem, message.ImportMode).First();
|
||||
|
||||
imported.Add(importResult);
|
||||
|
||||
|
||||
@@ -45,25 +45,17 @@ namespace NzbDrone.Core.Notifications.Email
|
||||
{
|
||||
_logger.Error("Error sending email. Subject: {0}", email.Subject);
|
||||
_logger.Debug(ex, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void Send(MailMessage email, string server, int port, bool ssl, NetworkCredential credentials)
|
||||
{
|
||||
try
|
||||
{
|
||||
var smtp = new SmtpClient(server, port);
|
||||
smtp.EnableSsl = ssl;
|
||||
smtp.Credentials = credentials;
|
||||
var smtp = new SmtpClient(server, port);
|
||||
smtp.EnableSsl = ssl;
|
||||
smtp.Credentials = credentials;
|
||||
|
||||
smtp.Send(email);
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "There was an error sending an email.");
|
||||
throw;
|
||||
}
|
||||
smtp.Send(email);
|
||||
}
|
||||
|
||||
public ValidationFailure Test(EmailSettings settings)
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace NzbDrone.Core.Notifications.Plex.Models
|
||||
{
|
||||
public class PlexPreferences
|
||||
{
|
||||
[JsonProperty("_children")]
|
||||
[JsonProperty("Setting")]
|
||||
public List<PlexPreference> Preferences { get; set; }
|
||||
}
|
||||
|
||||
@@ -15,4 +15,10 @@ namespace NzbDrone.Core.Notifications.Plex.Models
|
||||
public string Type { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public class PlexPreferencesLegacy
|
||||
{
|
||||
[JsonProperty("_children")]
|
||||
public List<PlexPreference> Preferences { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace NzbDrone.Core.Notifications.Plex.Models
|
||||
{
|
||||
public class PlexResponse<T>
|
||||
{
|
||||
public T MediaContainer { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,10 @@ using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Plex.Models
|
||||
{
|
||||
public class PlexSectionDetails
|
||||
public class PlexSectionLocation
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Path { get; set; }
|
||||
public string Language { get; set; }
|
||||
}
|
||||
|
||||
public class PlexSection
|
||||
@@ -18,13 +17,31 @@ namespace NzbDrone.Core.Notifications.Plex.Models
|
||||
public string Type { get; set; }
|
||||
public string Language { get; set; }
|
||||
|
||||
[JsonProperty("_children")]
|
||||
public List<PlexSectionDetails> Sections { get; set; }
|
||||
[JsonProperty("Location")]
|
||||
public List<PlexSectionLocation> Locations { get; set; }
|
||||
}
|
||||
|
||||
public class PlexMediaContainer
|
||||
public class PlexSectionsContainer
|
||||
{
|
||||
[JsonProperty("Directory")]
|
||||
public List<PlexSection> Sections { get; set; }
|
||||
}
|
||||
|
||||
public class PlexSectionLegacy
|
||||
{
|
||||
[JsonProperty("key")]
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Type { get; set; }
|
||||
public string Language { get; set; }
|
||||
|
||||
[JsonProperty("_children")]
|
||||
public List<PlexSectionLocation> Locations { get; set; }
|
||||
}
|
||||
|
||||
public class PlexMediaContainerLegacy
|
||||
{
|
||||
[JsonProperty("_children")]
|
||||
public List<PlexSection> Directories { get; set; }
|
||||
public List<PlexSectionLegacy> Sections { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ namespace NzbDrone.Core.Notifications.Plex.Models
|
||||
}
|
||||
|
||||
public class PlexSectionResponse
|
||||
{
|
||||
[JsonProperty("Metadata")]
|
||||
public List<PlexSectionItem> Items { get; set; }
|
||||
}
|
||||
|
||||
public class PlexSectionResponseLegacy
|
||||
{
|
||||
[JsonProperty("_children")]
|
||||
public List<PlexSectionItem> Items { get; set; }
|
||||
|
||||
@@ -44,8 +44,24 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||
_logger.Trace("Sections response: {0}", response.Content);
|
||||
CheckForError(response, settings);
|
||||
|
||||
return Json.Deserialize<PlexMediaContainer>(response.Content)
|
||||
.Directories
|
||||
if (response.Content.Contains("_children"))
|
||||
{
|
||||
return Json.Deserialize<PlexMediaContainerLegacy>(response.Content)
|
||||
.Sections
|
||||
.Where(d => d.Type == "show")
|
||||
.Select(s => new PlexSection
|
||||
{
|
||||
Id = s.Id,
|
||||
Language = s.Language,
|
||||
Locations = s.Locations,
|
||||
Type = s.Type
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return Json.Deserialize<PlexResponse<PlexSectionsContainer>>(response.Content)
|
||||
.MediaContainer
|
||||
.Sections
|
||||
.Where(d => d.Type == "show")
|
||||
.ToList();
|
||||
}
|
||||
@@ -81,7 +97,15 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||
_logger.Trace("Version response: {0}", response.Content);
|
||||
CheckForError(response, settings);
|
||||
|
||||
return Json.Deserialize<PlexIdentity>(response.Content).Version;
|
||||
if (response.Content.Contains("_children"))
|
||||
{
|
||||
return Json.Deserialize<PlexIdentity>(response.Content)
|
||||
.Version;
|
||||
}
|
||||
|
||||
return Json.Deserialize<PlexResponse<PlexIdentity>>(response.Content)
|
||||
.MediaContainer
|
||||
.Version;
|
||||
}
|
||||
|
||||
public List<PlexPreference> Preferences(PlexServerSettings settings)
|
||||
@@ -93,7 +117,15 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||
_logger.Trace("Preferences response: {0}", response.Content);
|
||||
CheckForError(response, settings);
|
||||
|
||||
return Json.Deserialize<PlexPreferences>(response.Content).Preferences;
|
||||
if (response.Content.Contains("_children"))
|
||||
{
|
||||
return Json.Deserialize<PlexPreferencesLegacy>(response.Content)
|
||||
.Preferences;
|
||||
}
|
||||
|
||||
return Json.Deserialize<PlexResponse<PlexPreferences>>(response.Content)
|
||||
.MediaContainer
|
||||
.Preferences;
|
||||
}
|
||||
|
||||
public int? GetMetadataId(int sectionId, int tvdbId, string language, PlexServerSettings settings)
|
||||
@@ -107,8 +139,20 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||
_logger.Trace("Sections response: {0}", response.Content);
|
||||
CheckForError(response, settings);
|
||||
|
||||
var items = Json.Deserialize<PlexSectionResponse>(response.Content)
|
||||
.Items;
|
||||
List<PlexSectionItem> items;
|
||||
|
||||
if (response.Content.Contains("_children"))
|
||||
{
|
||||
items = Json.Deserialize<PlexSectionResponseLegacy>(response.Content)
|
||||
.Items;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
items = Json.Deserialize<PlexResponse<PlexSectionResponse>>(response.Content)
|
||||
.MediaContainer
|
||||
.Items;
|
||||
}
|
||||
|
||||
if (items == null || items.Empty())
|
||||
{
|
||||
@@ -203,7 +247,15 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||
throw new PlexAuthenticationException("Unauthorized - Username or password is incorrect");
|
||||
}
|
||||
|
||||
var error = Json.Deserialize<PlexError>(response.Content);
|
||||
if (response.Content.IsNullOrWhiteSpace())
|
||||
{
|
||||
_logger.Trace("No response body returned, no error detected");
|
||||
return;
|
||||
}
|
||||
|
||||
var error = response.Content.Contains("_children") ?
|
||||
Json.Deserialize<PlexError>(response.Content) :
|
||||
Json.Deserialize<PlexResponse<PlexError>>(response.Content).MediaContainer;
|
||||
|
||||
if (error != null && !error.Error.IsNullOrWhiteSpace())
|
||||
{
|
||||
|
||||
@@ -19,12 +19,14 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||
|
||||
public class PlexServerService : IPlexServerService
|
||||
{
|
||||
private readonly ICached<Version> _versionCache;
|
||||
private readonly ICached<bool> _partialUpdateCache;
|
||||
private readonly IPlexServerProxy _plexServerProxy;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public PlexServerService(ICacheManager cacheManager, IPlexServerProxy plexServerProxy, Logger logger)
|
||||
{
|
||||
_versionCache = cacheManager.GetCache<Version>(GetType(), "versionCache");
|
||||
_partialUpdateCache = cacheManager.GetCache<bool>(GetType(), "partialUpdateCache");
|
||||
_plexServerProxy = plexServerProxy;
|
||||
_logger = logger;
|
||||
@@ -35,9 +37,12 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||
try
|
||||
{
|
||||
_logger.Debug("Sending Update Request to Plex Server");
|
||||
|
||||
|
||||
var version = _versionCache.Get(settings.Host, () => GetVersion(settings), TimeSpan.FromHours(2));
|
||||
ValidateVersion(version);
|
||||
|
||||
var sections = GetSections(settings);
|
||||
var partialUpdates = _partialUpdateCache.Get(settings.Host, () => PartialUpdatesAllowed(settings), TimeSpan.FromHours(2));
|
||||
var partialUpdates = _partialUpdateCache.Get(settings.Host, () => PartialUpdatesAllowed(settings, version), TimeSpan.FromHours(2));
|
||||
|
||||
if (partialUpdates)
|
||||
{
|
||||
@@ -64,13 +69,10 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||
return _plexServerProxy.GetTvSections(settings).ToList();
|
||||
}
|
||||
|
||||
private bool PartialUpdatesAllowed(PlexServerSettings settings)
|
||||
private bool PartialUpdatesAllowed(PlexServerSettings settings, Version version)
|
||||
{
|
||||
try
|
||||
{
|
||||
var rawVersion = GetVersion(settings);
|
||||
var version = new Version(Regex.Match(rawVersion, @"^(\d+[.-]){4}").Value.Trim('.', '-'));
|
||||
|
||||
if (version >= new Version(0, 9, 12, 0))
|
||||
{
|
||||
var preferences = GetPreferences(settings);
|
||||
@@ -92,13 +94,28 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||
return false;
|
||||
}
|
||||
|
||||
private string GetVersion(PlexServerSettings settings)
|
||||
private void ValidateVersion(Version version)
|
||||
{
|
||||
if (version >= new Version(1, 3, 0) && version < new Version(1, 3, 1))
|
||||
{
|
||||
throw new PlexVersionException("Found version {0}, upgrade to PMS 1.3.1 to fix library updating and then restart Sonarr", version);
|
||||
}
|
||||
}
|
||||
|
||||
private Version GetVersion(PlexServerSettings settings)
|
||||
{
|
||||
_logger.Debug("Getting version from Plex host: {0}", settings.Host);
|
||||
|
||||
return _plexServerProxy.Version(settings);
|
||||
var rawVersion = _plexServerProxy.Version(settings);
|
||||
var version = new Version(Regex.Match(rawVersion, @"^(\d+[.-]){4}").Value.Trim('.', '-'));
|
||||
|
||||
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private List<PlexPreference> GetPreferences(PlexServerSettings settings)
|
||||
{
|
||||
_logger.Debug("Getting preferences from Plex host: {0}", settings.Host);
|
||||
|
||||
15
src/NzbDrone.Core/Notifications/Plex/PlexVersionException.cs
Normal file
15
src/NzbDrone.Core/Notifications/Plex/PlexVersionException.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using NzbDrone.Common.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Plex
|
||||
{
|
||||
public class PlexVersionException : NzbDroneException
|
||||
{
|
||||
public PlexVersionException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public PlexVersionException(string message, params object[] args) : base(message, args)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Telegram
|
||||
{
|
||||
public class InvalidResponseException : Exception
|
||||
{
|
||||
public InvalidResponseException()
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidResponseException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/NzbDrone.Core/Notifications/Telegram/Telegram.cs
Normal file
65
src/NzbDrone.Core/Notifications/Telegram/Telegram.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation.Results;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Telegram
|
||||
{
|
||||
public class Telegram : NotificationBase<TelegramSettings>
|
||||
{
|
||||
private readonly ITelegramProxy _proxy;
|
||||
|
||||
public Telegram(ITelegramProxy proxy)
|
||||
{
|
||||
_proxy = proxy;
|
||||
}
|
||||
|
||||
public override string Link
|
||||
{
|
||||
get { return "https://telegram.org/"; }
|
||||
}
|
||||
|
||||
public override void OnGrab(GrabMessage grabMessage)
|
||||
{
|
||||
const string title = "Episode Grabbed";
|
||||
|
||||
_proxy.SendNotification(title, grabMessage.Message, Settings);
|
||||
}
|
||||
|
||||
public override void OnDownload(DownloadMessage message)
|
||||
{
|
||||
const string title = "Episode Downloaded";
|
||||
|
||||
_proxy.SendNotification(title, message.Message, Settings);
|
||||
}
|
||||
|
||||
public override void OnRename(Series series)
|
||||
{
|
||||
}
|
||||
|
||||
public override string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Telegram";
|
||||
}
|
||||
}
|
||||
|
||||
public override bool SupportsOnRename
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override ValidationResult Test()
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
failures.AddIfNotNull(_proxy.Test(Settings));
|
||||
|
||||
return new ValidationResult(failures);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/NzbDrone.Core/Notifications/Telegram/TelegramError.cs
Normal file
14
src/NzbDrone.Core/Notifications/Telegram/TelegramError.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Telegram
|
||||
{
|
||||
public class TelegramError
|
||||
{
|
||||
public bool Ok { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "error_code")]
|
||||
public int ErrorCode { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
}
|
||||
}
|
||||
72
src/NzbDrone.Core/Notifications/Telegram/TelegramService.cs
Normal file
72
src/NzbDrone.Core/Notifications/Telegram/TelegramService.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using RestSharp;
|
||||
using NzbDrone.Core.Rest;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Telegram
|
||||
{
|
||||
public interface ITelegramProxy
|
||||
{
|
||||
void SendNotification(string title, string message, TelegramSettings settings);
|
||||
ValidationFailure Test(TelegramSettings settings);
|
||||
}
|
||||
|
||||
public class TelegramProxy : ITelegramProxy
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private const string URL = "https://api.telegram.org";
|
||||
|
||||
public TelegramProxy(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void SendNotification(string title, string message, TelegramSettings settings)
|
||||
{
|
||||
//Format text to add the title before and bold using markdown
|
||||
var text = $"*{title}*\n{message}";
|
||||
var client = RestClientFactory.BuildClient(URL);
|
||||
var request = new RestRequest("bot{token}/sendmessage", Method.POST);
|
||||
|
||||
request.AddUrlSegment("token", settings.BotToken);
|
||||
request.AddParameter("chat_id", settings.ChatId);
|
||||
request.AddParameter("parse_mode", "Markdown");
|
||||
request.AddParameter("text", text);
|
||||
|
||||
client.ExecuteAndValidate(request);
|
||||
}
|
||||
|
||||
public ValidationFailure Test(TelegramSettings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
const string title = "Test Notification";
|
||||
const string body = "This is a test message from Sonarr";
|
||||
|
||||
SendNotification(title, body, settings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send test message: " + ex.Message);
|
||||
|
||||
var restException = ex as RestException;
|
||||
|
||||
if (restException != null && restException.Response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
var error = Json.Deserialize<TelegramError>(restException.Response.Content);
|
||||
var property = error.Description.ContainsIgnoreCase("chat not found") ? "ChatId" : "BotToken";
|
||||
|
||||
return new ValidationFailure(property, error.Description);
|
||||
}
|
||||
|
||||
return new ValidationFailure("BotToken", "Unable to send test message");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs
Normal file
40
src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Telegram
|
||||
{
|
||||
public class TelegramSettingsValidator : AbstractValidator<TelegramSettings>
|
||||
{
|
||||
public TelegramSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.BotToken).NotEmpty();
|
||||
RuleFor(c => c.ChatId).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class TelegramSettings : IProviderConfig
|
||||
{
|
||||
private static readonly TelegramSettingsValidator Validator = new TelegramSettingsValidator();
|
||||
|
||||
[FieldDefinition(0, Label = "Bot Token", HelpLink = "https://core.telegram.org/bots")]
|
||||
public string BotToken { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Chat ID", HelpLink = "http://stackoverflow.com/a/37396871/882971", HelpText = "You must start a conversation with the bot or add it to your group to receive messages")]
|
||||
public string ChatId { get; set; }
|
||||
|
||||
public bool IsValid
|
||||
{
|
||||
get
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(ChatId) && !string.IsNullOrWhiteSpace(BotToken);
|
||||
}
|
||||
}
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -401,6 +401,7 @@
|
||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexVersionResponse.cs" />
|
||||
<Compile Include="Download\Clients\Pneumatic\Pneumatic.cs" />
|
||||
<Compile Include="Download\Clients\Pneumatic\PneumaticSettings.cs" />
|
||||
<Compile Include="Download\Clients\QBittorrent\QBittorrentPreferences.cs" />
|
||||
<Compile Include="Download\Clients\rTorrent\RTorrentDirectoryValidator.cs" />
|
||||
<Compile Include="Download\Clients\QBittorrent\QBittorrent.cs" />
|
||||
<Compile Include="Download\Clients\QBittorrent\QBittorrentPriority.cs" />
|
||||
@@ -685,6 +686,7 @@
|
||||
<Compile Include="Lifecycle\Commands\ShutdownCommand.cs" />
|
||||
<Compile Include="Lifecycle\LifecycleService.cs" />
|
||||
<Compile Include="MediaCover\CoverAlreadyExistsSpecification.cs" />
|
||||
<Compile Include="MediaCover\GdiPlusInterop.cs" />
|
||||
<Compile Include="MediaCover\MediaCover.cs" />
|
||||
<Compile Include="MediaCover\ImageResizer.cs" />
|
||||
<Compile Include="MediaCover\MediaCoverService.cs" />
|
||||
@@ -692,6 +694,7 @@
|
||||
<Compile Include="MediaFiles\Commands\BackendCommandAttribute.cs" />
|
||||
<Compile Include="MediaFiles\Commands\CleanUpRecycleBinCommand.cs" />
|
||||
<Compile Include="MediaFiles\Commands\DownloadedEpisodesScanCommand.cs" />
|
||||
<Compile Include="MediaFiles\EpisodeImport\ImportMode.cs" />
|
||||
<Compile Include="MediaFiles\Commands\RenameFilesCommand.cs" />
|
||||
<Compile Include="MediaFiles\Commands\RenameSeriesCommand.cs" />
|
||||
<Compile Include="MediaFiles\Commands\RescanSeriesCommand.cs" />
|
||||
@@ -819,12 +822,14 @@
|
||||
<Compile Include="Notifications\Boxcar\BoxcarSettings.cs" />
|
||||
<Compile Include="Notifications\GrabMessage.cs" />
|
||||
<Compile Include="Notifications\Plex\Models\PlexIdentity.cs" />
|
||||
<Compile Include="Notifications\Plex\Models\PlexResponse.cs" />
|
||||
<Compile Include="Notifications\Plex\Models\PlexPreferences.cs" />
|
||||
<Compile Include="Notifications\Plex\Models\PlexSectionItem.cs" />
|
||||
<Compile Include="Notifications\Plex\Models\PlexSection.cs" />
|
||||
<Compile Include="Notifications\Plex\PlexAuthenticationException.cs" />
|
||||
<Compile Include="Notifications\CustomScript\CustomScript.cs" />
|
||||
<Compile Include="Notifications\CustomScript\CustomScriptSettings.cs" />
|
||||
<Compile Include="Notifications\Plex\PlexVersionException.cs" />
|
||||
<Compile Include="Notifications\Plex\PlexHomeTheater.cs" />
|
||||
<Compile Include="Notifications\Plex\PlexHomeTheaterSettings.cs" />
|
||||
<Compile Include="Notifications\Plex\PlexClientService.cs" />
|
||||
@@ -838,6 +843,10 @@
|
||||
<Compile Include="Notifications\Synology\SynologyIndexer.cs" />
|
||||
<Compile Include="Notifications\Synology\SynologyIndexerProxy.cs" />
|
||||
<Compile Include="Notifications\Synology\SynologyIndexerSettings.cs" />
|
||||
<Compile Include="Notifications\Telegram\InvalidResponseException.cs" />
|
||||
<Compile Include="Notifications\Telegram\Telegram.cs" />
|
||||
<Compile Include="Notifications\Telegram\TelegramService.cs" />
|
||||
<Compile Include="Notifications\Telegram\TelegramSettings.cs" />
|
||||
<Compile Include="Notifications\Twitter\OAuthToken.cs" />
|
||||
<Compile Include="Notifications\Twitter\TwitterException.cs" />
|
||||
<Compile Include="Notifications\Webhook\WebhookEpisode.cs" />
|
||||
@@ -1169,6 +1178,7 @@
|
||||
<Link>libsqlite3.0.dylib</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Compile Include="Notifications\Telegram\TelegramError.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
|
||||
@@ -470,6 +470,10 @@ namespace NzbDrone.Core.Organizer
|
||||
}
|
||||
break;
|
||||
|
||||
case "MPEG-2 Video":
|
||||
videoCodec = "MPEG2";
|
||||
break;
|
||||
|
||||
default:
|
||||
videoCodec = episodeFile.MediaInfo.VideoCodec;
|
||||
break;
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Mono.Unix;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Mono.Disk
|
||||
{
|
||||
@@ -51,5 +52,18 @@ namespace NzbDrone.Mono.Disk
|
||||
{
|
||||
get { return _unixDriveInfo.VolumeLabel; }
|
||||
}
|
||||
|
||||
public string VolumeName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (VolumeLabel.IsNullOrWhiteSpace())
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
return string.Format("{0} ({1})", Name, VolumeLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace NzbDrone.Mono.Disk
|
||||
{
|
||||
_logger.Debug(ex, "Failed to get filesystem types from {0}, using default set.", PROC_FILESYSTEMS_FILENAME);
|
||||
}
|
||||
|
||||
|
||||
if (result.Empty())
|
||||
{
|
||||
foreach (var type in _fixedTypes)
|
||||
@@ -96,6 +96,7 @@ namespace NzbDrone.Mono.Disk
|
||||
if (split.Length != 6)
|
||||
{
|
||||
_logger.Debug("Unable to parse {0} line: {1}", PROC_MOUNTS_FILENAME, line);
|
||||
return null;
|
||||
}
|
||||
|
||||
var name = split[0];
|
||||
@@ -103,6 +104,13 @@ namespace NzbDrone.Mono.Disk
|
||||
var type = split[2];
|
||||
var options = ParseOptions(split[3]);
|
||||
|
||||
if (!mount.StartsWith("/"))
|
||||
{
|
||||
// Possible a namespace like 'net:[1234]'.
|
||||
// But we just filter anything not starting with /, we're not interested in anything that isn't mounted somewhere.
|
||||
return null;
|
||||
}
|
||||
|
||||
var driveType = FindDriveType.Find(type);
|
||||
|
||||
if (name.StartsWith("/dev/") || GetFileSystems().GetValueOrDefault(type, false))
|
||||
|
||||
@@ -27,7 +27,8 @@ module.exports = Marionette.Layout.extend({
|
||||
},
|
||||
|
||||
ui : {
|
||||
importButton : '.x-import'
|
||||
importButton : '.x-import',
|
||||
importMode : '.x-importmode'
|
||||
},
|
||||
|
||||
events : {
|
||||
@@ -94,6 +95,7 @@ module.exports = Marionette.Layout.extend({
|
||||
this.folder = options.folder;
|
||||
this.downloadId = options.downloadId;
|
||||
this.title = options.title;
|
||||
this.importMode = options.importMode || 'Move';
|
||||
|
||||
this.templateHelpers = {
|
||||
title : this.title || this.folder
|
||||
@@ -105,11 +107,13 @@ module.exports = Marionette.Layout.extend({
|
||||
if (this.folder || this.downloadId) {
|
||||
this._showLoading();
|
||||
this._loadCollection();
|
||||
this.ui.importMode.val(this.importMode);
|
||||
}
|
||||
|
||||
else {
|
||||
this._showSelectFolder();
|
||||
this.ui.importButton.hide();
|
||||
this.ui.importMode.hide();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -196,6 +200,8 @@ module.exports = Marionette.Layout.extend({
|
||||
return;
|
||||
}
|
||||
|
||||
var importMode = this.ui.importMode.val();
|
||||
|
||||
CommandController.Execute('manualImport', {
|
||||
name : 'manualImport',
|
||||
files : _.map(selected, function (file) {
|
||||
@@ -206,7 +212,8 @@ module.exports = Marionette.Layout.extend({
|
||||
quality : file.get('quality'),
|
||||
downloadId : file.get('downloadId')
|
||||
};
|
||||
})
|
||||
}),
|
||||
importMode : importMode
|
||||
});
|
||||
|
||||
vent.trigger(vent.Commands.CloseModalCommand);
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
<div class="x-footer"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="col-md-2 pull-left">
|
||||
<select class="form-control x-importmode">
|
||||
<option value="Move">Move Files</option>
|
||||
<option value="Copy">Copy Files</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button class="btn btn-success x-import" disabled="disabled">Import</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user