Compare commits

...

11 commits

Author SHA1 Message Date
Roger Far
858bd7c95c no message
Some checks failed
dotnet test / build (push) Has been cancelled
Create GitHub Release / Test, Build, and Bundle (push) Has been cancelled
Release Docker Image / build (map[arch:amd64 platform:linux/amd64 runs-on:ubuntu-latest]) (push) Has been cancelled
Release Docker Image / build (map[arch:arm64 platform:linux/arm64 runs-on:ubuntu-24.04-arm]) (push) Has been cancelled
Create GitHub Release / Create GitHub release (push) Has been cancelled
Release Docker Image / push-images (push) Has been cancelled
2026-06-05 20:32:20 -06:00
Roger Far
2822653d3b Merge branch 'main' of https://github.com/rogerfar/rdt-client 2026-06-05 20:16:42 -06:00
Roger Far
e8a86cf806 Fix torbox save path mapping for single file downloads.
Some checks failed
Release Docker Image / build (map[arch:amd64 platform:linux/amd64 runs-on:ubuntu-latest]) (push) Has been cancelled
Release Docker Image / build (map[arch:arm64 platform:linux/arm64 runs-on:ubuntu-24.04-arm]) (push) Has been cancelled
Create GitHub Release / Test, Build, and Bundle (push) Has been cancelled
dotnet test / build (push) Has been cancelled
Release Docker Image / push-images (push) Has been cancelled
Create GitHub Release / Create GitHub release (push) Has been cancelled
2026-06-05 20:16:14 -06:00
Roger Far
38530cd6fd
Merge pull request #991 from omgbeez/work/sabnzdb-delete
fix(sabnzbd): Fix delete implementation to respect finished action
2026-05-31 16:06:39 -06:00
omgbeez
a7fb1e3e9d
fix(sabnzbd): Fix delete implementation to respect finished action 2026-05-31 17:12:13 -04:00
Roger Far
5c8a078927 no message
Some checks failed
Release Docker Image / build (map[arch:amd64 platform:linux/amd64 runs-on:ubuntu-latest]) (push) Has been cancelled
Release Docker Image / build (map[arch:arm64 platform:linux/arm64 runs-on:ubuntu-24.04-arm]) (push) Has been cancelled
Create GitHub Release / Test, Build, and Bundle (push) Has been cancelled
dotnet test / build (push) Has been cancelled
Release Docker Image / push-images (push) Has been cancelled
Create GitHub Release / Create GitHub release (push) Has been cancelled
2026-05-30 11:52:11 -06:00
Roger Far
f369e97d6e
Merge pull request #989 from omgbeez/upstream/devcontainer
Add dev container setup for streamlined development
2026-05-30 08:57:55 -06:00
Roger Far
a9013e0c2a
Merge pull request #986 from asylumexp/fix/torbox-database-error
[TorBox] fix: remove usage of GetHashInfoAsync
2026-05-30 08:55:56 -06:00
Sam Heinz
c6f6008591 Migrate existing TorBox torrents RdId 2026-05-29 23:04:22 +10:00
omgbeez
b8e467652b
add(devcontainer): Add dev container setup for streamlined development
Includes Dockerfile, scripts for post-start and post-create configuration, and README instructions for using the environment with .NET and Angular.
2026-05-28 15:44:43 -04:00
Sam Heinz
e1635a43a9 fix: remove usage of GetHashInfoAsync
Removes GetHashInfoAsync, and only uses the actual torrentId.

This was originally required as cachedtorrents endpoints used to use a different torrentId to the regular torrent endpoints for whatever reason, but luckily they figured out a while ago that that was not a good idea and just combined them which means calling API to search for hashes isnt needed anymore, hopefully this will reduce rate limiting a bit.
2026-05-28 14:50:39 +10:00
17 changed files with 328 additions and 48 deletions

