Merge branch 'master' into fix/bad-download-path
This commit is contained in:
commit
ef8142453f
11 changed files with 313 additions and 17 deletions
|
|
@ -193,7 +193,7 @@ By default the application runs in the root of your hosted address (i.e. https:/
|
|||
- (optional) Resharper
|
||||
|
||||
1. Open the client folder project in VS Code and run `npm install`.
|
||||
1. To debug run `ng serve`, to build run `ng build --prod`.
|
||||
1. To debug run `ng serve`, to build run `ng build -c production`.
|
||||
1. Open the Visual Studio 2019 project `RdtClient.sln` and `Publish` the `RdtClient.Web` to the given `PublishFolder` target.
|
||||
1. When debugging, make sure to run `RdtClient.Web.dll` and not `IISExpress`.
|
||||
1. The result is found in `Publish`.
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@
|
|||
<option [ngValue]="1">Bezzad</option>
|
||||
<option [ngValue]="2">Aria2c</option>
|
||||
<option [ngValue]="3">Symlink Downloader</option>
|
||||
<option [ngValue]="4">Synology DownloadStation</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="help">
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
<th (click)="sort('files.length')">Files</th>
|
||||
<th (click)="sort('downloads.length')">Downloads</th>
|
||||
<th (click)="sort('rdSize')">Size</th>
|
||||
<th (click)="sort('added')">Requested</th>
|
||||
<th (click)="sort('status')">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -53,6 +54,9 @@
|
|||
<td>
|
||||
{{ torrent.rdSize | filesize }}
|
||||
</td>
|
||||
<td>
|
||||
{{ torrent.added | date : 'medium' }}
|
||||
</td>
|
||||
<td>
|
||||
{{ torrent | status }}
|
||||
</td>
|
||||
|
|
@ -191,6 +195,7 @@
|
|||
<option [ngValue]="1">Bezzad</option>
|
||||
<option [ngValue]="2">Aria2c</option>
|
||||
<option [ngValue]="3">Symlink Downloader</option>
|
||||
<option [ngValue]="4">Synology DownloadStation</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="help">
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@
|
|||
<ng-container *ngSwitchCase="1">Bezadd</ng-container>
|
||||
<ng-container *ngSwitchCase="2">Aria2c</ng-container>
|
||||
<ng-container *ngSwitchCase="3">Symlink Downloader</ng-container>
|
||||
<ng-component *ngSwitchCase="4">Synology DownloadStation</ng-component>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
|
@ -498,6 +499,7 @@
|
|||
<option [ngValue]="1">Bezzad</option>
|
||||
<option [ngValue]="2">Aria2c</option>
|
||||
<option [ngValue]="3">Symlink Downloader</option>
|
||||
<option [ngValue]="4">Synology DownloadStation</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="help">
|
||||
|
|
|
|||
|
|
@ -15,4 +15,7 @@ public enum DownloadClient
|
|||
|
||||
[Description("Symlink Downloader")]
|
||||
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
|
||||
|
||||
|
|
@ -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.")]
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using System.Web;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using System.Web;
|
||||
|
||||
namespace RdtClient.Service.Helpers;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.39.0" />
|
||||
<PackageReference Include="Synology.Api.Client" Version="0.3.87" />
|
||||
<PackageReference Include="TorBox.NET" Version="1.3.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ 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, symlinkSourcePath, torrent.ClientKind),
|
||||
Data.Enums.DownloadClient.Symlink => new SymlinkDownloader(download.Link, filePath, downloadPath, torrent.ClientKind),
|
||||
Data.Enums.DownloadClient.DownloadStation => await DownloadStationDownloader.Init(download.RemoteId, download.Link, filePath, downloadPath, category),
|
||||
_ => throw new($"Unknown download client {Type}")
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,254 @@
|
|||
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 _filePath;
|
||||
private readonly String _uri;
|
||||
private readonly String? _remotePath;
|
||||
|
||||
private String? _gid;
|
||||
|
||||
private DownloadStationDownloader(String? gid, String uri, String? remotePath, String filePath, String downloadPath, SynologyClient synologyClient)
|
||||
{
|
||||
_logger = Log.ForContext<DownloadStationDownloader>();
|
||||
_logger.Debug($"Instantiated new DownloadStation Downloader for URI {uri} to filePath {filePath} and downloadPath {downloadPath} and GID {gid}");
|
||||
|
||||
_gid = gid;
|
||||
_filePath = filePath;
|
||||
_uri = uri;
|
||||
_remotePath = remotePath;
|
||||
_synologyClient = synologyClient;
|
||||
}
|
||||
|
||||
public static async Task<DownloadStationDownloader> Init(String? gid, String uri, String filePath, String downloadPath, String? category)
|
||||
{
|
||||
if (Settings.Get.DownloadClient.DownloadStationUrl == null)
|
||||
{
|
||||
throw new("No URL specified for Synology download station");
|
||||
}
|
||||
|
||||
if (Settings.Get.DownloadClient.DownloadStationUsername == null || Settings.Get.DownloadClient.DownloadStationPassword == null)
|
||||
{
|
||||
throw new("No username/password specified for Synology download station");
|
||||
}
|
||||
|
||||
var synologyClient = new SynologyClient(Settings.Get.DownloadClient.DownloadStationUrl);
|
||||
await synologyClient.LoginAsync(Settings.Get.DownloadClient.DownloadStationUsername, Settings.Get.DownloadClient.DownloadStationPassword);
|
||||
|
||||
String? remotePath = null;
|
||||
String? rootPath;
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(Settings.Get.DownloadClient.DownloadStationDownloadPath))
|
||||
{
|
||||
rootPath = Settings.Get.DownloadClient.DownloadStationDownloadPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
var config = await synologyClient.DownloadStationApi().InfoEndpoint().GetConfigAsync();
|
||||
rootPath = config.DefaultDestination;
|
||||
}
|
||||
|
||||
if (rootPath != null)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(category))
|
||||
{
|
||||
remotePath = Path.Combine(rootPath, downloadPath).Replace('\\', '/');
|
||||
}
|
||||
else
|
||||
{
|
||||
remotePath = Path.Combine(rootPath, category, downloadPath).Replace('\\', '/');
|
||||
}
|
||||
}
|
||||
|
||||
return new(gid, uri, remotePath, filePath, downloadPath, synologyClient);
|
||||
}
|
||||
|
||||
public async Task Cancel()
|
||||
{
|
||||
if (_gid != null)
|
||||
{
|
||||
_logger.Debug($"Remove download {_uri} {_gid} from Synology DownloadStation");
|
||||
|
||||
await _synologyClient.DownloadStationApi()
|
||||
.TaskEndpoint()
|
||||
.DeleteAsync(new()
|
||||
{
|
||||
Ids =
|
||||
[
|
||||
_gid
|
||||
],
|
||||
ForceComplete = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<String> Download()
|
||||
{
|
||||
var path = Path.GetDirectoryName(_remotePath)?.Replace('\\', '/') ?? throw new($"Invalid file path {_filePath}");
|
||||
|
||||
if (!path.StartsWith('/'))
|
||||
{
|
||||
path = '/' + path;
|
||||
}
|
||||
|
||||
_logger.Debug($"Starting download of {_uri}, writing to path: {path}");
|
||||
|
||||
if (_gid != null)
|
||||
{
|
||||
var task = await GetTask();
|
||||
|
||||
if (task != 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;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var createResult = await _synologyClient
|
||||
.DownloadStationApi()
|
||||
.TaskEndpoint()
|
||||
.CreateAsync(new(_uri, path[1..]));
|
||||
|
||||
_gid = createResult.TaskId?.FirstOrDefault();
|
||||
_logger.Debug($"Added download to DownloadStation, received ID {_gid}");
|
||||
|
||||
_gid ??= await GetGidFromUri();
|
||||
|
||||
if (_gid != null)
|
||||
{
|
||||
_logger.Debug($"Download with ID {_gid} found in DownloadStation");
|
||||
|
||||
return _gid;
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
_logger.Error($"Task not found in DownloadStation after creat Sucess. Retrying {retryCount}/{RetryCount}");
|
||||
await Task.Delay(retryCount * 1000);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
retryCount++;
|
||||
_logger.Error($"Error starting download: {e.Message}. Retrying {retryCount}/{RetryCount}");
|
||||
await Task.Delay(retryCount * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
throw new($"Unable to download file");
|
||||
}
|
||||
|
||||
private async Task<String?> GetGidFromUri()
|
||||
{
|
||||
var tasks = await _synologyClient.DownloadStationApi().TaskEndpoint().ListAsync();
|
||||
|
||||
return tasks.Task?.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)
|
||||
{
|
||||
DownloadComplete?.Invoke(this,
|
||||
new()
|
||||
{
|
||||
Error = "Task not found"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var task = await GetTask();
|
||||
|
||||
if (task == null)
|
||||
{
|
||||
DownloadComplete?.Invoke(this,
|
||||
new()
|
||||
{
|
||||
Error = "Task not found"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.Status == DownloadStationTaskStatus.Finished)
|
||||
{
|
||||
DownloadComplete?.Invoke(this,
|
||||
new()
|
||||
{
|
||||
Error = null
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
DownloadProgress?.Invoke(this,
|
||||
new()
|
||||
{
|
||||
BytesDone = task.Additional?.Transfer?.SizeDownloaded ?? 0,
|
||||
BytesTotal = task.Size,
|
||||
Speed = task.Additional?.Transfer?.SpeedDownload ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<DownloadStationTask?> GetTask()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_gid == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await _synologyClient.DownloadStationApi().TaskEndpoint().GetInfoAsync(_gid);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TorrentRunner> 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<TorrentRunner> 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<TorrentRunner> 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<TorrentRunner> 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<TorrentRunner> 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<TorrentRunner> 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))
|
||||
{
|
||||
|
|
@ -539,7 +552,7 @@ public class TorrentRunner(ILogger<TorrentRunner> 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);
|
||||
|
|
@ -625,7 +638,7 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
|
|||
await torrents.UpdateComplete(torrent.TorrentId, ex.Message, DateTimeOffset.UtcNow, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await remoteService.Update();
|
||||
|
||||
sw.Stop();
|
||||
|
|
|
|||
Loading…
Reference in a new issue