From 6ca403f7f2430edca766ddb565806e43f5209469 Mon Sep 17 00:00:00 2001 From: Roger Far Date: Sat, 19 Mar 2022 15:12:37 -0600 Subject: [PATCH] Add running of external applications. --- .../src/app/settings/settings.component.html | 34 +++++++ client/src/app/settings/settings.component.ts | 12 +++ server/RdtClient.Data/Data/DataContext.cs | 12 +++ server/RdtClient.Data/Data/SettingData.cs | 2 + .../Models/Internal/DbSettings.cs | 2 + .../Services/TorrentRunner.cs | 2 + server/RdtClient.Service/Services/Torrents.cs | 88 +++++++++++++++++++ 7 files changed, 152 insertions(+) diff --git a/client/src/app/settings/settings.component.html b/client/src/app/settings/settings.component.html index b878cbb..03fc759 100644 --- a/client/src/app/settings/settings.component.html +++ b/client/src/app/settings/settings.component.html @@ -137,6 +137,40 @@

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: +

+ +
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