diff --git a/README.md b/README.md index 54a6dc3..b432cfc 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,29 @@ Replace the paths in `volumes` as in the above step. 1. Same goes for `Mapped path`, but this is the destination path from your docker mapping. This is a path on your host. 1. Save your settings. +### Download Clients + +Currently there 2 available download clients: + +#### Simple Downloader + +This is a simple 1 connection only download manager. It uses less resources than the multi-part downloader. It downloads straight to the download path. + +It has the following options: + +- Maximum parallel downloads: This number indicates how many completed torrents from Real Debrid can be downloaded at the same time. On low powered systems it is recommended to keep this number low. + +#### Multi Part Downloader + +This [downloader](https://github.com/bezzad/Downloader) as more options and such uses more resources (memory, CPU) to download files. Recommended more powerful systems. + +It has the following options: + +- Temp Download path: Set this path to where the downloader temporarily stores chunks. This path can be an internal path in Docker (i.e. `/data/temp`) but make sure you have enough disk space to complete the whole download. When all chunks are completed the completed file is copied to your download folder. +- Maximum parallel downloads: This number indicates how many completed torrents from Real Debrid can be downloaded at the same time. +- Parallel connections per download: This number indicates how many threads/connections/parts/chunks it will use per download. This can increase speed, recommended is no more than 8. +- Download speed (in MB/s): This number indicates the speed in MB/s per download. If you set this to 10 and `Maximum parallel downloads` to 2, you can download with a maximum of 20MB/s. + ### Troubleshooting - If you forgot your logins simply delete the `rdtclient.db` and restart the service. diff --git a/client/src/app/navbar/settings/settings.component.html b/client/src/app/navbar/settings/settings.component.html index 42d6c90..33d26f1 100644 --- a/client/src/app/navbar/settings/settings.component.html +++ b/client/src/app/navbar/settings/settings.component.html @@ -38,7 +38,22 @@
- + +
+ +
+

+ Select which download client to use, see the + README for the various options. +

+
+ +
+
@@ -56,7 +71,7 @@

Maximum amount of torrents that get downloaded to your host at the same time.

-
+
@@ -67,7 +82,7 @@

