From 047326a30fe4c7e1c8685efaee305cd781a60f40 Mon Sep 17 00:00:00 2001
From: YvesPa <166829028+YvesPa@users.noreply.github.com>
Date: Mon, 23 Sep 2024 12:51:04 +0200
Subject: [PATCH] Add Synology Download Station
---
.../add-new-torrent.component.html | 1 +
server/RdtClient.Data/Enums/DownloadClient.cs | 3 +
.../Models/Internal/DbSettings.cs | 21 +-
.../RdtClient.Service.csproj | 1 +
.../Services/DownloadClient.cs | 1 +
.../Downloaders/DownloadStationDownloader.cs | 189 ++++++++++++++++++
.../Services/TorrentRunner.cs | 37 ++--
7 files changed, 238 insertions(+), 15 deletions(-)
create mode 100644 server/RdtClient.Service/Services/Downloaders/DownloadStationDownloader.cs
diff --git a/client/src/app/add-new-torrent/add-new-torrent.component.html b/client/src/app/add-new-torrent/add-new-torrent.component.html
index ed6a853..7d939e6 100644
--- a/client/src/app/add-new-torrent/add-new-torrent.component.html
+++ b/client/src/app/add-new-torrent/add-new-torrent.component.html
@@ -50,6 +50,7 @@
+
diff --git a/server/RdtClient.Data/Enums/DownloadClient.cs b/server/RdtClient.Data/Enums/DownloadClient.cs
index 9f43867..dd31e51 100644
--- a/server/RdtClient.Data/Enums/DownloadClient.cs
+++ b/server/RdtClient.Data/Enums/DownloadClient.cs
@@ -15,4 +15,7 @@ public enum DownloadClient
[Description("Symlink Downloader")]
Symlink,
+
+ [Description("Synology DownloadStation")]
+ DownloadStation,
}
\ No newline at end of file
diff --git a/server/RdtClient.Data/Models/Internal/DbSettings.cs b/server/RdtClient.Data/Models/Internal/DbSettings.cs
index 10381fd..0461d9a 100644
--- a/server/RdtClient.Data/Models/Internal/DbSettings.cs
+++ b/server/RdtClient.Data/Models/Internal/DbSettings.cs
@@ -1,5 +1,5 @@
-using System.ComponentModel;
-using RdtClient.Data.Enums;
+using RdtClient.Data.Enums;
+using System.ComponentModel;
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
@@ -123,7 +123,7 @@ http://127.0.0.1:6800/jsonrpc.")]
[DisplayName("Aria2c Secret (only used for the Aria2c Downloader)")]
[Description("The secret of your Aria2c instance. Optional.")]
public String Aria2cSecret { get; set; } = "mysecret123";
-
+
[DisplayName("Aria2c Download Path")]
[Description("The root path to download the file to on the Aria2c host, if empty use the Download path setting.")]
public String? Aria2cDownloadPath { get; set; } = null;
@@ -132,6 +132,21 @@ http://127.0.0.1:6800/jsonrpc.")]
[Description("Path where Rclone is mounted. Required for Symlink Downloader. Suffix this path with a * to search subdirectories too.")]
public String RcloneMountPath { get; set; } = "/mnt/rd/";
+ [DisplayName("Synology DownloadStation URL")]
+ [Description("The URL to the Synology DownloadStation. A common URL is http://127.0.0.1:5000")]
+ public String DownloadStationUrl { get; set; } = "http://127.0.0.1:5000";
+
+ [DisplayName("Synology DownloadStation Username")]
+ [Description("The username to use when connecting to the Synology DownloadStation.")]
+ public String? DownloadStationUsername { get; set; } = null;
+ [DisplayName("Synology DownloadStation Password")]
+ [Description("The password to use when connecting to the Synology DownloadStation.")]
+ public String? DownloadStationPassword { get; set; } = null;
+
+ [DisplayName("Synology Download Station Download Path")]
+ [Description("The root path to doawnload the file on the Synology DownloadStation host, if empty use the default DownloadStation path but won't create catagory folders.")]
+ public String? DownloadStationDownloadPath { get; set; } = null;
+
[DisplayName("Log level")]
[Description("Only set when trying to debug a download client, can generate a lot of logs.")]
public DownloadClientLogLevel LogLevel { get; set; } = DownloadClientLogLevel.None;
diff --git a/server/RdtClient.Service/RdtClient.Service.csproj b/server/RdtClient.Service/RdtClient.Service.csproj
index 86ce058..dd334a4 100644
--- a/server/RdtClient.Service/RdtClient.Service.csproj
+++ b/server/RdtClient.Service/RdtClient.Service.csproj
@@ -22,6 +22,7 @@
+
diff --git a/server/RdtClient.Service/Services/DownloadClient.cs b/server/RdtClient.Service/Services/DownloadClient.cs
index 3ab400b..049bde1 100644
--- a/server/RdtClient.Service/Services/DownloadClient.cs
+++ b/server/RdtClient.Service/Services/DownloadClient.cs
@@ -52,6 +52,7 @@ public class DownloadClient(Download download, Torrent torrent, String destinati
Data.Enums.DownloadClient.Bezzad => new BezzadDownloader(download.Link, filePath),
Data.Enums.DownloadClient.Aria2c => new Aria2cDownloader(download.RemoteId, download.Link, filePath, downloadPath, category),
Data.Enums.DownloadClient.Symlink => new SymlinkDownloader(download.Link, filePath, downloadPath),
+ Data.Enums.DownloadClient.DownloadStation => await DownloadStationDownloader.Init(download.RemoteId, download.Link, filePath, downloadPath, category),
_ => throw new($"Unknown download client {Type}")
};
diff --git a/server/RdtClient.Service/Services/Downloaders/DownloadStationDownloader.cs b/server/RdtClient.Service/Services/Downloaders/DownloadStationDownloader.cs
new file mode 100644
index 0000000..0bf53c9
--- /dev/null
+++ b/server/RdtClient.Service/Services/Downloaders/DownloadStationDownloader.cs
@@ -0,0 +1,189 @@
+
+using Serilog;
+using Synology.Api.Client;
+using Synology.Api.Client.Apis.DownloadStation.Task.Models;
+
+namespace RdtClient.Service.Services.Downloaders;
+
+public class DownloadStationDownloader : IDownloader
+{
+ public event EventHandler? DownloadComplete;
+ public event EventHandler? DownloadProgress;
+
+ private const Int32 RetryCount = 5;
+
+ private readonly SynologyClient _synologyClient;
+
+ private readonly ILogger _logger;
+ private readonly String _uri;
+ private readonly String _filePath;
+ private readonly String? _remotePath;
+
+ private String? _gid;
+ private DownloadStationDownloader(String? gid, String uri, String filePath, String downloadPath, String? category)
+ {
+ _logger = Log.ForContext();
+ _logger.Debug($"Instantiated new DownloadStation Downloader for URI {uri} to filePath {filePath} and downloadPath {downloadPath} and GID {gid}");
+
+ _gid = gid;
+ _uri = uri;
+ _filePath = filePath;
+
+ _remotePath = !String.IsNullOrWhiteSpace(Settings.Get.DownloadClient.DownloadStationDownloadPath)
+ ? String.IsNullOrWhiteSpace(category)
+ ? Path.Combine(ToCurrentPath(Settings.Get.DownloadClient.DownloadStationDownloadPath))
+ : Path.Combine(ToCurrentPath(Settings.Get.DownloadClient.DownloadStationDownloadPath), category)
+ : null;
+
+ _synologyClient = new SynologyClient(Settings.Get.DownloadClient.DownloadStationUrl);
+ }
+
+ public static async Task Init(String? gid, String uri, String filePath, String downloadPath, String? category)
+ {
+ var result = new DownloadStationDownloader(gid, uri, filePath, downloadPath, category);
+ if (Settings.Get.DownloadClient.DownloadStationUsername != null && Settings.Get.DownloadClient.DownloadStationPassword != null)
+ await result._synologyClient.LoginAsync(Settings.Get.DownloadClient.DownloadStationUsername, Settings.Get.DownloadClient.DownloadStationPassword);
+
+ return result;
+ }
+
+ public async Task Cancel()
+ {
+ if (_gid != null)
+ {
+ _logger.Debug($"Remove download {_uri} {_gid} from Synology DownloadStation");
+
+ await _synologyClient.DownloadStationApi().TaskEndpoint().DeleteAsync(new DownloadStationTaskDeleteRequest
+ {
+ Ids = new List { _gid },
+ ForceComplete = false
+ });
+ }
+ }
+
+ public async Task Download()
+ {
+ var path = _remotePath != null ? ToUnixPath(_remotePath) : null;
+ _logger.Debug($"Starting download of {_uri}, writing to path: {path}");
+
+ if (_gid != null)
+ {
+ if (GetTask() != null)
+ {
+ throw new($"The download link {_uri} has already been added to DownloadStation");
+ }
+ }
+
+
+ var retryCount = 0;
+ while (retryCount < 5)
+ {
+ _gid = await GetGidFromUri();
+ if (_gid != null)
+ {
+ _logger.Debug($"Download with ID {_gid} found in DownloadStation");
+ return _gid;
+ }
+
+ var createResult = await _synologyClient
+ .DownloadStationApi()
+ .TaskEndpoint()
+ .CreateAsync(new DownloadStationTaskCreateRequest(_uri, path));
+
+ _logger.Debug($"Added download to DownloadStation, received ID {_gid}");
+
+ if (createResult.Success)
+ {
+ _gid = await GetGidFromUri();
+ if (_gid != null)
+ {
+ _logger.Debug($"Download with ID {_gid} found in DownloadStation");
+ return _gid;
+ }
+ else
+ {
+ retryCount++;
+ _logger.Debug($"Task not found in DownloadStation after creat Sucess. Retrying {retryCount}/{RetryCount}");
+ await Task.Delay(retryCount * 1000);
+ }
+ }
+ else
+ {
+ retryCount++;
+ _logger.Debug($"Error starting download: {createResult.Error.Code}. Retrying {retryCount}/{RetryCount}");
+ await Task.Delay(retryCount * 1000);
+ }
+ }
+
+ throw new Exception($"Unable to download file");
+ }
+
+ private async Task GetGidFromUri()
+ {
+ var tasks = await _synologyClient.DownloadStationApi().TaskEndpoint().ListAsync();
+ return tasks.Tasks.FirstOrDefault(t => t.Additional?.Detail?.Uri == _uri)?.Id;
+ }
+
+ public async Task Pause()
+ {
+ _logger.Debug($"Pausing download {_uri} {_gid}");
+ if (_gid != null)
+ await _synologyClient.DownloadStationApi().TaskEndpoint().PauseAsync(_gid);
+ }
+
+ public async Task Resume()
+ {
+ _logger.Debug($"Resuming download {_uri} {_gid}");
+ if (_gid != null)
+ await _synologyClient.DownloadStationApi().TaskEndpoint().ResumeAsync(_gid);
+ }
+
+ public async Task Update()
+ {
+ if (_gid == null)
+ return;
+
+ var task = await GetTask();
+
+ if (task == null)
+ {
+ DownloadComplete?.Invoke(this, new() { Error = "Task not found" });
+ return;
+ }
+
+ if (task.Status == "finished")
+ {
+ DownloadComplete?.Invoke(this, new() { Error = null });
+ return;
+ }
+
+ DownloadProgress?.Invoke(this, new()
+ {
+ BytesDone = task.Additional.Transfer.SizeDownloaded,
+ BytesTotal = task.Size,
+ Speed = task.Additional.Transfer.SpeedDownload
+ });
+ }
+
+ private static string ToUnixPath(string path)
+ {
+ return path.Replace(Path.DirectorySeparatorChar, '/');
+ }
+
+ private static string ToCurrentPath(string path)
+ {
+ return path.Replace('/', Path.DirectorySeparatorChar);
+ }
+
+ private async Task GetTask()
+ {
+ try
+ {
+ return await _synologyClient.DownloadStationApi().TaskEndpoint().GetInfoAsync(_gid);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
diff --git a/server/RdtClient.Service/Services/TorrentRunner.cs b/server/RdtClient.Service/Services/TorrentRunner.cs
index 6b55c83..06507f4 100644
--- a/server/RdtClient.Service/Services/TorrentRunner.cs
+++ b/server/RdtClient.Service/Services/TorrentRunner.cs
@@ -1,14 +1,14 @@
-using System.Collections.Concurrent;
-using System.Diagnostics;
-using System.Text.Json;
-using System.Web;
-using Aria2NET;
+using Aria2NET;
using Microsoft.Extensions.Logging;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.Internal;
using RdtClient.Service.Helpers;
using RdtClient.Service.Services.Downloaders;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Text.Json;
+using System.Web;
namespace RdtClient.Service.Services;
@@ -38,7 +38,7 @@ public class TorrentRunner(ILogger logger, Torrents torrents, Dow
// When starting up reset any pending downloads or unpackings so that they are restarted.
var allTorrents = await torrents.Get();
-
+
allTorrents = allTorrents.Where(m => m.Completed == null).ToList();
Log($"Found {allTorrents.Count} not completed torrents");
@@ -73,7 +73,7 @@ public class TorrentRunner(ILogger logger, Torrents torrents, Dow
Log($"No RealDebridApiKey set in settings");
return;
}
-
+
var settingDownloadLimit = Settings.Get.General.DownloadLimit;
if (settingDownloadLimit < 1)
{
@@ -122,6 +122,19 @@ public class TorrentRunner(ILogger logger, Torrents torrents, Dow
Log("Finished updating Aria2 status");
}
+ if (ActiveDownloadClients.Any(m => m.Value.Type == Data.Enums.DownloadClient.DownloadStation))
+ {
+ Log("Updating DownloadStation status");
+
+ foreach (var activeDownload in ActiveDownloadClients)
+ {
+ if (activeDownload.Value.Downloader is DownloadStationDownloader downloadStationDownloader)
+ {
+ await downloadStationDownloader.Update();
+ }
+ }
+ }
+
// 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();
@@ -149,7 +162,7 @@ public class TorrentRunner(ILogger logger, Torrents torrents, Dow
// Retry the download if an error is encountered.
LogError($"Download reported an error: {downloadClient.Error}", download, download.Torrent);
Log($"Download retry count {download.RetryCount}/{download.Torrent!.DownloadRetryAttempts}, torrent retry count {download.Torrent.RetryCount}/{download.Torrent.TorrentRetryAttempts}", download, download.Torrent);
-
+
if (download.RetryCount < download.Torrent.DownloadRetryAttempts)
{
Log($"Retrying download", download, download.Torrent);
@@ -202,7 +215,7 @@ public class TorrentRunner(ILogger logger, Torrents torrents, Dow
if (unpackClient.Error != null)
{
Log($"Unpack reported an error: {unpackClient.Error}", download, download.Torrent);
-
+
await downloads.UpdateError(downloadId, unpackClient.Error);
await downloads.UpdateCompleted(downloadId, DateTimeOffset.UtcNow);
}
@@ -287,7 +300,7 @@ public class TorrentRunner(ILogger logger, Torrents torrents, Dow
await torrents.Delete(torrent.TorrentId, true, true, true);
}
-
+
// Process torrent lifetime
foreach (var torrent in allTorrents.Where(m => m.Downloads.Count == 0 && m.Completed == null && m.Lifetime > 0))
{
@@ -536,7 +549,7 @@ public class TorrentRunner(ILogger logger, Torrents torrents, Dow
}
// Check if torrent is complete, or if we don't want to download any files to the host.
- if ((torrent.Downloads.Count > 0) ||
+ if ((torrent.Downloads.Count > 0) ||
torrent.RdStatus == TorrentStatus.Finished && torrent.HostDownloadAction == TorrentHostDownloadAction.DownloadNone)
{
var completeCount = torrent.Downloads.Count(m => m.Completed != null);
@@ -622,7 +635,7 @@ public class TorrentRunner(ILogger logger, Torrents torrents, Dow
await torrents.UpdateComplete(torrent.TorrentId, ex.Message, DateTimeOffset.UtcNow, true);
}
}
-
+
await remoteService.Update();
sw.Stop();