14
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1,14 @@
FROM mcr.microsoft.com/devcontainers/dotnet:2-10.0-noble
ARG NODE_MAJOR=22
ARG ANGULAR_CLI_VERSION=21
RUN curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - \
&& apt-get update \
&& apt-get install -y --no-install-recommends nodejs sqlite3 \
&& npm install -g "@angular/cli@^${ANGULAR_CLI_VERSION}" \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /data/db /data/downloads \
&& chown -R vscode:vscode /data

View file

@ -0,0 +1,51 @@
{
"name": "rdt-client",
"build": {
"dockerfile": "Dockerfile",
"args": {
"NODE_MAJOR": "22"
}
},
"remoteUser": "vscode",
"runArgs": ["--init"],
"containerEnv": {
"ASPNETCORE_ENVIRONMENT": "Development",
"BASE_PATH": "",
"DOTNET_USE_POLLING_FILE_WATCHER": "1",
"CHOKIDAR_USEPOLLING": "true"
},
"forwardPorts": [4200, 6500],
"portsAttributes": {
"4200": {
"label": "Angular client",
"onAutoForward": "notify"
},
"6500": {
"label": "RdtClient backend",
"onAutoForward": "silent"
}
},
"mounts": [
"source=rdtclient-data-db,target=/data/db,type=volume",
"source=rdtclient-data-downloads,target=/data/downloads,type=volume",
"source=rdtclient-nuget,target=/home/vscode/.nuget,type=volume",
"source=rdtclient-npm-cache,target=/home/vscode/.npm,type=volume"
],
"postCreateCommand": "/bin/bash .devcontainer/post-create.sh",
"postStartCommand": "/bin/bash .devcontainer/post-start.sh",
"customizations": {
"vscode": {
"extensions": [
"ms-dotnettools.csharp",
"ms-dotnettools.csdevkit",
"Angular.ng-template",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
],
"settings": {
"dotnet.defaultSolution": "server/RdtClient.sln",
"terminal.integrated.defaultProfile.linux": "zsh"
}
}
}
}

16
.devcontainer/post-create.sh Executable file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
sudo install -d -m 0775 -o vscode -g vscode \
/data/db \
/data/downloads \
/home/vscode/.nuget \
/home/vscode/.nuget/NuGet \
/home/vscode/.nuget/packages \
/home/vscode/.npm
sudo chown -R vscode:vscode /data /home/vscode/.nuget /home/vscode/.npm
dotnet restore server/RdtClient.sln
npm install --prefix client

13
.devcontainer/post-start.sh Executable file
View file

@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
sudo install -d -m 0775 -o vscode -g vscode \
/data/db \
/data/downloads \
/home/vscode/.nuget \
/home/vscode/.nuget/NuGet \
/home/vscode/.nuget/packages \
/home/vscode/.npm
sudo chown -R vscode:vscode /data /home/vscode/.nuget /home/vscode/.npm

View file

@ -6,6 +6,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased
## [2.0.138] - 2026-06-05
### Fixed
- SABnzbd fixes.
## [2.0.137] - 2026-06-05
### Fixed
- Fix torbox save path mapping for single file downloads.
## [2.0.136] - 2026-05-30
### Added
- Added devcontainer development workflow.
### Changed
- Upgraded torbox.net dependency.
## [2.0.135] - 2026-05-27
### Changed
- When adding a torrent through the qBittorrent endpoints, it will wait to see if the torrent gets added properly or errors out, resulting in a better experience for infringing files and Sonarr / Radarr.

View file