-
+
diff --git a/client/src/app/navbar/settings/settings.component.ts b/client/src/app/navbar/settings/settings.component.ts index 42a8b52..dc79e1c 100644 --- a/client/src/app/navbar/settings/settings.component.ts +++ b/client/src/app/navbar/settings/settings.component.ts @@ -39,6 +39,7 @@ export class SettingsComponent implements OnInit { public settingDownloadPath: string; public settingMappedPath: string; public settingTempPath: string; + public settingDownloadClient: string; public settingDownloadLimit: number; public settingDownloadChunkCount: number; public settingDownloadMaxSpeed: number; @@ -59,6 +60,7 @@ export class SettingsComponent implements OnInit { this.settingDownloadPath = this.getSetting(results, 'DownloadPath'); this.settingMappedPath = this.getSetting(results, 'MappedPath'); this.settingTempPath = this.getSetting(results, 'TempPath'); + this.settingDownloadClient = this.getSetting(results, 'DownloadClient'); this.settingDownloadLimit = parseInt(this.getSetting(results, 'DownloadLimit'), 10); this.settingDownloadChunkCount = parseInt(this.getSetting(results, 'DownloadChunkCount'), 10); this.settingDownloadMaxSpeed = parseInt(this.getSetting(results, 'DownloadMaxSpeed'), 10); @@ -92,6 +94,10 @@ export class SettingsComponent implements OnInit { settingId: 'TempPath', value: this.settingTempPath, }, + { + settingId: 'DownloadClient', + value: this.settingDownloadClient, + }, { settingId: 'DownloadLimit', value: (this.settingDownloadLimit ?? 10).toString(), diff --git a/server/RdtClient.Data/Data/DataContext.cs b/server/RdtClient.Data/Data/DataContext.cs index eb98f52..254991b 100644 --- a/server/RdtClient.Data/Data/DataContext.cs +++ b/server/RdtClient.Data/Data/DataContext.cs @@ -57,6 +57,12 @@ namespace RdtClient.Data.Data #endif }, new Setting + { + SettingId = "DownloadClient", + Type = "String", + Value = @"Simple" + }, + new Setting { SettingId = "TempPath", Type = "String", diff --git a/server/RdtClient.Service/Services/DownloadClient.cs b/server/RdtClient.Service/Services/DownloadClient.cs index d3e9c63..e492071 100644 --- a/server/RdtClient.Service/Services/DownloadClient.cs +++ b/server/RdtClient.Service/Services/DownloadClient.cs @@ -1,11 +1,12 @@ using System; -using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; using Downloader; using RdtClient.Data.Models.Data; +using RdtClient.Service.Helpers; namespace RdtClient.Service.Services { @@ -17,6 +18,7 @@ namespace RdtClient.Service.Services private readonly Torrent _torrent; private DownloadService _downloader; + private SimpleDownloader _simpleDownloader; public DownloadClient(Download download, String destinationPath) { @@ -33,10 +35,7 @@ namespace RdtClient.Service.Services public Int64 BytesTotal { get; private set; } public Int64 BytesDone { get; private set; } - private Int64 LastTick { get; set; } - private ConcurrentBag AverageSpeed { get; } = new ConcurrentBag(); - - public async Task Start(Boolean onTheFlyDownload, String tempDirectory, Int32 chunkCount, Int64 maximumBytesPerSecond) + public async Task Start(IList settings) { BytesDone = 0; BytesTotal = 0; @@ -44,6 +43,8 @@ namespace RdtClient.Service.Services try { + var downloadClientSetting = settings.GetString("DownloadClient"); + var fileUrl = _download.Link; if (String.IsNullOrWhiteSpace(fileUrl)) @@ -69,7 +70,17 @@ namespace RdtClient.Service.Services await Task.Factory.StartNew(async delegate { - await Download(filePath, onTheFlyDownload, tempDirectory, chunkCount, maximumBytesPerSecond); + switch (downloadClientSetting) + { + case "Simple": + await DownloadSimple(uri, filePath); + break; + case "MultiPart": + await DownloadMultiPart(filePath, settings); + break; + default: + throw new Exception($"Unknown download client {downloadClientSetting}"); + } }); } catch (Exception ex) @@ -82,24 +93,82 @@ namespace RdtClient.Service.Services public void Cancel() { _downloader?.CancelAsync(); + _simpleDownloader?.Cancel(); } - private async Task Download(String filePath, Boolean onTheFlyDownload, String tempDirectory, Int32 chunkCount, Int64 maximumBytesPerSecond) + private async Task DownloadSimple(Uri uri, String filePath) { try { - LastTick = 0; + _simpleDownloader = new SimpleDownloader(); + _simpleDownloader.DownloadProgressChanged += (_, args) => + { + Speed = (Int64) args.BytesPerSecondSpeed; + BytesDone = args.ReceivedBytesSize; + BytesTotal = args.TotalBytesToReceive; + }; + + _simpleDownloader.DownloadFileCompleted += (_, args) => + { + if (args.Cancelled) + { + Error = $"The download was cancelled"; + } + else if (args.Error != null) + { + Error = args.Error.Message; + } + + Finished = true; + }; + + Speed = 0; + BytesDone = 0; + BytesTotal = 0; + + await _simpleDownloader.Download(uri, filePath); + } + catch (Exception ex) + { + Error = $"An unexpected error occurred downloading {_download.Link} for torrent {_torrent.RdName}: {ex.Message}"; + Finished = true; + } + } + + private async Task DownloadMultiPart(String filePath, IList settings) + { + try + { + var settingTempPath = settings.GetString("TempPath"); + if (String.IsNullOrWhiteSpace(settingTempPath)) + { + settingTempPath = Path.GetTempPath(); + } + + var settingDownloadChunkCount = settings.GetNumber("DownloadChunkCount"); + if (settingDownloadChunkCount <= 0) + { + settingDownloadChunkCount = 1; + } + + var settingDownloadMaxSpeed = settings.GetNumber("DownloadMaxSpeed"); + if (settingDownloadMaxSpeed <= 0) + { + settingDownloadMaxSpeed = 0; + } + settingDownloadMaxSpeed = settingDownloadMaxSpeed * 1024 * 1024; + var downloadOpt = new DownloadConfiguration { MaxTryAgainOnFailover = Int32.MaxValue, - ParallelDownload = chunkCount > 1, - ChunkCount = chunkCount, + ParallelDownload = settingDownloadChunkCount > 1, + ChunkCount = settingDownloadChunkCount, Timeout = 1000, - OnTheFlyDownload = onTheFlyDownload, + OnTheFlyDownload = false, BufferBlockSize = 1024 * 8, - MaximumBytesPerSecond = maximumBytesPerSecond, - TempDirectory = tempDirectory, + MaximumBytesPerSecond = settingDownloadMaxSpeed, + TempDirectory = settingTempPath, RequestConfiguration = { Accept = "*/*", @@ -115,31 +184,11 @@ namespace RdtClient.Service.Services _downloader.DownloadProgressChanged += (_, args) => { - var isElapsedTimeMoreThanOneSecond = Environment.TickCount - LastTick >= 1000; - - if (isElapsedTimeMoreThanOneSecond) - { - AverageSpeed.Add(args.BytesPerSecondSpeed); - LastTick = Environment.TickCount; - } - - if (AverageSpeed.Count > 0) - { - Speed = (Int64) AverageSpeed.Average(); - } - + Speed = (Int64) args.BytesPerSecondSpeed; BytesDone = args.ReceivedBytesSize; BytesTotal = args.TotalBytesToReceive; }; - - _downloader.DownloadStarted += (_, args) => - { - AverageSpeed?.Clear(); - Speed = 0; - BytesDone = 0; - BytesTotal = args.TotalBytesToReceive; - }; - + _downloader.DownloadFileCompleted += (_, args) => { if (args.Cancelled) diff --git a/server/RdtClient.Service/Services/Settings.cs b/server/RdtClient.Service/Services/Settings.cs index eed52ec..e77e95a 100644 --- a/server/RdtClient.Service/Services/Settings.cs +++ b/server/RdtClient.Service/Services/Settings.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -30,10 +29,7 @@ namespace RdtClient.Service.Services { _settingData = settingData; } - - private static Int64 LastTick { get; set; } - private static ConcurrentBag AverageSpeed { get; } = new ConcurrentBag(); - + public async Task> GetAll() { return await _settingData.GetAll(); @@ -91,7 +87,8 @@ namespace RdtClient.Service.Services public async Task TestDownloadSpeed(CancellationToken cancellationToken) { var downloadPath = await GetString("DownloadPath"); - var tempPath = await GetString("TempPath"); + + var settings = await GetAll(); var testFilePath = Path.Combine(downloadPath, "testDefault.rar"); @@ -111,7 +108,7 @@ namespace RdtClient.Service.Services var downloadClient = new DownloadClient(download, downloadPath); - await downloadClient.Start(false, tempPath, 8, 0); + await downloadClient.Start(settings); while (!downloadClient.Finished) { diff --git a/server/RdtClient.Service/Services/SimpleDownloader.cs b/server/RdtClient.Service/Services/SimpleDownloader.cs new file mode 100644 index 0000000..6d062c3 --- /dev/null +++ b/server/RdtClient.Service/Services/SimpleDownloader.cs @@ -0,0 +1,127 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using DownloadProgressChangedEventArgs = Downloader.DownloadProgressChangedEventArgs; + +namespace RdtClient.Service.Services +{ + public class SimpleDownloader + { + public Int64 Speed { get; private set; } + public Int64 BytesTotal { get; private set; } + public Int64 BytesDone { get; private set; } + + public event EventHandler DownloadFileCompleted; + public event EventHandler DownloadProgressChanged; + + private Boolean _cancelled = false; + + private Int64 _bytesLastUpdate; + private DateTime _nextUpdate; + + public async Task Download(Uri uri, String filePath) + { + try + { + _bytesLastUpdate = 0; + _nextUpdate = DateTime.UtcNow.AddSeconds(1); + + // Determine the file size + var webRequest = WebRequest.Create(uri); + webRequest.Method = "HEAD"; + webRequest.Timeout = 5000; + Int64 responseLength; + + using (var webResponse = await webRequest.GetResponseAsync()) + { + responseLength = Int64.Parse(webResponse.Headers.Get("Content-Length")); + } + + var timeout = DateTimeOffset.UtcNow.AddHours(1); + + while (timeout > DateTimeOffset.UtcNow && !_cancelled) + { + try + { + var request = WebRequest.Create(uri); + using var response = await request.GetResponseAsync(); + + await using var stream = response.GetResponseStream(); + + if (stream == null) + { + throw new IOException("No stream"); + } + + await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Write); + var buffer = new Byte[64 * 1024]; + + while (fileStream.Length < response.ContentLength && !_cancelled) + { + var read = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length)); + + if (read > 0) + { + fileStream.Write(buffer, 0, read); + + BytesDone = fileStream.Length; + BytesTotal = responseLength; + + if (DateTime.UtcNow > _nextUpdate) + { + Speed = fileStream.Length - _bytesLastUpdate; + + _nextUpdate = DateTime.UtcNow.AddSeconds(1); + _bytesLastUpdate = fileStream.Length; + + timeout = DateTimeOffset.UtcNow.AddHours(1); + + DownloadProgressChanged?.Invoke(this, new DownloadProgressChangedEventArgs(null) + { + BytesPerSecondSpeed = Speed, + ProgressedByteSize = _bytesLastUpdate, + TotalBytesToReceive = BytesTotal, + AverageBytesPerSecondSpeed = Speed, + ReceivedBytesSize = BytesDone, + }); + } + } + else + { + break; + } + } + + break; + } + catch (IOException) + { + await Task.Delay(1000); + } + catch (WebException) + { + await Task.Delay(1000); + } + } + + if (timeout <= DateTimeOffset.UtcNow) + { + throw new Exception($"Download timed out"); + } + + DownloadFileCompleted?.Invoke(this, new AsyncCompletedEventArgs(null, false, null)); + } + catch (Exception ex) + { + DownloadFileCompleted?.Invoke(this, new AsyncCompletedEventArgs(ex, false, null)); + } + } + + public void Cancel() + { + _cancelled = true; + } + } +} diff --git a/server/RdtClient.Service/Services/TorrentRunner.cs b/server/RdtClient.Service/Services/TorrentRunner.cs index 37b7d09..434541f 100644 --- a/server/RdtClient.Service/Services/TorrentRunner.cs +++ b/server/RdtClient.Service/Services/TorrentRunner.cs @@ -96,25 +96,6 @@ namespace RdtClient.Service.Services return; } - var settingTempPath = settings.GetString("TempPath"); - if (String.IsNullOrWhiteSpace(settingTempPath)) - { - settingTempPath = Path.GetTempPath(); - } - - var settingDownloadChunkCount = settings.GetNumber("DownloadChunkCount"); - if (settingDownloadChunkCount <= 0) - { - settingDownloadChunkCount = 1; - } - - var settingDownloadMaxSpeed = settings.GetNumber("DownloadMaxSpeed"); - if (settingDownloadMaxSpeed <= 0) - { - settingDownloadMaxSpeed = 0; - } - settingDownloadMaxSpeed = settingDownloadMaxSpeed * 1024 * 1024; - // Check if any torrents are finished downloading to the host, remove them from the active download list. var completedActiveDownloads = ActiveDownloadClients.Where(m => m.Value.Finished).ToList(); @@ -213,7 +194,7 @@ namespace RdtClient.Service.Services if (TorrentRunner.ActiveDownloadClients.TryAdd(download.DownloadId, downloadClient)) { - await downloadClient.Start(false, settingTempPath, settingDownloadChunkCount, settingDownloadMaxSpeed); + await downloadClient.Start(settings); } }