Maximum amount of downloads that get unpacked on your host at the same time.
+
+
+
+
+
+
+
+ Path to the executable to run when the torrent and all downloads are finished. No arguments should be passed here.
+
+ When running in Docker, this command will run on your docker instance!
+
+
+
+
+
+
+
+
+
+ When the executable above is executed, use these parameters.
+
+ Supports the following parameters:
+
+
+
%N: Torrent name
+
%L: Category
+
%F: Content path (same as root path for multifile torrent)
+
%R: Root path (first torrent subdirectory path)
+
%D: Save path
+
%C: Number of files
+
%Z: Torrent size (bytes)
+
%I: Info hash
+
+
diff --git a/client/src/app/settings/settings.component.ts b/client/src/app/settings/settings.component.ts
index 48fbd39..129c113 100644
--- a/client/src/app/settings/settings.component.ts
+++ b/client/src/app/settings/settings.component.ts
@@ -48,6 +48,8 @@ export class SettingsComponent implements OnInit {
public settingTorrentRetryAttempts: number;
public settingDeleteOnError: number;
public settingTorrentLifetime: number;
+ public settingRunOnTorrentCompleteFileName: string;
+ public settingRunOnTorrentCompleteArguments: string;
constructor(private settingsService: SettingsService) {}
@@ -84,6 +86,8 @@ export class SettingsComponent implements OnInit {
this.settingTorrentRetryAttempts = parseInt(this.getSetting(results, 'TorrentRetryAttempts'), 10);
this.settingDeleteOnError = parseInt(this.getSetting(results, 'DeleteOnError'), 10);
this.settingTorrentLifetime = parseInt(this.getSetting(results, 'TorrentLifetime'), 10);
+ this.settingRunOnTorrentCompleteFileName = this.getSetting(results, 'RunOnTorrentCompleteFileName');
+ this.settingRunOnTorrentCompleteArguments = this.getSetting(results, 'RunOnTorrentCompleteArguments');
},
(err) => {
this.error = err.error;
@@ -188,6 +192,14 @@ export class SettingsComponent implements OnInit {
settingId: 'TorrentLifetime',
value: (this.settingTorrentLifetime ?? 0).toString(),
},
+ {
+ settingId: 'RunOnTorrentCompleteFileName',
+ value: this.settingRunOnTorrentCompleteFileName,
+ },
+ {
+ settingId: 'RunOnTorrentCompleteArguments',
+ value: this.settingRunOnTorrentCompleteArguments,
+ },
];
this.settingsService.update(settings).subscribe(
diff --git a/server/RdtClient.Data/Data/DataContext.cs b/server/RdtClient.Data/Data/DataContext.cs
index 5a391d8..7ae2032 100644
--- a/server/RdtClient.Data/Data/DataContext.cs
+++ b/server/RdtClient.Data/Data/DataContext.cs
@@ -195,6 +195,18 @@ namespace RdtClient.Data.Data
SettingId = "TorrentRetryAttempts",
Type = "Int32",
Value = "1"
+ },
+ new Setting
+ {
+ SettingId = "RunOnTorrentCompleteFileName",
+ Type = "String",
+ Value = ""
+ },
+ new Setting
+ {
+ SettingId = "RunOnTorrentCompleteArguments",
+ Type = "String",
+ Value = ""
}
};
diff --git a/server/RdtClient.Data/Data/SettingData.cs b/server/RdtClient.Data/Data/SettingData.cs
index 50fda43..38ee9b9 100644
--- a/server/RdtClient.Data/Data/SettingData.cs
+++ b/server/RdtClient.Data/Data/SettingData.cs
@@ -72,6 +72,8 @@ namespace RdtClient.Data.Data
TorrentRetryAttempts = GetInt32("TorrentRetryAttempts"),
DeleteOnError = GetInt32("DeleteOnError"),
TorrentLifetime = GetInt32("TorrentLifetime"),
+ RunOnTorrentCompleteFileName = GetString("RunOnTorrentCompleteFileName"),
+ RunOnTorrentCompleteArguments = GetString("RunOnTorrentCompleteArguments")
};
}
diff --git a/server/RdtClient.Data/Models/Internal/DbSettings.cs b/server/RdtClient.Data/Models/Internal/DbSettings.cs
index e76da42..cf71517 100644
--- a/server/RdtClient.Data/Models/Internal/DbSettings.cs
+++ b/server/RdtClient.Data/Models/Internal/DbSettings.cs
@@ -28,5 +28,7 @@ namespace RdtClient.Data.Models.Internal
public Int32 TorrentRetryAttempts { get; set; }
public Int32 DeleteOnError { get; set; }
public Int32 TorrentLifetime { get; set; }
+ public String RunOnTorrentCompleteFileName { get; set; }
+ public String RunOnTorrentCompleteArguments { get; set; }
}
}
diff --git a/server/RdtClient.Service/Services/TorrentRunner.cs b/server/RdtClient.Service/Services/TorrentRunner.cs
index 27178dd..3dcd362 100644
--- a/server/RdtClient.Service/Services/TorrentRunner.cs
+++ b/server/RdtClient.Service/Services/TorrentRunner.cs
@@ -573,6 +573,8 @@ namespace RdtClient.Service.Services
break;
}
+
+ await _torrents.RunTorrentComplete(torrent.TorrentId);
}
else
{
diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs
index 33c97df..8d14baa 100644
--- a/server/RdtClient.Service/Services/Torrents.cs
+++ b/server/RdtClient.Service/Services/Torrents.cs
@@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -624,6 +627,91 @@ namespace RdtClient.Service.Services
await _torrentData.Update(torrent);
}
+ public async Task RunTorrentComplete(Guid torrentId)
+ {
+ if (String.IsNullOrWhiteSpace(Settings.Get.RunOnTorrentCompleteFileName))
+ {
+ return;
+ }
+
+ var torrent = await _torrentData.GetById(torrentId);
+ var downloads = await _downloads.GetForTorrent(torrentId);
+
+ var fileName = Settings.Get.RunOnTorrentCompleteFileName;
+ var arguments = Settings.Get.RunOnTorrentCompleteArguments ?? "";
+
+ Log($"Parsing external program {fileName} with arguments {arguments}", torrent);
+
+ var downloadPath = DownloadPath(torrent);
+ var torrentPath = Path.Combine(downloadPath, torrent.RdName);
+
+ arguments = arguments.Replace("%N", $"\"{torrent.RdName}\"");
+ arguments = arguments.Replace("%L", $"\"{torrent.Category}\"");
+ arguments = arguments.Replace("%F", $"\"{torrentPath}\"");
+ arguments = arguments.Replace("%R", $"\"{downloadPath}\"");
+ arguments = arguments.Replace("%D", $"\"{torrentPath}\"");
+ arguments = arguments.Replace("%C", downloads.Count.ToString(CultureInfo.InvariantCulture).Replace(",", "").Replace(".", ""));
+ arguments = arguments.Replace("%Z", torrent.RdSize.ToString(CultureInfo.InvariantCulture).Replace(",", "").Replace(".", ""));
+ arguments = arguments.Replace("%I", torrent.Hash);
+
+ Log($"Executing external program {fileName} with arguments {arguments}", torrent);
+
+ var errorSb = new StringBuilder();
+ var outputSb = new StringBuilder();
+
+ using var process = new Process();
+
+ process.StartInfo.FileName = fileName;
+ process.StartInfo.Arguments = arguments;
+ process.StartInfo.CreateNoWindow = true;
+ process.StartInfo.UseShellExecute = false;
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardError = true;
+
+ process.OutputDataReceived += (_, data) =>
+ {
+ if (data.Data == null)
+ {
+ return;
+ }
+
+ outputSb.AppendLine(data.Data.Trim());
+ };
+ process.ErrorDataReceived += (_, data) =>
+ {
+ if (data.Data == null)
+ {
+ return;
+ }
+
+ errorSb.AppendLine(data.Data.Trim());
+ };
+
+ process.Start();
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+
+ var exited = process.WaitForExit(60000 * 10);
+
+ var errors = errorSb.ToString();
+ var output = outputSb.ToString();
+
+ if (errors.Length > 0)
+ {
+ Log($"External application exited with errors: {errors}", torrent);
+ }
+
+ if (output.Length > 0)
+ {
+ Log($"External application exited with output: {output}", torrent);
+ }
+
+ if (!exited)
+ {
+ Log("External application after a 60 second timeout", torrent);
+ }
+ }
+
private async Task UpdateTorrentClientData(Torrent torrent, TorrentClientTorrent torrentClientTorrent = null)
{
try