@ -218,6 +218,17 @@ By default the application runs in the root of your hosted address (i.e. https:/
- Visual Studio 2025
- (optional) Resharper
### Dev Container
The repository includes a dev container under `.devcontainer/` for the split development workflow used by this project.
It installs .NET 10 and Node 22, forwards ports `4200` and `6500`, and persists `/data/db` and `/data/downloads` in named volumes so the local SQLite database, logs, and downloads survive container rebuilds.
1. Open the repository in the dev container.
1. In one terminal run `dotnet watch run --project server/RdtClient.Web`.
1. In another terminal run `cd client && npm start`.
1. Open `http://localhost:4200`. The Angular dev server proxies `/Api` and `/hub` to the backend running on `6500`.
1. Open the client folder project in VS Code and run `npm install`.
1. To debug run `ng serve`, to build run `ng build -c production`.
1. Open the Visual Studio 2025 project `RdtClient.sln` and `Publish` the `RdtClient.Web` to the given `PublishFolder` target.

View file

@ -22,7 +22,7 @@
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.1.1" />
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="22.1.1" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.1.1" />
<PackageReference Include="TorBox.NET" Version="2.0.0" />
<PackageReference Include="TorBox.NET" Version="2.1.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>

View file

@ -366,6 +366,64 @@ public class SabnzbdTest
Assert.Equal("Failed", result.Slots[0].Status);
}
[Theory]
[InlineData(TorrentFinishedAction.RemoveAllTorrents, true, true, true, true)]
[InlineData(TorrentFinishedAction.RemoveAllTorrents, false, true, true, false)]
[InlineData(TorrentFinishedAction.RemoveRealDebrid, true, false, true, true)]
[InlineData(TorrentFinishedAction.RemoveRealDebrid, false, false, true, false)]
[InlineData(TorrentFinishedAction.RemoveClient, true, true, false, true)]
[InlineData(TorrentFinishedAction.RemoveClient, false, true, false, false)]
public async Task Delete_ShouldRespectFinishedActionAndDeleteFiles(
TorrentFinishedAction finishedAction,
Boolean deleteFiles,
Boolean expectedDeleteData,
Boolean expectedDeleteRdTorrent,
Boolean expectedDeleteLocalFiles)
{
// Arrange
var torrentId = Guid.NewGuid();
_settings.Current.Integrations.Default.FinishedAction = finishedAction;
_torrentsMock.Setup(t => t.GetByHash("hash1"))
.ReturnsAsync(new Torrent
{
TorrentId = torrentId,
Hash = "hash1",
Type = DownloadType.Nzb
});
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings);
// Act
await sabnzbd.Delete("hash1", deleteFiles);
// Assert
_torrentsMock.Verify(t => t.Delete(torrentId, expectedDeleteData, expectedDeleteRdTorrent, expectedDeleteLocalFiles), Times.Once);
}
[Fact]
public async Task Delete_ShouldNotDelete_WhenFinishedActionIsNone()
{
// Arrange
_settings.Current.Integrations.Default.FinishedAction = TorrentFinishedAction.None;
_torrentsMock.Setup(t => t.GetByHash("hash1"))
.ReturnsAsync(new Torrent
{
TorrentId = Guid.NewGuid(),
Hash = "hash1",
Type = DownloadType.Nzb
});
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings);
// Act
await sabnzbd.Delete("hash1", true);
// Assert
_torrentsMock.Verify(t => t.Delete(It.IsAny<Guid>(), It.IsAny<Boolean>(), It.IsAny<Boolean>(), It.IsAny<Boolean>()), Times.Never);
}
[Fact]
public void GetConfig_ShouldReturnCorrectConfig()
{

View file

@ -46,6 +46,7 @@ public class TorBoxDebridClientTest
{
new()
{
Id = 12345,
Hash = "hash1",
Name = "torrent1",
Size = 1000,
@ -78,7 +79,7 @@ public class TorBoxDebridClientTest
// Assert
Assert.Equal(2, result.Count);
var torrentResult = result.FirstOrDefault(r => r.Id == "hash1");
var torrentResult = result.FirstOrDefault(r => r.Id == "12345");
Assert.NotNull(torrentResult);
Assert.Equal(DownloadType.Torrent, torrentResult.Type);
@ -200,12 +201,12 @@ public class TorBoxDebridClientTest
}
[Fact]
public async Task Delete_CallsTorrentsControl_WhenTypeIsTorrent()
public async Task Delete_CallsTorrentsControlById_WhenTypeIsTorrent()
{
// Arrange
var torrent = new Torrent
{
RdId = "torrent-id",
RdId = "12345",
Type = DownloadType.Torrent
};
@ -220,7 +221,7 @@ public class TorBoxDebridClientTest
await clientMock.Object.Delete(torrent);
// Assert
torrentsApiMock.Verify(m => m.ControlAsync("torrent-id", "delete", It.IsAny<CancellationToken>()), Times.Once);
torrentsApiMock.Verify(m => m.ControlByIdAsync(12345, "delete", It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
@ -375,6 +376,7 @@ public class TorBoxDebridClientTest
var torrent = new Torrent
{
Hash = "test-hash",
RdId = "12345",
RdFiles = JsonConvert.SerializeObject(files)
};
@ -385,12 +387,6 @@ public class TorBoxDebridClientTest
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.GetHashInfoAsync("test-hash", true, 1000, It.IsAny<CancellationToken>()))
.ReturnsAsync(new TorrentInfoResult
{
Id = 12345
});
_fileFilterMock.Setup(m => m.IsDownloadable(torrent, It.IsAny<String>(), It.IsAny<Int64>())).Returns(true);
_settings.Current.Provider.PreferZippedDownloads = false;
@ -422,6 +418,7 @@ public class TorBoxDebridClientTest
var torrent = new Torrent
{
Hash = "test-hash",
RdId = "12345",
RdName = "TestTorrent",
RdFiles = JsonConvert.SerializeObject(files),
DownloadClient = DownloadClient.Aria2c
@ -436,12 +433,6 @@ public class TorBoxDebridClientTest
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.GetHashInfoAsync("test-hash", true, 1000, It.IsAny<CancellationToken>()))
.ReturnsAsync(new TorrentInfoResult
{
Id = 12345
});
_fileFilterMock.Setup(m => m.IsDownloadable(torrent, It.IsAny<String>(), It.IsAny<Int64>())).Returns(true);
// Act

View file

@ -26,7 +26,7 @@
<PackageReference Include="Synology.Api.Client" Version="[0.3.93]" />
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.1.1" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.1.1" />
<PackageReference Include="TorBox.NET" Version="2.0.0" />
<PackageReference Include="TorBox.NET" Version="2.1.0" />
</ItemGroup>
<ItemGroup>

View file

@ -81,7 +81,7 @@ public class TorBoxDebridClient(
allowZip: Settings.Get.Provider.PreferZippedDownloads,
as_queued: asQueued);
return result.Data!.Hash!;
return result.Data?.TorrentId?.ToString() ?? throw new InvalidOperationException("TorBox API did not return torrent ID.");
});
}
@ -97,7 +97,7 @@ public class TorBoxDebridClient(
allowZip: Settings.Get.Provider.PreferZippedDownloads,
as_queued: asQueued);
return result.Data!.Hash!;
return result.Data?.TorrentId?.ToString() ?? throw new InvalidOperationException("TorBox API did not return torrent ID");
});
}
@ -171,7 +171,7 @@ public class TorBoxDebridClient(
}
else
{
await GetClient().Torrents.ControlAsync(torrent.RdId, "delete");
await GetClient().Torrents.ControlByIdAsync(Int32.Parse(torrent.RdId), "delete");
}
});
}
@ -331,8 +331,12 @@ public class TorBoxDebridClient(
}
else
{
var torrentId = await HandleErrors(() => GetClient().Torrents.GetHashInfoAsync(torrent.Hash, true));
id = torrentId?.Id;
if (torrent.RdId == null)
{
return null;
}
id = Int32.Parse(torrent.RdId);
}
if (id == null)
@ -456,7 +460,7 @@ public class TorBoxDebridClient(
{
return new()
{
Id = torrent.Hash,
Id = torrent.Id.ToString(),
Filename = torrent.Name,
OriginalFilename = torrent.Name,
Hash = torrent.Hash,
@ -611,7 +615,7 @@ public class TorBoxDebridClient(
}
else
{
var result = await GetClient().Torrents.GetHashInfoAsync(id, true);
var result = await GetClient().Torrents.GetIdInfoAsync(Int32.Parse(id), true);
if (result != null)
{

View file

@ -359,6 +359,11 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
var selectedFileName = topLevelSelectedFiles[0]!;
var selectedFileBaseName = Path.GetFileNameWithoutExtension(selectedFileName);
if (torrent.ClientKind == Provider.TorBox)
{
return selectedFileBaseName;
}
if (!String.IsNullOrWhiteSpace(selectedFileBaseName) &&
selectedFileBaseName.Equals(torrent.RdName, StringComparison.OrdinalIgnoreCase))
{

View file

@ -177,7 +177,7 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
return result.Hash;
}
public virtual async Task Delete(String hash)
public virtual async Task Delete(String hash, Boolean deleteFiles = false)
{
var torrent = await torrents.GetByHash(hash);
@ -189,18 +189,18 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
switch (settings.Current.Integrations.Default.FinishedAction)
{
case TorrentFinishedAction.RemoveAllTorrents:
logger.LogDebug("Removing nzb from debrid provider and RDT-Client, no files");
await torrents.Delete(torrent.TorrentId, true, true, true);
logger.LogDebug("Removing nzb from debrid provider and RDT-Client, {Files}", deleteFiles ? "with files" : "no files");
await torrents.Delete(torrent.TorrentId, true, true, deleteFiles);
break;
case TorrentFinishedAction.RemoveRealDebrid:
logger.LogDebug("Removing nzb from debrid provider, no files");
await torrents.Delete(torrent.TorrentId, false, true, true);
logger.LogDebug("Removing nzb from debrid provider, {Files}", deleteFiles ? "with files" : "no files");
await torrents.Delete(torrent.TorrentId, false, true, deleteFiles);
break;
case TorrentFinishedAction.RemoveClient:
logger.LogDebug("Removing nzb from client, no files");
await torrents.Delete(torrent.TorrentId, true, false, true);
logger.LogDebug("Removing nzb from client, {Files}", deleteFiles ? "with files" : "no files");
await torrents.Delete(torrent.TorrentId, true, false, deleteFiles);
break;
case TorrentFinishedAction.None:

View file

@ -41,6 +41,11 @@ public class Settings(IServiceScopeFactory serviceScopeFactory) : ISettings
{
var downloadPath = settings.DownloadClient.MappedPath;
if (String.IsNullOrWhiteSpace(downloadPath))
{
downloadPath = settings.DownloadClient.DownloadPath;
}
downloadPath = downloadPath.TrimEnd('\\')
.TrimEnd('/');

View file

@ -664,6 +664,38 @@ public class Torrents(
{
torrentsByRdId.TryGetValue(rdTorrent.Id, out var torrent);
// TorBox migration from storing torrent hash in RdId to torrent ids.
if (torrent == null
&& Settings.Get.Provider.Provider == Provider.TorBox
&& rdTorrent.Type == DownloadType.Torrent
&& !String.IsNullOrWhiteSpace(rdTorrent.Hash)
&& !String.IsNullOrWhiteSpace(rdTorrent.Id))
{
torrent = torrents.FirstOrDefault(localTorrent => localTorrent is { Type: DownloadType.Torrent, ClientKind: null or Provider.TorBox }
&& !String.IsNullOrWhiteSpace(localTorrent.Hash)
&& !String.IsNullOrWhiteSpace(localTorrent.RdId)
&& localTorrent.RdId.Equals(localTorrent.Hash, StringComparison.OrdinalIgnoreCase)
&& localTorrent.Hash.Equals(rdTorrent.Hash, StringComparison.OrdinalIgnoreCase));
if (torrent != null)
{
if (!String.IsNullOrWhiteSpace(torrent.RdId))
{
torrentsByRdId.Remove(torrent.RdId);
}
await torrentData.UpdateRdId(torrent, rdTorrent.Id);
torrent.RdId = rdTorrent.Id;
torrent.ClientKind = Provider.TorBox;
torrentsByRdId[rdTorrent.Id] = torrent;
logger.LogInformation("Migrated TorBox torrent RdId from hash to torrent id for {TorrentName} ({Hash}) -> {RdId}",
torrent.RdName ?? rdTorrent.Filename,
rdTorrent.Hash,
rdTorrent.Id);
}
}
// Auto import torrents only torrents that have their files selected
if (torrent == null && settings.Current.Provider.AutoImport)
{

View file

@ -108,6 +108,30 @@ public class SabnzbdControllerTest
Assert.IsType<OkObjectResult>(result);
}
[Theory]
[InlineData("?name=delete&value=hash1&del_files=1", true)]
[InlineData("?name=delete&value=hash1&del_files=true", true)]
[InlineData("?name=delete&value=hash1&del_files=0", false)]
[InlineData("?name=delete&value=hash1", false)]
public async Task Queue_Delete_PassesDeleteFilesFlag(String queryString, Boolean expectedDeleteFiles)
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.QueryString = new(queryString);
_controller.ControllerContext.HttpContext = httpContext;
_sabnzbdMock.Setup(s => s.Delete("hash1", expectedDeleteFiles)).Returns(Task.CompletedTask);
// Act
var result = await _controller.Queue();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var response = Assert.IsType<SabnzbdResponse>(okResult.Value);
Assert.True(response.Status);
_sabnzbdMock.Verify(s => s.Delete("hash1", expectedDeleteFiles), Times.Once);
}
[Fact]
public async Task GetHistory_ReturnsOk()
{
@ -129,6 +153,26 @@ public class SabnzbdControllerTest
Assert.Equal(1, response.History.NoOfSlots);
}
[Fact]
public async Task History_Delete_PassesDeleteFilesFlag()
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.QueryString = new("?name=delete&value=hash1&del_files=1");
_controller.ControllerContext.HttpContext = httpContext;
_sabnzbdMock.Setup(s => s.Delete("hash1", true)).Returns(Task.CompletedTask);
// Act
var result = await _controller.History();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var response = Assert.IsType<SabnzbdResponse>(okResult.Value);
Assert.True(response.Status);
_sabnzbdMock.Verify(s => s.Delete("hash1", true), Times.Once);
}
[Fact]
public void GetVersion_HasAllowAnonymousAttribute()
{

View file

@ -56,22 +56,7 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
if (name == "delete")
{
var value = GetParam("value");
if (String.IsNullOrWhiteSpace(value))
{
return BadRequest(new SabnzbdResponse
{
Error = "No value specified for delete operation"
});
}
await sabnzbd.Delete(value ?? "");
return Ok(new SabnzbdResponse
{
Status = true
});
return await DeleteDownloads();
}
return Ok(new SabnzbdResponse
@ -85,6 +70,12 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
public async Task<ActionResult> History()
{
logger.LogDebug("Sabnzbd mode: history");
var name = GetParam("name");
if (name == "delete")
{
return await DeleteDownloads();
}
return Ok(new SabnzbdResponse
{
@ -183,6 +174,31 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
});
}
private async Task<ActionResult> DeleteDownloads()
{
var value = GetParam("value");
if (String.IsNullOrWhiteSpace(value))
{
return BadRequest(new SabnzbdResponse
{
Error = "No value specified for delete operation"
});
}
var deleteFiles = IsTrue(GetParam("del_files"));
foreach (var hash in value.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
await sabnzbd.Delete(hash, deleteFiles);
}
return Ok(new SabnzbdResponse
{
Status = true
});
}
private String? GetParam(String name)
{
var value = Request.Query[name].ToString();
@ -194,4 +210,9 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
return value;
}
private static Boolean IsTrue(String? value)
{
return value is "1" || Boolean.TryParse(value, out var result) && result;
}
}