parent
5ce8ea8985
commit
fb76c237bf
|
@ -0,0 +1,371 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Download;
|
||||||
|
using NzbDrone.Core.Download.Clients;
|
||||||
|
using NzbDrone.Core.Download.Clients.FreeboxDownload;
|
||||||
|
using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Download.DownloadClientTests.FreeboxDownloadTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class TorrentFreeboxDownloadFixture : DownloadClientFixtureBase<TorrentFreeboxDownload>
|
||||||
|
{
|
||||||
|
protected FreeboxDownloadSettings _settings;
|
||||||
|
|
||||||
|
protected FreeboxDownloadConfiguration _downloadConfiguration;
|
||||||
|
|
||||||
|
protected FreeboxDownloadTask _task;
|
||||||
|
|
||||||
|
protected string _defaultDestination = @"/some/path";
|
||||||
|
protected string _encodedDefaultDestination = "L3NvbWUvcGF0aA==";
|
||||||
|
protected string _category = "somecat";
|
||||||
|
protected string _encodedDefaultDestinationAndCategory = "L3NvbWUvcGF0aC9zb21lY2F0";
|
||||||
|
protected string _destinationDirectory = @"/path/to/media";
|
||||||
|
protected string _encodedDestinationDirectory = "L3BhdGgvdG8vbWVkaWE=";
|
||||||
|
protected OsPath _physicalPath = new OsPath("/mnt/sdb1/mydata");
|
||||||
|
protected string _downloadURL => "magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcad53426&dn=download";
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
Subject.Definition = new DownloadClientDefinition();
|
||||||
|
|
||||||
|
_settings = new FreeboxDownloadSettings()
|
||||||
|
{
|
||||||
|
Host = "127.0.0.1",
|
||||||
|
Port = 443,
|
||||||
|
ApiUrl = "/api/v1/",
|
||||||
|
AppId = "someid",
|
||||||
|
AppToken = "S0mEv3RY1oN9T0k3n"
|
||||||
|
};
|
||||||
|
|
||||||
|
Subject.Definition.Settings = _settings;
|
||||||
|
|
||||||
|
_downloadConfiguration = new FreeboxDownloadConfiguration()
|
||||||
|
{
|
||||||
|
DownloadDirectory = _encodedDefaultDestination
|
||||||
|
};
|
||||||
|
|
||||||
|
_task = new FreeboxDownloadTask()
|
||||||
|
{
|
||||||
|
Id = "id0",
|
||||||
|
Name = "name",
|
||||||
|
DownloadDirectory = "L3NvbWUvcGF0aA==",
|
||||||
|
InfoHash = "HASH",
|
||||||
|
QueuePosition = 1,
|
||||||
|
Status = FreeboxDownloadTaskStatus.Unknown,
|
||||||
|
Eta = 0,
|
||||||
|
Error = "none",
|
||||||
|
Type = FreeboxDownloadTaskType.Bt.ToString(),
|
||||||
|
IoPriority = FreeboxDownloadTaskIoPriority.Normal.ToString(),
|
||||||
|
StopRatio = 150,
|
||||||
|
PieceLength = 125,
|
||||||
|
CreatedTimestamp = 1665261599,
|
||||||
|
Size = 1000,
|
||||||
|
ReceivedPrct = 0,
|
||||||
|
ReceivedBytes = 0,
|
||||||
|
ReceivedRate = 0,
|
||||||
|
TransmittedPrct = 0,
|
||||||
|
TransmittedBytes = 0,
|
||||||
|
TransmittedRate = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Mocker.GetMock<IHttpClient>()
|
||||||
|
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
|
||||||
|
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void GivenCategory()
|
||||||
|
{
|
||||||
|
_settings.Category = _category;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void GivenDestinationDirectory()
|
||||||
|
{
|
||||||
|
_settings.DestinationDirectory = _destinationDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void GivenDownloadConfiguration()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||||
|
.Setup(s => s.GetDownloadConfiguration(It.IsAny<FreeboxDownloadSettings>()))
|
||||||
|
.Returns(_downloadConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void GivenTasks(List<FreeboxDownloadTask> torrents)
|
||||||
|
{
|
||||||
|
if (torrents == null)
|
||||||
|
{
|
||||||
|
torrents = new List<FreeboxDownloadTask>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||||
|
.Setup(s => s.GetTasks(It.IsAny<FreeboxDownloadSettings>()))
|
||||||
|
.Returns(torrents);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void PrepareClientToReturnQueuedItem()
|
||||||
|
{
|
||||||
|
_task.Status = FreeboxDownloadTaskStatus.Queued;
|
||||||
|
|
||||||
|
GivenTasks(new List<FreeboxDownloadTask>
|
||||||
|
{
|
||||||
|
_task
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void GivenSuccessfulDownload()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IHttpClient>()
|
||||||
|
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
|
||||||
|
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[1000]));
|
||||||
|
|
||||||
|
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||||
|
.Setup(s => s.AddTaskFromUrl(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<double?>(), It.IsAny<FreeboxDownloadSettings>()))
|
||||||
|
.Callback(PrepareClientToReturnQueuedItem);
|
||||||
|
|
||||||
|
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||||
|
.Setup(s => s.AddTaskFromFile(It.IsAny<string>(), It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<double?>(), It.IsAny<FreeboxDownloadSettings>()))
|
||||||
|
.Callback(PrepareClientToReturnQueuedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override RemoteEpisode CreateRemoteEpisode()
|
||||||
|
{
|
||||||
|
var episode = base.CreateRemoteEpisode();
|
||||||
|
|
||||||
|
episode.Release.DownloadUrl = _downloadURL;
|
||||||
|
|
||||||
|
return episode;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Download_with_DestinationDirectory_should_force_directory()
|
||||||
|
{
|
||||||
|
GivenDestinationDirectory();
|
||||||
|
GivenSuccessfulDownload();
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
|
||||||
|
Subject.Download(remoteEpisode);
|
||||||
|
|
||||||
|
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||||
|
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), _encodedDestinationDirectory, It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<double?>(), It.IsAny<FreeboxDownloadSettings>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Download_with_Category_should_force_directory()
|
||||||
|
{
|
||||||
|
GivenDownloadConfiguration();
|
||||||
|
GivenCategory();
|
||||||
|
GivenSuccessfulDownload();
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
|
||||||
|
Subject.Download(remoteEpisode);
|
||||||
|
|
||||||
|
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||||
|
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), _encodedDefaultDestinationAndCategory, It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<double?>(), It.IsAny<FreeboxDownloadSettings>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Download_without_DestinationDirectory_and_Category_should_use_default()
|
||||||
|
{
|
||||||
|
GivenDownloadConfiguration();
|
||||||
|
GivenSuccessfulDownload();
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
|
||||||
|
Subject.Download(remoteEpisode);
|
||||||
|
|
||||||
|
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||||
|
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), _encodedDefaultDestination, It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<double?>(), It.IsAny<FreeboxDownloadSettings>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(false, false)]
|
||||||
|
[TestCase(true, true)]
|
||||||
|
public void Download_should_pause_torrent_as_expected(bool addPausedSetting, bool toBePausedFlag)
|
||||||
|
{
|
||||||
|
_settings.AddPaused = addPausedSetting;
|
||||||
|
|
||||||
|
GivenDownloadConfiguration();
|
||||||
|
GivenSuccessfulDownload();
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
|
||||||
|
Subject.Download(remoteEpisode);
|
||||||
|
|
||||||
|
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||||
|
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), It.IsAny<string>(), toBePausedFlag, It.IsAny<bool>(), It.IsAny<double?>(), It.IsAny<FreeboxDownloadSettings>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(0, (int)FreeboxDownloadPriority.First, (int)FreeboxDownloadPriority.First, true)]
|
||||||
|
[TestCase(0, (int)FreeboxDownloadPriority.Last, (int)FreeboxDownloadPriority.First, true)]
|
||||||
|
[TestCase(0, (int)FreeboxDownloadPriority.First, (int)FreeboxDownloadPriority.Last, false)]
|
||||||
|
[TestCase(0, (int)FreeboxDownloadPriority.Last, (int)FreeboxDownloadPriority.Last, false)]
|
||||||
|
[TestCase(15, (int)FreeboxDownloadPriority.First, (int)FreeboxDownloadPriority.First, true)]
|
||||||
|
[TestCase(15, (int)FreeboxDownloadPriority.Last, (int)FreeboxDownloadPriority.First, false)]
|
||||||
|
[TestCase(15, (int)FreeboxDownloadPriority.First, (int)FreeboxDownloadPriority.Last, true)]
|
||||||
|
[TestCase(15, (int)FreeboxDownloadPriority.Last, (int)FreeboxDownloadPriority.Last, false)]
|
||||||
|
public void Download_should_queue_torrent_first_as_expected(int ageDay, int olderPriority, int recentPriority, bool toBeQueuedFirstFlag)
|
||||||
|
{
|
||||||
|
_settings.OlderPriority = olderPriority;
|
||||||
|
_settings.RecentPriority = recentPriority;
|
||||||
|
|
||||||
|
GivenDownloadConfiguration();
|
||||||
|
GivenSuccessfulDownload();
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
|
||||||
|
var episode = new Tv.Episode()
|
||||||
|
{
|
||||||
|
AirDateUtc = DateTime.UtcNow.Date.AddDays(-ageDay)
|
||||||
|
};
|
||||||
|
|
||||||
|
remoteEpisode.Episodes.Add(episode);
|
||||||
|
|
||||||
|
Subject.Download(remoteEpisode);
|
||||||
|
|
||||||
|
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||||
|
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), toBeQueuedFirstFlag, It.IsAny<double?>(), It.IsAny<FreeboxDownloadSettings>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(0, 0)]
|
||||||
|
[TestCase(1.5, 150)]
|
||||||
|
public void Download_should_define_seed_ratio_as_expected(double? providerSeedRatio, double? expectedSeedRatio)
|
||||||
|
{
|
||||||
|
GivenDownloadConfiguration();
|
||||||
|
GivenSuccessfulDownload();
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
|
||||||
|
remoteEpisode.SeedConfiguration = new TorrentSeedConfiguration();
|
||||||
|
remoteEpisode.SeedConfiguration.Ratio = providerSeedRatio;
|
||||||
|
|
||||||
|
Subject.Download(remoteEpisode);
|
||||||
|
|
||||||
|
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||||
|
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>(), expectedSeedRatio, It.IsAny<FreeboxDownloadSettings>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetItems_should_return_empty_list_if_no_tasks_available()
|
||||||
|
{
|
||||||
|
GivenTasks(new List<FreeboxDownloadTask>());
|
||||||
|
|
||||||
|
Subject.GetItems().Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetItems_should_return_ignore_tasks_of_unknown_type()
|
||||||
|
{
|
||||||
|
_task.Status = FreeboxDownloadTaskStatus.Done;
|
||||||
|
_task.Type = "toto";
|
||||||
|
|
||||||
|
GivenTasks(new List<FreeboxDownloadTask> { _task });
|
||||||
|
|
||||||
|
Subject.GetItems().Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetItems_when_destinationdirectory_is_set_should_ignore_downloads_in_wrong_folder()
|
||||||
|
{
|
||||||
|
_settings.DestinationDirectory = @"/some/path/that/will/not/match";
|
||||||
|
|
||||||
|
_task.Status = FreeboxDownloadTaskStatus.Done;
|
||||||
|
|
||||||
|
GivenTasks(new List<FreeboxDownloadTask> { _task });
|
||||||
|
|
||||||
|
Subject.GetItems().Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetItems_when_category_is_set_should_ignore_downloads_in_wrong_folder()
|
||||||
|
{
|
||||||
|
_settings.Category = "somecategory";
|
||||||
|
|
||||||
|
_task.Status = FreeboxDownloadTaskStatus.Done;
|
||||||
|
|
||||||
|
GivenTasks(new List<FreeboxDownloadTask> { _task });
|
||||||
|
|
||||||
|
Subject.GetItems().Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(FreeboxDownloadTaskStatus.Downloading, false, false)]
|
||||||
|
[TestCase(FreeboxDownloadTaskStatus.Done, true, true)]
|
||||||
|
[TestCase(FreeboxDownloadTaskStatus.Seeding, false, false)]
|
||||||
|
[TestCase(FreeboxDownloadTaskStatus.Stopped, false, false)]
|
||||||
|
public void GetItems_should_return_canBeMoved_and_canBeDeleted_as_expected(FreeboxDownloadTaskStatus apiStatus, bool canMoveFilesExpected, bool canBeRemovedExpected)
|
||||||
|
{
|
||||||
|
_task.Status = apiStatus;
|
||||||
|
|
||||||
|
GivenTasks(new List<FreeboxDownloadTask>() { _task });
|
||||||
|
|
||||||
|
var items = Subject.GetItems();
|
||||||
|
|
||||||
|
items.Should().HaveCount(1);
|
||||||
|
items.First().CanBeRemoved.Should().Be(canBeRemovedExpected);
|
||||||
|
items.First().CanMoveFiles.Should().Be(canMoveFilesExpected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(FreeboxDownloadTaskStatus.Stopped, DownloadItemStatus.Paused)]
|
||||||
|
[TestCase(FreeboxDownloadTaskStatus.Stopping, DownloadItemStatus.Paused)]
|
||||||
|
[TestCase(FreeboxDownloadTaskStatus.Queued, DownloadItemStatus.Queued)]
|
||||||
|
[TestCase(FreeboxDownloadTaskStatus.Starting, DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase(FreeboxDownloadTaskStatus.Downloading, DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase(FreeboxDownloadTaskStatus.Retry, DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase(FreeboxDownloadTaskStatus.Checking, DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase(FreeboxDownloadTaskStatus.Error, DownloadItemStatus.Warning)]
|
||||||
|
[TestCase(FreeboxDownloadTaskStatus.Seeding, DownloadItemStatus.Completed)]
|
||||||
|
[TestCase(FreeboxDownloadTaskStatus.Done, DownloadItemStatus.Completed)]
|
||||||
|
[TestCase(FreeboxDownloadTaskStatus.Unknown, DownloadItemStatus.Downloading)]
|
||||||
|
public void GetItems_should_return_item_as_downloadItemStatus(FreeboxDownloadTaskStatus apiStatus, DownloadItemStatus expectedItemStatus)
|
||||||
|
{
|
||||||
|
_task.Status = apiStatus;
|
||||||
|
|
||||||
|
GivenTasks(new List<FreeboxDownloadTask>() { _task });
|
||||||
|
|
||||||
|
var items = Subject.GetItems();
|
||||||
|
|
||||||
|
items.Should().HaveCount(1);
|
||||||
|
items.First().Status.Should().Be(expectedItemStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetItems_should_return_decoded_destination_directory()
|
||||||
|
{
|
||||||
|
var decodedDownloadDirectory = "/that/the/path";
|
||||||
|
|
||||||
|
_task.Status = FreeboxDownloadTaskStatus.Done;
|
||||||
|
_task.DownloadDirectory = "L3RoYXQvdGhlL3BhdGg=";
|
||||||
|
|
||||||
|
GivenTasks(new List<FreeboxDownloadTask> { _task });
|
||||||
|
|
||||||
|
var items = Subject.GetItems();
|
||||||
|
|
||||||
|
items.Should().HaveCount(1);
|
||||||
|
items.First().OutputPath.Should().Be(decodedDownloadDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetItems_should_return_message_if_tasks_in_error()
|
||||||
|
{
|
||||||
|
_task.Status = FreeboxDownloadTaskStatus.Error;
|
||||||
|
_task.Error = "internal";
|
||||||
|
|
||||||
|
GivenTasks(new List<FreeboxDownloadTask> { _task });
|
||||||
|
|
||||||
|
var items = Subject.GetItems();
|
||||||
|
|
||||||
|
items.Should().HaveCount(1);
|
||||||
|
items.First().Message.Should().Be("Internal error.");
|
||||||
|
items.First().Status.Should().Be(DownloadItemStatus.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||||
|
{
|
||||||
|
public static class EncodingForBase64
|
||||||
|
{
|
||||||
|
public static string EncodeBase64(this string text)
|
||||||
|
{
|
||||||
|
if (text == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] textAsBytes = System.Text.Encoding.UTF8.GetBytes(text);
|
||||||
|
return System.Convert.ToBase64String(textAsBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string DecodeBase64(this string encodedText)
|
||||||
|
{
|
||||||
|
if (encodedText == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] textAsBytes = System.Convert.FromBase64String(encodedText);
|
||||||
|
return System.Text.Encoding.UTF8.GetString(textAsBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||||
|
{
|
||||||
|
public class FreeboxDownloadException : DownloadClientException
|
||||||
|
{
|
||||||
|
public FreeboxDownloadException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||||
|
{
|
||||||
|
public enum FreeboxDownloadPriority
|
||||||
|
{
|
||||||
|
Last = 0,
|
||||||
|
First = 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,277 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Cache;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||||
|
{
|
||||||
|
public interface IFreeboxDownloadProxy
|
||||||
|
{
|
||||||
|
void Authenticate(FreeboxDownloadSettings settings);
|
||||||
|
string AddTaskFromUrl(string url, string directory, bool addPaused, bool addFirst, double? seedRatio, FreeboxDownloadSettings settings);
|
||||||
|
string AddTaskFromFile(string fileName, byte[] fileContent, string directory, bool addPaused, bool addFirst, double? seedRatio, FreeboxDownloadSettings settings);
|
||||||
|
void DeleteTask(string id, bool deleteData, FreeboxDownloadSettings settings);
|
||||||
|
FreeboxDownloadConfiguration GetDownloadConfiguration(FreeboxDownloadSettings settings);
|
||||||
|
List<FreeboxDownloadTask> GetTasks(FreeboxDownloadSettings settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FreeboxDownloadProxy : IFreeboxDownloadProxy
|
||||||
|
{
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
private ICached<string> _authSessionTokenCache;
|
||||||
|
|
||||||
|
public FreeboxDownloadProxy(ICacheManager cacheManager, IHttpClient httpClient, Logger logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_logger = logger;
|
||||||
|
_authSessionTokenCache = cacheManager.GetCache<string>(GetType(), "authSessionToken");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Authenticate(FreeboxDownloadSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/login").Build();
|
||||||
|
|
||||||
|
var response = ProcessRequest<FreeboxLogin>(request, settings);
|
||||||
|
|
||||||
|
if (response.Result.LoggedIn == false)
|
||||||
|
{
|
||||||
|
throw new DownloadClientAuthenticationException("Not logged");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string AddTaskFromUrl(string url, string directory, bool addPaused, bool addFirst, double? seedRatio, FreeboxDownloadSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/downloads/add").Post();
|
||||||
|
request.Headers.ContentType = "application/x-www-form-urlencoded";
|
||||||
|
|
||||||
|
request.AddFormParameter("download_url", System.Web.HttpUtility.UrlPathEncode(url));
|
||||||
|
|
||||||
|
if (!directory.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
request.AddFormParameter("download_dir", directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = ProcessRequest<FreeboxDownloadTask>(request.Build(), settings);
|
||||||
|
|
||||||
|
SetTorrentSettings(response.Result.Id, addPaused, addFirst, seedRatio, settings);
|
||||||
|
|
||||||
|
return response.Result.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string AddTaskFromFile(string fileName, byte[] fileContent, string directory, bool addPaused, bool addFirst, double? seedRatio, FreeboxDownloadSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/downloads/add").Post();
|
||||||
|
|
||||||
|
request.AddFormUpload("download_file", fileName, fileContent, "multipart/form-data");
|
||||||
|
|
||||||
|
if (directory.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
request.AddFormParameter("download_dir", directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = ProcessRequest<FreeboxDownloadTask>(request.Build(), settings);
|
||||||
|
|
||||||
|
SetTorrentSettings(response.Result.Id, addPaused, addFirst, seedRatio, settings);
|
||||||
|
|
||||||
|
return response.Result.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteTask(string id, bool deleteData, FreeboxDownloadSettings settings)
|
||||||
|
{
|
||||||
|
var uri = "/downloads/" + id;
|
||||||
|
|
||||||
|
if (deleteData == true)
|
||||||
|
{
|
||||||
|
uri += "/erase";
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = BuildRequest(settings).Resource(uri).Build();
|
||||||
|
|
||||||
|
request.Method = HttpMethod.Delete;
|
||||||
|
|
||||||
|
ProcessRequest<string>(request, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FreeboxDownloadConfiguration GetDownloadConfiguration(FreeboxDownloadSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/downloads/config/").Build();
|
||||||
|
|
||||||
|
return ProcessRequest<FreeboxDownloadConfiguration>(request, settings).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FreeboxDownloadTask> GetTasks(FreeboxDownloadSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/downloads/").Build();
|
||||||
|
|
||||||
|
return ProcessRequest<List<FreeboxDownloadTask>>(request, settings).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildCachedHeaderKey(FreeboxDownloadSettings settings)
|
||||||
|
{
|
||||||
|
return $"{settings.Host}:{settings.AppId}:{settings.AppToken}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetTorrentSettings(string id, bool addPaused, bool addFirst, double? seedRatio, FreeboxDownloadSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/downloads/" + id).Build();
|
||||||
|
|
||||||
|
request.Method = HttpMethod.Put;
|
||||||
|
|
||||||
|
var body = new Dictionary<string, object> { };
|
||||||
|
|
||||||
|
if (addPaused)
|
||||||
|
{
|
||||||
|
body.Add("status", FreeboxDownloadTaskStatus.Stopped.ToString().ToLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addFirst)
|
||||||
|
{
|
||||||
|
body.Add("queue_pos", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seedRatio != null)
|
||||||
|
{
|
||||||
|
// 0 means unlimited seeding
|
||||||
|
body.Add("stop_ratio", seedRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
request.SetContent(body.ToJson());
|
||||||
|
|
||||||
|
ProcessRequest<FreeboxDownloadTask>(request, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetSessionToken(HttpRequestBuilder requestBuilder, FreeboxDownloadSettings settings, bool force = false)
|
||||||
|
{
|
||||||
|
var sessionToken = _authSessionTokenCache.Find(BuildCachedHeaderKey(settings));
|
||||||
|
|
||||||
|
if (sessionToken == null || force)
|
||||||
|
{
|
||||||
|
_authSessionTokenCache.Remove(BuildCachedHeaderKey(settings));
|
||||||
|
|
||||||
|
_logger.Debug($"Client needs a new Session Token to reach the API with App ID '{settings.AppId}'");
|
||||||
|
|
||||||
|
// Obtaining a Session Token (from official documentation):
|
||||||
|
// To protect the app_token secret, it will never be used directly to authenticate the
|
||||||
|
// application, instead the API will provide a challenge the app will combine to its
|
||||||
|
// app_token to open a session and get a session_token.
|
||||||
|
// The validity of the session_token is limited in time and the app will have to renew
|
||||||
|
// this session_token once in a while.
|
||||||
|
|
||||||
|
// Retrieving the 'challenge' value (it changes frequently and have a limited time validity)
|
||||||
|
// needed to build password
|
||||||
|
var challengeRequest = requestBuilder.Resource("/login").Build();
|
||||||
|
challengeRequest.Method = HttpMethod.Get;
|
||||||
|
|
||||||
|
var challenge = ProcessRequest<FreeboxLogin>(challengeRequest, settings).Result.Challenge;
|
||||||
|
|
||||||
|
// The password is computed using the 'challenge' value and the 'app_token' ('App Token' setting)
|
||||||
|
var enc = System.Text.Encoding.ASCII;
|
||||||
|
var hmac = new HMACSHA1(enc.GetBytes(settings.AppToken));
|
||||||
|
hmac.Initialize();
|
||||||
|
var buffer = enc.GetBytes(challenge);
|
||||||
|
var password = System.BitConverter.ToString(hmac.ComputeHash(buffer)).Replace("-", "").ToLower();
|
||||||
|
|
||||||
|
// Both 'app_id' ('App ID' setting) and computed password are set to get a Session Token
|
||||||
|
var sessionRequest = requestBuilder.Resource("/login/session").Post().Build();
|
||||||
|
var body = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "app_id", settings.AppId },
|
||||||
|
{ "password", password }
|
||||||
|
};
|
||||||
|
sessionRequest.SetContent(body.ToJson());
|
||||||
|
|
||||||
|
sessionToken = ProcessRequest<FreeboxLogin>(sessionRequest, settings).Result.SessionToken;
|
||||||
|
|
||||||
|
_authSessionTokenCache.Set(BuildCachedHeaderKey(settings), sessionToken);
|
||||||
|
|
||||||
|
_logger.Debug($"New Session Token stored in cache for App ID '{settings.AppId}', ready to reach API");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpRequestBuilder BuildRequest(FreeboxDownloadSettings settings, bool authentication = true)
|
||||||
|
{
|
||||||
|
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.ApiUrl)
|
||||||
|
{
|
||||||
|
LogResponseContent = true
|
||||||
|
};
|
||||||
|
|
||||||
|
requestBuilder.Headers.ContentType = "application/json";
|
||||||
|
|
||||||
|
if (authentication == true)
|
||||||
|
{
|
||||||
|
requestBuilder.SetHeader("X-Fbx-App-Auth", GetSessionToken(requestBuilder, settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FreeboxResponse<T> ProcessRequest<T>(HttpRequest request, FreeboxDownloadSettings settings)
|
||||||
|
{
|
||||||
|
request.LogResponseContent = true;
|
||||||
|
request.SuppressHttpError = true;
|
||||||
|
|
||||||
|
HttpResponse response;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = _httpClient.Execute(request);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
throw new DownloadClientUnavailableException($"Unable to reach Freebox API. Verify 'Host', 'Port' or 'Use SSL' settings. (Error: {ex.Message})", ex);
|
||||||
|
}
|
||||||
|
catch (WebException ex)
|
||||||
|
{
|
||||||
|
throw new DownloadClientUnavailableException("Unable to connect to Freebox API, please check your settings", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.StatusCode == HttpStatusCode.Forbidden || response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
_authSessionTokenCache.Remove(BuildCachedHeaderKey(settings));
|
||||||
|
|
||||||
|
var responseContent = Json.Deserialize<FreeboxResponse<FreeboxLogin>>(response.Content);
|
||||||
|
|
||||||
|
var msg = $"Authentication to Freebox API failed. Reason: {responseContent.GetErrorDescription()}";
|
||||||
|
_logger.Error(msg);
|
||||||
|
throw new DownloadClientAuthenticationException(msg);
|
||||||
|
}
|
||||||
|
else if (response.StatusCode == HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
throw new FreeboxDownloadException("Unable to reach Freebox API. Verify 'API URL' setting for base URL and version.");
|
||||||
|
}
|
||||||
|
else if (response.StatusCode == HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
var responseContent = Json.Deserialize<FreeboxResponse<T>>(response.Content);
|
||||||
|
|
||||||
|
if (responseContent.Success)
|
||||||
|
{
|
||||||
|
return responseContent;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var msg = $"Freebox API returned error: {responseContent.GetErrorDescription()}";
|
||||||
|
_logger.Error(msg);
|
||||||
|
throw new DownloadClientException(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("Unable to connect to Freebox, please check your settings.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using FluentValidation;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Annotations;
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
using NzbDrone.Core.Validation.Paths;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||||
|
{
|
||||||
|
public class FreeboxDownloadSettingsValidator : AbstractValidator<FreeboxDownloadSettings>
|
||||||
|
{
|
||||||
|
public FreeboxDownloadSettingsValidator()
|
||||||
|
{
|
||||||
|
RuleFor(c => c.Host).ValidHost();
|
||||||
|
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
|
||||||
|
RuleFor(c => c.ApiUrl).NotEmpty()
|
||||||
|
.WithMessage("'API URL' must not be empty.");
|
||||||
|
RuleFor(c => c.ApiUrl).ValidUrlBase();
|
||||||
|
RuleFor(c => c.AppId).NotEmpty()
|
||||||
|
.WithMessage("'App ID' must not be empty.");
|
||||||
|
RuleFor(c => c.AppToken).NotEmpty()
|
||||||
|
.WithMessage("'App Token' must not be empty.");
|
||||||
|
RuleFor(c => c.Category).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase)
|
||||||
|
.WithMessage("Allowed characters a-z and -");
|
||||||
|
RuleFor(c => c.DestinationDirectory).IsValidPath()
|
||||||
|
.When(c => c.DestinationDirectory.IsNotNullOrWhiteSpace());
|
||||||
|
RuleFor(c => c.DestinationDirectory).Empty()
|
||||||
|
.When(c => c.Category.IsNotNullOrWhiteSpace())
|
||||||
|
.WithMessage("Cannot use 'Category' and 'Destination Directory' at the same time.");
|
||||||
|
RuleFor(c => c.Category).Empty()
|
||||||
|
.When(c => c.DestinationDirectory.IsNotNullOrWhiteSpace())
|
||||||
|
.WithMessage("Cannot use 'Category' and 'Destination Directory' at the same time.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FreeboxDownloadSettings : IProviderConfig
|
||||||
|
{
|
||||||
|
private static readonly FreeboxDownloadSettingsValidator Validator = new FreeboxDownloadSettingsValidator();
|
||||||
|
|
||||||
|
public FreeboxDownloadSettings()
|
||||||
|
{
|
||||||
|
Host = "mafreebox.freebox.fr";
|
||||||
|
Port = 443;
|
||||||
|
UseSsl = true;
|
||||||
|
ApiUrl = "/api/v1/";
|
||||||
|
}
|
||||||
|
|
||||||
|
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox, HelpText = "Hostname or host IP address of the Freebox, defaults to 'mafreebox.freebox.fr' (will only work if on same network)")]
|
||||||
|
public string Host { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox, HelpText = "Port used to access Freebox interface, defaults to '443'")]
|
||||||
|
public int Port { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secured connection when connecting to Freebox API")]
|
||||||
|
public bool UseSsl { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(3, Label = "API URL", Type = FieldType.Textbox, Advanced = true, HelpText = "Define Freebox API base URL with API version, eg http://[host]:[port]/[api_base_url]/[api_version]/, defaults to '/api/v1/'")]
|
||||||
|
public string ApiUrl { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(4, Label = "App ID", Type = FieldType.Textbox, HelpText = "App ID given when creating access to Freebox API (ie 'app_id')")]
|
||||||
|
public string AppId { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(5, Label = "App Token", Type = FieldType.Password, Privacy = PrivacyLevel.Password, HelpText = "App token retrieved when creating access to Freebox API (ie 'app_token')")]
|
||||||
|
public string AppToken { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(6, Label = "Destination Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default Freebox download location")]
|
||||||
|
public string DestinationDirectory { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(7, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated non-Sonarr downloads (will create a [category] subdirectory in the output directory)")]
|
||||||
|
public string Category { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(8, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(FreeboxDownloadPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
|
||||||
|
public int RecentPriority { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(FreeboxDownloadPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
|
||||||
|
public int OlderPriority { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(10, Label = "Add Paused", Type = FieldType.Checkbox)]
|
||||||
|
public bool AddPaused { get; set; }
|
||||||
|
|
||||||
|
public NzbDroneValidationResult Validate()
|
||||||
|
{
|
||||||
|
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.FreeboxDownload.Responses
|
||||||
|
{
|
||||||
|
public class FreeboxDownloadConfiguration
|
||||||
|
{
|
||||||
|
[JsonProperty(PropertyName = "download_dir")]
|
||||||
|
public string DownloadDirectory { get; set; }
|
||||||
|
public string DecodedDownloadDirectory
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return DownloadDirectory.DecodeBase64();
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
DownloadDirectory = value.EncodeBase64();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.FreeboxDownload.Responses
|
||||||
|
{
|
||||||
|
public enum FreeboxDownloadTaskType
|
||||||
|
{
|
||||||
|
Bt,
|
||||||
|
Nzb,
|
||||||
|
Http,
|
||||||
|
Ftp
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FreeboxDownloadTaskStatus
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
Stopped,
|
||||||
|
Queued,
|
||||||
|
Starting,
|
||||||
|
Downloading,
|
||||||
|
Stopping,
|
||||||
|
Error,
|
||||||
|
Done,
|
||||||
|
Checking,
|
||||||
|
Repairing,
|
||||||
|
Extracting,
|
||||||
|
Seeding,
|
||||||
|
Retry
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FreeboxDownloadTaskIoPriority
|
||||||
|
{
|
||||||
|
Low,
|
||||||
|
Normal,
|
||||||
|
High
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FreeboxDownloadTask
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, string> Descriptions;
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "download_dir")]
|
||||||
|
public string DownloadDirectory { get; set; }
|
||||||
|
public string DecodedDownloadDirectory
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return DownloadDirectory.DecodeBase64();
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
DownloadDirectory = value.EncodeBase64();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "info_hash")]
|
||||||
|
public string InfoHash { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "queue_pos")]
|
||||||
|
public int QueuePosition { get; set; }
|
||||||
|
[JsonConverter(typeof(UnderscoreStringEnumConverter), FreeboxDownloadTaskStatus.Unknown)]
|
||||||
|
public FreeboxDownloadTaskStatus Status { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "eta")]
|
||||||
|
public long Eta { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "error")]
|
||||||
|
public string Error { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "io_priority")]
|
||||||
|
public string IoPriority { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "stop_ratio")]
|
||||||
|
public long StopRatio { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "piece_length")]
|
||||||
|
public long PieceLength { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "created_ts")]
|
||||||
|
public long CreatedTimestamp { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "size")]
|
||||||
|
public long Size { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "rx_pct")]
|
||||||
|
public long ReceivedPrct { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "rx_bytes")]
|
||||||
|
public long ReceivedBytes { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "rx_rate")]
|
||||||
|
public long ReceivedRate { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "tx_pct")]
|
||||||
|
public long TransmittedPrct { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "tx_bytes")]
|
||||||
|
public long TransmittedBytes { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "tx_rate")]
|
||||||
|
public long TransmittedRate { get; set; }
|
||||||
|
|
||||||
|
static FreeboxDownloadTask()
|
||||||
|
{
|
||||||
|
Descriptions = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "internal", "Internal error." },
|
||||||
|
{ "disk_full", "The disk is full." },
|
||||||
|
{ "unknown", "Unknown error." },
|
||||||
|
{ "parse_error", "Parse error." },
|
||||||
|
{ "unknown_host", "Unknown host." },
|
||||||
|
{ "timeout", "Timeout." },
|
||||||
|
{ "bad_authentication", "Invalid credentials." },
|
||||||
|
{ "connection_refused", "Remote host refused connection." },
|
||||||
|
{ "bt_tracker_error", "Unable to announce on tracker." },
|
||||||
|
{ "bt_missing_files", "Missing torrent files." },
|
||||||
|
{ "bt_file_error", "Error accessing torrent files." },
|
||||||
|
{ "missing_ctx_file", "Error accessing task context file." },
|
||||||
|
{ "nzb_no_group", "Cannot find the requested group on server." },
|
||||||
|
{ "nzb_not_found", "Article not fount on the server." },
|
||||||
|
{ "nzb_invalid_crc", "Invalid article CRC." },
|
||||||
|
{ "nzb_invalid_size", "Invalid article size." },
|
||||||
|
{ "nzb_invalid_filename", "Invalid filename." },
|
||||||
|
{ "nzb_open_failed", "Error opening." },
|
||||||
|
{ "nzb_write_failed", "Error writing." },
|
||||||
|
{ "nzb_missing_size", "Missing article size." },
|
||||||
|
{ "nzb_decode_error", "Article decoding error." },
|
||||||
|
{ "nzb_missing_segments", "Missing article segments." },
|
||||||
|
{ "nzb_error", "Other nzb error." },
|
||||||
|
{ "nzb_authentication_required", "Nzb server need authentication." }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetErrorDescription()
|
||||||
|
{
|
||||||
|
if (Descriptions.ContainsKey(Error))
|
||||||
|
{
|
||||||
|
return Descriptions[Error];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{Error} - Unknown error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.FreeboxDownload.Responses
|
||||||
|
{
|
||||||
|
public class FreeboxLogin
|
||||||
|
{
|
||||||
|
[JsonProperty(PropertyName = "logged_in")]
|
||||||
|
public bool LoggedIn { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "challenge")]
|
||||||
|
public string Challenge { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "password_salt")]
|
||||||
|
public string PasswordSalt { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "password_set")]
|
||||||
|
public bool PasswordSet { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "session_token")]
|
||||||
|
public string SessionToken { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.FreeboxDownload.Responses
|
||||||
|
{
|
||||||
|
public class FreeboxResponse<T>
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, string> Descriptions;
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "success")]
|
||||||
|
public bool Success { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "msg")]
|
||||||
|
public string Message { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "error_code")]
|
||||||
|
public string ErrorCode { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "result")]
|
||||||
|
public T Result { get; set; }
|
||||||
|
|
||||||
|
static FreeboxResponse()
|
||||||
|
{
|
||||||
|
Descriptions = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
// Common errors
|
||||||
|
{ "invalid_request", "Your request is invalid." },
|
||||||
|
{ "invalid_api_version", "Invalid API base url or unknown API version." },
|
||||||
|
{ "internal_error", "Internal error." },
|
||||||
|
|
||||||
|
// Login API errors
|
||||||
|
{ "auth_required", "Invalid session token, or no session token sent." },
|
||||||
|
{ "invalid_token", "The app token you are trying to use is invalid or has been revoked." },
|
||||||
|
{ "pending_token", "The app token you are trying to use has not been validated by user yet." },
|
||||||
|
{ "insufficient_rights", "Your app permissions does not allow accessing this API." },
|
||||||
|
{ "denied_from_external_ip", "You are trying to get an app_token from a remote IP." },
|
||||||
|
{ "ratelimited", "Too many auth error have been made from your IP." },
|
||||||
|
{ "new_apps_denied", "New application token request has been disabled." },
|
||||||
|
{ "apps_denied", "API access from apps has been disabled." },
|
||||||
|
|
||||||
|
// Download API errors
|
||||||
|
{ "task_not_found", "No task was found with the given id." },
|
||||||
|
{ "invalid_operation", "Attempt to perform an invalid operation." },
|
||||||
|
{ "invalid_file", "Error with the download file (invalid format ?)." },
|
||||||
|
{ "invalid_url", "URL is invalid." },
|
||||||
|
{ "not_implemented", "Method not implemented." },
|
||||||
|
{ "out_of_memory", "No more memory available to perform the requested action." },
|
||||||
|
{ "invalid_task_type", "The task type is invalid." },
|
||||||
|
{ "hibernating", "The downloader is hibernating." },
|
||||||
|
{ "need_bt_stopped_done", "This action is only valid for Bittorrent task in stopped or done state." },
|
||||||
|
{ "bt_tracker_not_found", "Attempt to access an invalid tracker object." },
|
||||||
|
{ "too_many_tasks", "Too many tasks." },
|
||||||
|
{ "invalid_address", "Invalid peer address." },
|
||||||
|
{ "port_conflict", "Port conflict when setting config." },
|
||||||
|
{ "invalid_priority", "Invalid priority." },
|
||||||
|
{ "ctx_file_error", "Failed to initialize task context file (need to check disk)." },
|
||||||
|
{ "exists", "Same task already exists." },
|
||||||
|
{ "port_outside_range", "Incoming port is not available for this customer." }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetErrorDescription()
|
||||||
|
{
|
||||||
|
if (Descriptions.ContainsKey(ErrorCode))
|
||||||
|
{
|
||||||
|
return Descriptions[ErrorCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{ErrorCode} - Unknown error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,227 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentValidation.Results;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses;
|
||||||
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||||
|
{
|
||||||
|
public class TorrentFreeboxDownload : TorrentClientBase<FreeboxDownloadSettings>
|
||||||
|
{
|
||||||
|
private readonly IFreeboxDownloadProxy _proxy;
|
||||||
|
|
||||||
|
public TorrentFreeboxDownload(IFreeboxDownloadProxy proxy,
|
||||||
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
|
IHttpClient httpClient,
|
||||||
|
IConfigService configService,
|
||||||
|
IDiskProvider diskProvider,
|
||||||
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
Logger logger)
|
||||||
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
||||||
|
{
|
||||||
|
_proxy = proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Name => "Freebox Download";
|
||||||
|
|
||||||
|
protected IEnumerable<FreeboxDownloadTask> GetTorrents()
|
||||||
|
{
|
||||||
|
return _proxy.GetTasks(Settings).Where(v => v.Type.ToLower() == FreeboxDownloadTaskType.Bt.ToString().ToLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<DownloadClientItem> GetItems()
|
||||||
|
{
|
||||||
|
var torrents = GetTorrents();
|
||||||
|
|
||||||
|
var queueItems = new List<DownloadClientItem>();
|
||||||
|
|
||||||
|
foreach (var torrent in torrents)
|
||||||
|
{
|
||||||
|
var outputPath = new OsPath(torrent.DecodedDownloadDirectory);
|
||||||
|
|
||||||
|
if (Settings.DestinationDirectory.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
if (!new OsPath(Settings.DestinationDirectory).Contains(outputPath))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.Category.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
var directories = outputPath.FullPath.Split('\\', '/');
|
||||||
|
|
||||||
|
if (!directories.Contains(Settings.Category))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = new DownloadClientItem()
|
||||||
|
{
|
||||||
|
DownloadId = torrent.Id,
|
||||||
|
Category = Settings.Category,
|
||||||
|
Title = torrent.Name,
|
||||||
|
TotalSize = torrent.Size,
|
||||||
|
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
|
||||||
|
RemainingSize = (long)(torrent.Size * (double)(1 - ((double)torrent.ReceivedPrct / 10000))),
|
||||||
|
RemainingTime = torrent.Eta <= 0 ? null : TimeSpan.FromSeconds(torrent.Eta),
|
||||||
|
SeedRatio = torrent.StopRatio <= 0 ? 0 : torrent.StopRatio / 100,
|
||||||
|
OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, outputPath)
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (torrent.Status)
|
||||||
|
{
|
||||||
|
case FreeboxDownloadTaskStatus.Stopped: // task is stopped, can be resumed by setting the status to downloading
|
||||||
|
case FreeboxDownloadTaskStatus.Stopping: // task is gracefully stopping
|
||||||
|
item.Status = DownloadItemStatus.Paused;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FreeboxDownloadTaskStatus.Queued: // task will start when a new download slot is available the queue position is stored in queue_pos attribute
|
||||||
|
item.Status = DownloadItemStatus.Queued;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FreeboxDownloadTaskStatus.Starting: // task is preparing to start download
|
||||||
|
case FreeboxDownloadTaskStatus.Downloading:
|
||||||
|
case FreeboxDownloadTaskStatus.Retry: // you can set a task status to ‘retry’ to restart the download task.
|
||||||
|
case FreeboxDownloadTaskStatus.Checking: // checking data before lauching download.
|
||||||
|
item.Status = DownloadItemStatus.Downloading;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FreeboxDownloadTaskStatus.Error: // there was a problem with the download, you can get an error code in the error field
|
||||||
|
item.Status = DownloadItemStatus.Warning;
|
||||||
|
item.Message = torrent.GetErrorDescription();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FreeboxDownloadTaskStatus.Done: // the download is over. For bt you can resume seeding setting the status to seeding if the ratio is not reached yet
|
||||||
|
case FreeboxDownloadTaskStatus.Seeding: // download is over, the content is Change to being shared to other users. The task will automatically stop once the seed ratio has been reached
|
||||||
|
item.Status = DownloadItemStatus.Completed;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FreeboxDownloadTaskStatus.Unknown:
|
||||||
|
default: // new status in API? default to downloading
|
||||||
|
item.Message = "Unknown download state: " + torrent.Status;
|
||||||
|
_logger.Info(item.Message);
|
||||||
|
item.Status = DownloadItemStatus.Downloading;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.CanBeRemoved = item.CanMoveFiles = torrent.Status == FreeboxDownloadTaskStatus.Done;
|
||||||
|
|
||||||
|
queueItems.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return queueItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
|
||||||
|
{
|
||||||
|
return _proxy.AddTaskFromUrl(magnetLink,
|
||||||
|
GetDownloadDirectory().EncodeBase64(),
|
||||||
|
ToBePaused(),
|
||||||
|
ToBeQueuedFirst(remoteEpisode),
|
||||||
|
GetSeedRatio(remoteEpisode),
|
||||||
|
Settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent)
|
||||||
|
{
|
||||||
|
return _proxy.AddTaskFromFile(filename,
|
||||||
|
fileContent,
|
||||||
|
GetDownloadDirectory().EncodeBase64(),
|
||||||
|
ToBePaused(),
|
||||||
|
ToBeQueuedFirst(remoteEpisode),
|
||||||
|
GetSeedRatio(remoteEpisode),
|
||||||
|
Settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void RemoveItem(DownloadClientItem item, bool deleteData)
|
||||||
|
{
|
||||||
|
_proxy.DeleteTask(item.DownloadId, deleteData, Settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override DownloadClientInfo GetStatus()
|
||||||
|
{
|
||||||
|
var destDir = GetDownloadDirectory();
|
||||||
|
|
||||||
|
return new DownloadClientInfo
|
||||||
|
{
|
||||||
|
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "::1" || Settings.Host == "localhost",
|
||||||
|
OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(destDir)) }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_proxy.Authenticate(Settings);
|
||||||
|
}
|
||||||
|
catch (DownloadClientUnavailableException ex)
|
||||||
|
{
|
||||||
|
failures.Add(new ValidationFailure("Host", ex.Message));
|
||||||
|
failures.Add(new ValidationFailure("Port", ex.Message));
|
||||||
|
}
|
||||||
|
catch (DownloadClientAuthenticationException ex)
|
||||||
|
{
|
||||||
|
failures.Add(new ValidationFailure("AppId", ex.Message));
|
||||||
|
failures.Add(new ValidationFailure("AppToken", ex.Message));
|
||||||
|
}
|
||||||
|
catch (FreeboxDownloadException ex)
|
||||||
|
{
|
||||||
|
failures.Add(new ValidationFailure("ApiUrl", ex.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetDownloadDirectory()
|
||||||
|
{
|
||||||
|
if (Settings.DestinationDirectory.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
return Settings.DestinationDirectory.TrimEnd('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
var destDir = _proxy.GetDownloadConfiguration(Settings).DecodedDownloadDirectory.TrimEnd('/');
|
||||||
|
|
||||||
|
if (Settings.Category.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
destDir = $"{destDir}/{Settings.Category}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return destDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ToBePaused()
|
||||||
|
{
|
||||||
|
return Settings.AddPaused;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ToBeQueuedFirst(RemoteEpisode remoteEpisode)
|
||||||
|
{
|
||||||
|
if ((remoteEpisode.IsRecentEpisode() && Settings.RecentPriority == (int)FreeboxDownloadPriority.First) ||
|
||||||
|
(!remoteEpisode.IsRecentEpisode() && Settings.OlderPriority == (int)FreeboxDownloadPriority.First))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double? GetSeedRatio(RemoteEpisode remoteEpisode)
|
||||||
|
{
|
||||||
|
if (remoteEpisode.SeedConfiguration == null || remoteEpisode.SeedConfiguration.Ratio == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return remoteEpisode.SeedConfiguration.Ratio.Value * 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue