Add Synology Download Station
This commit is contained in:
parent
35f959ee26
commit
047326a30f
7 changed files with 238 additions and 15 deletions
|
|
@ -50,6 +50,7 @@
|
||||||
<option [ngValue]="1">Bezzad</option>
|
<option [ngValue]="1">Bezzad</option>
|
||||||
<option [ngValue]="2">Aria2c</option>
|
<option [ngValue]="2">Aria2c</option>
|
||||||
<option [ngValue]="3">Symlink Downloader</option>
|
<option [ngValue]="3">Symlink Downloader</option>
|
||||||
|
<option [ngValue]="4">Synology DownloadStation</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<p class="help">
|
<p class="help">
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,7 @@ public enum DownloadClient
|
||||||
|
|
||||||
[Description("Symlink Downloader")]
|
[Description("Symlink Downloader")]
|
||||||
Symlink,
|
Symlink,
|
||||||
|
|
||||||
|
[Description("Synology DownloadStation")]
|
||||||
|
DownloadStation,
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
using System.ComponentModel;
|
using RdtClient.Data.Enums;
|
||||||
using RdtClient.Data.Enums;
|
using System.ComponentModel;
|
||||||
|
|
||||||
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
|
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
|
||||||
|
|
||||||
|
|
@ -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.")]
|
[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/";
|
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")]
|
[DisplayName("Log level")]
|
||||||
[Description("Only set when trying to debug a download client, can generate a lot of logs.")]
|
[Description("Only set when trying to debug a download client, can generate a lot of logs.")]
|
||||||
public DownloadClientLogLevel LogLevel { get; set; } = DownloadClientLogLevel.None;
|
public DownloadClientLogLevel LogLevel { get; set; } = DownloadClientLogLevel.None;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
<PackageReference Include="Serilog" Version="4.0.1" />
|
<PackageReference Include="Serilog" Version="4.0.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.38.0" />
|
<PackageReference Include="SharpCompress" Version="0.38.0" />
|
||||||
|
<PackageReference Include="Synology.Api.Client" Version="0.2.66" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -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.Bezzad => new BezzadDownloader(download.Link, filePath),
|
||||||
Data.Enums.DownloadClient.Aria2c => new Aria2cDownloader(download.RemoteId, download.Link, filePath, downloadPath, category),
|
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.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}")
|
_ => throw new($"Unknown download client {Type}")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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<DownloadCompleteEventArgs>? DownloadComplete;
|
||||||
|
public event EventHandler<DownloadProgressEventArgs>? 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<DownloadStationDownloader>();
|
||||||
|
_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<DownloadStationDownloader> 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<string> { _gid },
|
||||||
|
ForceComplete = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> 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<String?> 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<DownloadStationTask?> GetTask()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _synologyClient.DownloadStationApi().TaskEndpoint().GetInfoAsync(_gid);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
using System.Collections.Concurrent;
|
using Aria2NET;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Web;
|
|
||||||
using Aria2NET;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using RdtClient.Data.Enums;
|
using RdtClient.Data.Enums;
|
||||||
using RdtClient.Data.Models.Data;
|
using RdtClient.Data.Models.Data;
|
||||||
using RdtClient.Data.Models.Internal;
|
using RdtClient.Data.Models.Internal;
|
||||||
using RdtClient.Service.Helpers;
|
using RdtClient.Service.Helpers;
|
||||||
using RdtClient.Service.Services.Downloaders;
|
using RdtClient.Service.Services.Downloaders;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Web;
|
||||||
|
|
||||||
namespace RdtClient.Service.Services;
|
namespace RdtClient.Service.Services;
|
||||||
|
|
||||||
|
|
@ -122,6 +122,19 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
|
||||||
Log("Finished updating Aria2 status");
|
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.
|
// 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();
|
var completedActiveDownloads = ActiveDownloadClients.Where(m => m.Value.Finished).ToList();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue