Skip to content

Conversation

@omgbeez
Copy link

@omgbeez omgbeez commented Dec 29, 2025

User description

This is an initial version of RDTClient that expands to support usenet via URL or NZB file.

  • Web UI buttons to add URL/NZB
  • Watch directory support for .nzb files
  • Add SABNZDB emulator for integration with ARR stack. (Tested with Prowlarr, Sonarr, Radarr). Same port and auth as qBitorrent.
  • Added a bunch of unit tests to help me work this out along the way
  • Minimal rename of a few things, notably ITorrentClient -> IDebridClient to make it a bit less confusing as I worked

It's possible to test in a ProxMox LXC (or update to this version) using: bash -c "$(curl -fsSL https://raw.githubusercontent.com/omgbeez/ProxmoxVE/main/ct/rdtclient.sh)"

WARNING: ^^ If you do test, this will apply a DB migration that cannot be reverted. Snaphot your current version in case you want to go back to the proper release.

CLOSES #612


PR Type

Enhancement, Tests


Description

  • Renamed ITorrentClient to IDebridClient across all implementations (AllDebrid, RealDebrid, Premiumize, DebridLink, TorBox) to better reflect support for multiple download types beyond torrents

  • Added comprehensive NZB/Usenet support with new methods AddNzbFileToDebridQueue() and AddNzbLinkToDebridQueue() in the Torrents service

  • Implemented SABnzbd API emulator (Sabnzbd service and SabnzbdController) for seamless integration with ARR stack (Prowlarr, Sonarr, Radarr) using same port and authentication as qBittorrent

  • Added DownloadType enum to distinguish between torrent and NZB downloads, with database migration to add Type column to Torrents table

  • Implemented watch folder support for .nzb files in WatchFolderChecker background service

  • Added SABnzbd authorization handler (SabnzbdHandler) with support for ma_username and ma_password authentication

  • Updated qBittorrent API filtering to exclude NZB downloads and only expose torrent downloads

  • Added new Web UI endpoints UploadNzbFile and UploadNzbLink to TorrentsController for NZB submission

  • Created comprehensive test suite with 11+ new test classes covering Sabnzbd service, TorBox client, watch folder checker, NZB operations, and authorization handling

  • Added utility helpers including FileSizeHelper for human-readable byte formatting and hash computation methods

  • Created SABnzbd data models (Queue, History, Config, Category, etc.) for API response serialization


Diagram Walkthrough

flowchart LR
  A["NZB Input<br/>URL or File"] -->|"Watch Folder<br/>or Web UI"| B["Torrents Service"]
  B -->|"AddNzbLink<br/>AddNzbFile"| C["DebridClient<br/>TorBox"]
  C -->|"Add NZB"| D["Torbox API"]
  B -->|"DownloadType.Nzb"| E["Torrent Entity"]
  E -->|"Filtered"| F["SABnzbd Service"]
  F -->|"Queue/History"| G["SabnzbdController"]
  G -->|"API Response"| H["ARR Stack<br/>Prowlarr/Sonarr/Radarr"]
  I["qBittorrent API"] -->|"Torrent Only<br/>Filter"| J["Torrent Downloads"]
Loading

File Walkthrough

Relevant files
Enhancement
29 files
Torrents.cs
Rename TorrentClient to DebridClient and add NZB support 

server/RdtClient.Service/Services/Torrents.cs

  • Renamed ITorrentClient to IDebridClient and updated all related class
    names and namespaces
  • Added support for NZB files and links with new methods
    AddNzbFileToDebridQueue and AddNzbLinkToDebridQueue
  • Added DownloadType enum parameter to distinguish between torrent and
    NZB downloads
  • Refactored CopyAddedTorrent method to use Torrent object instead of
    separate parameters
  • Added hash computation methods (ComputeMd5Hash,
    ComputeMd5HashFromBytes, ComputeSha1Hash)
  • Updated DequeueFromDebridQueue to handle both torrent and NZB types
    with appropriate API calls
  • Made several methods virtual to support testing and mocking
+201/-72
TorBoxDebridClient.cs
Add NZB support to TorBox debrid client implementation     

server/RdtClient.Service/Services/DebridClients/TorBoxDebridClient.cs

  • Renamed class from TorBoxTorrentClient to TorBoxDebridClient and
    updated interface from ITorrentClient to IDebridClient
  • Added protected virtual methods for fetching torrents and usenet
    downloads to support testing
  • Added Map method overload for UsenetInfoResult to handle NZB downloads
    with DownloadType.Nzb
  • Renamed GetTorrents to GetDownloads and updated to return both torrent
    and NZB results
  • Added NZB-specific methods: AddNzbLink and AddNzbFile
  • Updated GetAvailableFiles to check both torrent and usenet
    availability
  • Modified Delete and Unrestrict methods to accept Torrent object and
    handle both types
  • Updated GetDownloadInfos to handle NZB downloads with separate logic
    for fetching IDs
+193/-35
Sabnzbd.cs
Implement SABnzbd API emulator service                                     

server/RdtClient.Service/Services/Sabnzbd.cs

  • New Sabnzbd service class implementing SABnzbd API emulation for NZB
    downloads
  • Implements GetQueue to return active NZB downloads with progress and
    time-left calculations
  • Implements GetHistory to return completed NZB downloads with file
    paths
  • Implements AddFile and AddUrl methods for adding NZB files and links
  • Implements GetConfig and GetCategories for SABnzbd compatibility
  • Filters results to only include NZB downloads (excludes torrents)
+220/-0 
AllDebridDebridClient.cs
Rename AllDebrid client and update method signatures         

server/RdtClient.Service/Services/DebridClients/AllDebridDebridClient.cs

  • Renamed class from AllDebridTorrentClient to AllDebridDebridClient and
    updated interface
  • Updated namespace from TorrentClients to DebridClients
  • Renamed GetTorrents to GetDownloads
  • Renamed AddMagnet to AddTorrentMagnet and AddFile to AddTorrentFile
  • Added stub methods AddNzbLink and AddNzbFile that throw
    NotSupportedException
  • Updated Delete and Unrestrict method signatures to accept Torrent
    object
  • Updated all TorrentClient* type references to DebridClient*
+28/-18 
RealDebridDebridClient.cs
Rename RealDebrid client and update method signatures       

server/RdtClient.Service/Services/DebridClients/RealDebridDebridClient.cs

  • Renamed class from RealDebridTorrentClient to RealDebridDebridClient
    and updated interface
  • Updated namespace from TorrentClients to DebridClients
  • Renamed GetTorrents to GetDownloads
  • Renamed AddMagnet to AddTorrentMagnet and AddFile to AddTorrentFile
  • Added stub methods AddNzbLink and AddNzbFile that throw
    NotSupportedException
  • Updated Delete and Unrestrict method signatures to accept Torrent
    object
  • Updated all TorrentClient* type references to DebridClient*
+27/-17 
TorrentsController.cs
Add NZB upload endpoints to torrents controller                   

server/RdtClient.Web/Controllers/TorrentsController.cs

  • Updated namespace references from TorrentClient to DebridClient
  • Added new endpoint UploadNzbFile to handle NZB file uploads with form
    data
  • Added new endpoint UploadNzbLink to handle NZB URL submissions
  • Both endpoints accept optional torrent configuration and call
    appropriate service methods
  • Updated VerifyRegex to use DebridClientAvailableFile type
  • Added TorrentControllerUploadNzbLinkRequest class for NZB link
    requests
+73/-5   
DebridLinkTorrentClient.cs
Refactor debrid client interface and add NZB support stubs

server/RdtClient.Service/Services/DebridClients/DebridLinkTorrentClient.cs

  • Renamed namespace from TorrentClients to DebridClients and updated
    interface from ITorrentClient to IDebridClient
  • Renamed method GetTorrents() to GetDownloads() and return type from
    TorrentClientTorrent to DebridClientTorrent
  • Renamed methods AddMagnet() and AddFile() to AddTorrentMagnet() and
    AddTorrentFile() respectively
  • Added new methods AddNzbLink() and AddNzbFile() that throw
    NotSupportedException
  • Updated Delete() and Unrestrict() method signatures to accept Torrent
    object instead of string ID
+26/-16 
PremiumizeDebridClient.cs
Rename Premiumize client and implement debrid interface changes

server/RdtClient.Service/Services/DebridClients/PremiumizeDebridClient.cs

  • Renamed class from PremiumizeTorrentClient to PremiumizeDebridClient
    and updated namespace
  • Updated interface implementation from ITorrentClient to IDebridClient
  • Renamed GetTorrents() to GetDownloads() and updated return types to
    use DebridClient* classes
  • Renamed torrent-specific methods to AddTorrentMagnet() and
    AddTorrentFile()
  • Added stub methods AddNzbLink() and AddNzbFile() that throw
    NotSupportedException
  • Updated method signatures for Delete() and Unrestrict() to accept
    Torrent objects
+25/-15 
SabnzbdController.cs
Add SABnzbd API emulation controller for usenet integration

server/RdtClient.Web/Controllers/SabnzbdController.cs

  • New controller implementing SABnzbd API emulation with multiple
    endpoints
  • Supports version, queue, history, get_config, get_cats, addurl,
    addfile, and fullstatus modes
  • Implements authorization via SabnzbdRequirement policy
  • Handles both query string and form data parameters
  • Delegates to Sabnzbd service for business logic
+143/-0 
QBittorrent.cs
Filter qBittorrent API to exclude NZB downloads                   

server/RdtClient.Service/Services/QBittorrent.cs

  • Added filtering to TorrentInfo() to only return torrents with
    DownloadType.Torrent
  • Added type checks in multiple methods to exclude non-torrent downloads
  • Updated TorrentsTopPrio() to validate torrent type before processing
  • Ensures qBittorrent API only exposes torrent downloads, not NZBs
+14/-7   
WatchFolderChecker.cs
Add NZB file watch folder support                                               

server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs

  • Added support for .nzb file extension in watch folder monitoring
  • Refactored timing logic to set next check time at start of loop
  • Added handling for NZB files to call AddNzbFileToDebridQueue()
  • Improved delay logic to skip initial delay on first run
+11/-10 
IDebridClient.cs
Refactor debrid client interface for torrent and NZB support

server/RdtClient.Service/Services/DebridClients/IDebridClient.cs

  • Renamed interface from ITorrentClient to IDebridClient and updated
    namespace
  • Renamed GetTorrents() to GetDownloads() and updated return types
  • Split add methods into AddTorrentMagnet(), AddTorrentFile(),
    AddNzbLink(), and AddNzbFile()
  • Updated method signatures for Delete() and Unrestrict() to accept
    Torrent objects
  • Updated UpdateData() parameter type to DebridClientTorrent
+13/-11 
Torrent.cs
Add download type property to torrent model                           

server/RdtClient.Data/Models/Data/Torrent.cs

  • Added Type property of type DownloadType to distinguish between
    torrents and NZBs
  • Updated Files property to use DebridClientFile instead of
    TorrentClientFile
  • Updated namespace import from TorrentClient to DebridClient
+4/-3     
SabnzbdHandler.cs
Add SABnzbd authorization handler middleware                         

server/RdtClient.Service/Middleware/SabnzbdHandler.cs

  • New authorization handler for SABnzbd API requests
  • Implements SabnzbdRequirement authorization requirement
  • Supports authentication via ma_username and ma_password parameters
  • Handles both query string and form data parameter extraction
+58/-0   
TorrentData.cs
Add download type parameter and hash update method             

server/RdtClient.Data/Data/TorrentData.cs

  • Added downloadType parameter to Add() method signature
  • Sets Type property on new torrent entity
  • Added new UpdateHash() method to update torrent hash
+18/-0   
DebridClientTorrent.cs
Rename torrent client model to debrid client torrent         

server/RdtClient.Data/Models/DebridClient/DebridClientTorrent.cs

  • Renamed class from TorrentClientTorrent to DebridClientTorrent
  • Updated namespace from TorrentClient to DebridClient
  • Added Type property with default value DownloadType.Torrent
  • Updated Files property type to DebridClientFile
+7/-3     
SabnzbdQueueSlot.cs
Add SABnzbd queue slot data model                                               

server/RdtClient.Data/Models/Sabnzbd/SabnzbdQueueSlot.cs

  • New model class representing a single item in SABnzbd queue
  • Includes properties for NZO ID, filename, size, progress, status,
    category, time left, and priority
  • Uses JSON property name attributes for API serialization
+36/-0   
DownloadClient.cs
Update download client for debrid client naming                   

server/RdtClient.Service/Services/DownloadClient.cs

  • Updated class reference from AllDebridTorrentClient to
    AllDebridDebridClient
  • Updated namespace import from TorrentClients to DebridClients
+2/-2     
SabnzbdResponse.cs
Add SABnzbd response data model                                                   

server/RdtClient.Data/Models/Sabnzbd/SabnzbdResponse.cs

  • New model class for SABnzbd API responses
  • Includes properties for queue, history, version, status, error, NZO
    IDs, config, and categories
  • Uses JSON property name attributes for API serialization
+30/-0   
SabnzbdQueue.cs
Add SABnzbd queue status data model                                           

server/RdtClient.Data/Models/Sabnzbd/SabnzbdQueue.cs

  • New model class representing SABnzbd queue status
  • Includes version, status, speed, size, slots count, and list of queue
    slots
  • Uses JSON property name attributes for API serialization
+27/-0   
SabnzbdCategory.cs
Add SABnzbd category configuration model                                 

server/RdtClient.Data/Models/Sabnzbd/SabnzbdCategory.cs

  • New model class representing SABnzbd category configuration
  • Includes properties for name, order, directory, newzbin, priority, and
    script
  • Uses JSON property name attributes for API serialization
+24/-0   
SabnzbdHistorySlot.cs
Add SABnzbd history slot data model                                           

server/RdtClient.Data/Models/Sabnzbd/SabnzbdHistorySlot.cs

  • New model class representing a completed item in SABnzbd history
  • Includes properties for NZO ID, name, size, status, category, and
    storage path
  • Uses JSON property name attributes for API serialization
+24/-0   
FileSizeHelper.cs
Add file size formatting helper utility                                   

server/RdtClient.Service/Helpers/FileSizeHelper.cs

  • New utility class for formatting byte sizes to human-readable format
  • Supports units from bytes to exabytes
  • Returns formatted string with appropriate unit suffix
+24/-0   
SabnzbdModeAttribute.cs
Add SABnzbd mode routing attribute                                             

server/RdtClient.Web/Controllers/SabnzbdModeAttribute.cs

  • New custom action constraint attribute for routing SABnzbd API
    requests
  • Matches request mode parameter against attribute value
  • Supports both query string and form data parameter extraction
+23/-0   
UnpackClient.cs
Update unpack client for debrid client naming                       

server/RdtClient.Service/Services/UnpackClient.cs

  • Updated class reference from TorBoxTorrentClient to TorBoxDebridClient
  • Updated namespace import from TorrentClients to DebridClients
+2/-2     
SabnzbdMisc.cs
Add SABnzbd miscellaneous configuration model                       

server/RdtClient.Data/Models/Sabnzbd/SabnzbdMisc.cs

  • New model class for SABnzbd miscellaneous configuration
  • Includes properties for complete directory, download directory, port,
    and version
  • Uses JSON property name attributes for API serialization
+18/-0   
SabnzbdConfig.cs
Add SABnzbd configuration data model                                         

server/RdtClient.Data/Models/Sabnzbd/SabnzbdConfig.cs

  • New model class for SABnzbd configuration
  • Includes miscellaneous settings, categories, and servers
  • Uses JSON property name attributes for API serialization
+15/-0   
SabnzbdHistory.cs
Add SABnzbd history status data model                                       

server/RdtClient.Data/Models/Sabnzbd/SabnzbdHistory.cs

  • New model class representing SABnzbd history status
  • Includes total slots, number of slots, and list of history slots
  • Uses JSON property name attributes for API serialization
+15/-0   
Authentication.cs
Make authentication login method virtual                                 

server/RdtClient.Service/Services/Authentication.cs

  • Made Login() method virtual to allow mocking in tests
+1/-1     
Tests
11 files
AllDebridDebridClientTest.cs
Update AllDebrid client tests for renamed class                   

server/RdtClient.Service.Test/Services/TorrentClients/AllDebridDebridClientTest.cs

  • Renamed test class from AllDebridTorrentClientTest to
    AllDebridDebridClientTest
  • Updated all references from AllDebridTorrentClient to
    AllDebridDebridClient
  • Updated method calls from GetTorrents to GetDownloads
  • Updated mock logger type to match new class name
+30/-30 
SabnzbdTest.cs
Add Sabnzbd service unit tests                                                     

server/RdtClient.Service.Test/Services/SabnzbdTest.cs

  • New comprehensive test suite for Sabnzbd service with 11 test cases
  • Tests queue retrieval, history retrieval, file/URL addition, and
    configuration
  • Validates percentage and time-left calculations for NZB downloads
  • Tests filtering to ensure only NZB downloads are returned (not
    torrents)
  • Verifies category handling and path construction
+399/-0 
TorBoxDebridClientTest.cs
Add TorBox debrid client unit tests                                           

server/RdtClient.Service.Test/Services/TorrentClients/TorBoxDebridClientTest.cs

  • New test suite for TorBoxDebridClient with 8 test cases
  • Tests GetDownloads to verify correct DownloadType assignment for
    torrents and NZBs
  • Tests GetAvailableFiles for both torrent and usenet file retrieval
  • Tests Delete and Unrestrict methods for both download types
  • Tests AddNzbFile method with proper parameter passing
+280/-0 
SabnzbdControllerTest.cs
Add Sabnzbd controller unit tests                                               

server/RdtClient.Web.Test/Controllers/SabnzbdControllerTest.cs

  • New test suite for SabnzbdController with 10 test cases
  • Tests queue and history endpoints with proper response structure
  • Tests version endpoint with AllowAnonymous attribute verification
  • Tests configuration and category retrieval
  • Tests file upload with category and priority parameters
  • Validates authentication handling and error responses
+255/-0 
WatchFolderCheckerTests.cs
Add watch folder checker background service tests               

server/RdtClient.Service.Test/BackgroundServices/WatchFolderCheckerTests.cs

  • New test suite for WatchFolderChecker background service with 5 test
    cases
  • Tests processing of torrent files, magnet files, and NZB files
  • Tests file movement to processed folder after handling
  • Tests that non-matching files are ignored
  • Uses temporary directories and proper cleanup
+212/-0 
NzbTorrentsTest.cs
Add unit tests for NZB torrent operations                               

server/RdtClient.Service.Test/Services/NzbTorrentsTest.cs

  • New test class for NZB functionality in torrents service
  • Tests AddNzbLinkToDebridQueue() with valid and invalid links
  • Tests AddNzbFileToDebridQueue() with valid NZB XML and invalid XML
  • Verifies correct DownloadType.Nzb is set when adding NZBs
+135/-0 
SabnzbdHandlerTest.cs
Add authorization handler tests for SABnzbd                           

server/RdtClient.Web.Test/Controllers/SabnzbdHandlerTest.cs

  • New test class for SabnzbdHandler authorization handler
  • Tests authentication scenarios: no auth, valid credentials, already
    authenticated, invalid credentials, missing credentials
  • Verifies proper authorization handling for different authentication
    types
+118/-0 
TorrentsTest.cs
Add NZB download type tests to torrents service                   

server/RdtClient.Service.Test/Services/TorrentsTest.cs

  • Added two new test methods for NZB functionality
  • AddNzbFileToDebridQueue_ShouldSetDownloadTypeNzb() verifies NZB file
    handling
  • AddNzbLinkToDebridQueue_ShouldSetDownloadTypeNzb() verifies NZB link
    handling
  • Both tests ensure DownloadType.Nzb is correctly set
+92/-0   
TorrentsControllerNzbTest.cs
Add NZB upload controller tests                                                   

server/RdtClient.Web.Test/Controllers/TorrentsControllerNzbTest.cs

  • New test class for NZB upload endpoints in TorrentsController
  • Tests UploadNzbLink() with valid/invalid requests and empty links
  • Tests UploadNzbFile() with valid files and missing files
  • Verifies proper error handling and service method invocation
+111/-0 
QBittorrentTest.cs
Add qBittorrent filtering tests                                                   

server/RdtClient.Service.Test/Services/QBittorrentTest.cs

  • New test class for QBittorrent service
  • Tests TorrentInfo() to verify only torrent-type downloads are returned
  • Verifies NZB downloads are filtered out from qBittorrent API responses
+57/-0   
DownloadHelperTest.cs
Update download helper tests for debrid client models       

server/RdtClient.Service.Test/Helpers/DownloadHelperTest.cs

  • Updated test file references from TorrentClientFile to
    DebridClientFile
  • Updated namespace imports to use DebridClient models
+3/-3     
Configuration changes
3 files
20251224022148_AddDownloadTypeToTorrent.Designer.cs
Add DownloadType column to Torrent table migration             

server/RdtClient.Data/Migrations/20251224022148_AddDownloadTypeToTorrent.Designer.cs

  • New EF Core migration designer file for adding DownloadType property
    to Torrent entity
  • Defines database schema with new Type column as INTEGER in Torrents
    table
  • Auto-generated migration snapshot for database version 9.0.9
+485/-0 
DiConfig.cs
Update dependency injection for debrid and SABnzbd services

server/RdtClient.Service/DiConfig.cs

  • Updated service registrations to use new DebridClient class names
  • Added Sabnzbd service registration
  • Added SabnzbdHandler as authorization handler
  • Updated namespace imports from TorrentClients to DebridClients
+7/-5     
20251224022148_AddDownloadTypeToTorrent.cs
Add download type column to torrents table                             

server/RdtClient.Data/Migrations/20251224022148_AddDownloadTypeToTorrent.cs

  • New database migration adding Type column to Torrents table
  • Column type is INTEGER with default value 0
  • Includes rollback logic to drop column on migration down
+29/-0   
Additional files
19 files
add-new-torrent.component.html +79/-24 
add-new-torrent.component.ts +65/-19 
torrent.model.ts +6/-0     
navbar.component.html +3/-3     
torrent-status.pipe.ts +1/-1     
torrent.service.ts +14/-0   
ITorrentData.cs +1/-0     
SettingData.cs +1/-1     
DownloadType.cs +7/-0     
DataContextModelSnapshot.cs +3/-0     
DebridClientAvailableFile.cs +2/-2     
DebridClientFile.cs +2/-2     
DebridClientUser.cs +2/-2     
RdtClient.Service.Test.csproj +7/-0     
RdtClient.Service.csproj +1/-1     
RdtClient.Web.Test.csproj +32/-0   
dotnet-tools.json +3/-2     
QBittorrentController.cs +1/-0     
RdtClient.sln +50/-0   

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Dec 29, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Path traversal write

Description: Writing "added torrent/NZB" artifacts to Settings.Get.General.CopyAddedTorrents using
torrent.RdName for the filename may allow unsafe path construction (e.g., .. segments or
path separators not fully neutralized by FileHelper.RemoveInvalidFileNameChars),
potentially enabling overwrite/delete of unexpected files within that directory. Torrents.cs [306-352]

Referred Code
private async Task CopyAddedTorrent(Torrent torrent)
{
    if (String.IsNullOrWhiteSpace(Settings.Get.General.CopyAddedTorrents) || String.IsNullOrWhiteSpace(torrent.FileOrMagnet) || String.IsNullOrWhiteSpace(torrent.RdName))
    {
        return;
    }

    try
    {
        if (!fileSystem.Directory.Exists(Settings.Get.General.CopyAddedTorrents))
        {
            fileSystem.Directory.CreateDirectory(Settings.Get.General.CopyAddedTorrents);
        }

        var extension = torrent.Type switch
        {
            DownloadType.Nzb => ".nzb",
            DownloadType.Torrent => torrent.IsFile ? ".torrent" : ".magnet",
            _ => throw new ArgumentException("Unexpected DownloadType")
        };



 ... (clipped 26 lines)
Input parsing DoS

Description: Unrestrict parses link by splitting on / and indexing segments[4]/segments[5] without
validating length/format, which can be exploited to trigger exceptions and potentially
cause a denial-of-service if an attacker can influence the input link. TorBoxDebridClient.cs [291-307]

Referred Code
public async Task<String> Unrestrict(Torrent torrent, String link)
{
    var segments = link.Split('/');

    var zipped = segments[5] == "zip";
    var fileId = zipped ? "0" : segments[5];

    Response<String> result;

    if (torrent.Type == DownloadType.Nzb)
    {
        result = await GetClient().Usenet.RequestDownloadAsync(Convert.ToInt32(segments[4]), Convert.ToInt32(fileId), zipped);
    }
    else
    {
        result = await GetClient().Torrents.RequestDownloadAsync(Convert.ToInt32(segments[4]), Convert.ToInt32(fileId), zipped);
    }
Ticket Compliance
🟡
🎫 #612
🟢 Add support for Usenet on Torbox.
Verify end-to-end behavior via Web UI and/or watch folder by adding an NZB URL and NZB
file and confirming successful download completion in Torbox.
Verify SABnzbd emulator interoperability with ARR stack (Prowlarr/Sonarr/Radarr) in a real
environment (auth, port, queue/history behaviors).
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Unvalidated link parsing: The new Unrestrict implementation indexes segments[4] and segments[5] after
link.Split('/') without validating the segment count, which can throw on
malformed/short links rather than failing gracefully.

Referred Code
public async Task<String> Unrestrict(Torrent torrent, String link)
{
    var segments = link.Split('/');

    var zipped = segments[5] == "zip";
    var fileId = zipped ? "0" : segments[5];

    Response<String> result;

    if (torrent.Type == DownloadType.Nzb)
    {
        result = await GetClient().Usenet.RequestDownloadAsync(Convert.ToInt32(segments[4]), Convert.ToInt32(fileId), zipped);
    }
    else
    {
        result = await GetClient().Torrents.RequestDownloadAsync(Convert.ToInt32(segments[4]), Convert.ToInt32(fileId), zipped);
    }

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Sensitive data in errors: Exceptions thrown during NZB parsing embed the raw nzbLink and ex.Message into the thrown
message, which may be surfaced to callers and leak sensitive URL/query/token details.

Referred Code
public virtual async Task<Torrent> AddNzbLinkToDebridQueue(String nzbLink, Torrent torrent)
{
    torrent.RdStatus = TorrentStatus.Queued;
    try
    {
        var uri = new Uri(nzbLink);
        var lastSegment = uri.Segments.LastOrDefault()?.TrimEnd('/');
        torrent.RdName = !String.IsNullOrWhiteSpace(lastSegment) ? lastSegment : "Unknown NZB";
    }
    catch(Exception ex)
    {
        logger.LogError(ex, "{ex.Message}, trying to parse {nzbLink}", ex.Message, nzbLink);
        throw new ($"{ex.Message}, trying to parse {nzbLink}");
    }

    var nzbHash = ComputeMd5Hash(nzbLink);
    var nzbNewTorrent = await AddQueued(nzbHash, nzbLink, false, DownloadType.Nzb, torrent);
    Log($"Adding {nzbLink} with hash {nzbHash} (nzb link) to queue");

    await CopyAddedTorrent(nzbNewTorrent);




 ... (clipped 37 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Logs raw NZB link: Error logging and debug logging include the full nzbLink (and related identifiers), which
can contain embedded credentials/tokens and therefore risks leaking secrets into logs.

Referred Code
    logger.LogError(ex, "{ex.Message}, trying to parse {nzbLink}", ex.Message, nzbLink);
    throw new ($"{ex.Message}, trying to parse {nzbLink}");
}

var nzbHash = ComputeMd5Hash(nzbLink);
var nzbNewTorrent = await AddQueued(nzbHash, nzbLink, false, DownloadType.Nzb, torrent);
Log($"Adding {nzbLink} with hash {nzbHash} (nzb link) to queue");

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Missing input validation: New external inputs (nzbLink and NZB file bytes) are accepted and processed (URI parsing,
XML load, base64 storage) without explicit validation/sanitization (e.g., allowed schemes,
size limits, XML hardening), increasing risk from malformed or hostile inputs.

Referred Code
public virtual async Task<Torrent> AddNzbLinkToDebridQueue(String nzbLink, Torrent torrent)
{
    torrent.RdStatus = TorrentStatus.Queued;
    try
    {
        var uri = new Uri(nzbLink);
        var lastSegment = uri.Segments.LastOrDefault()?.TrimEnd('/');
        torrent.RdName = !String.IsNullOrWhiteSpace(lastSegment) ? lastSegment : "Unknown NZB";
    }
    catch(Exception ex)
    {
        logger.LogError(ex, "{ex.Message}, trying to parse {nzbLink}", ex.Message, nzbLink);
        throw new ($"{ex.Message}, trying to parse {nzbLink}");
    }

    var nzbHash = ComputeMd5Hash(nzbLink);
    var nzbNewTorrent = await AddQueued(nzbHash, nzbLink, false, DownloadType.Nzb, torrent);
    Log($"Adding {nzbLink} with hash {nzbHash} (nzb link) to queue");

    await CopyAddedTorrent(nzbNewTorrent);




 ... (clipped 47 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing user context: New critical actions (submitting NZB/torrent content and deleting remote resources) are
logged without any user identifier, so it is unclear whether audit trails can reconstruct
which user initiated the action.

Referred Code
public virtual async Task<Torrent> AddNzbLinkToDebridQueue(String nzbLink, Torrent torrent)
{
    torrent.RdStatus = TorrentStatus.Queued;
    try
    {
        var uri = new Uri(nzbLink);
        var lastSegment = uri.Segments.LastOrDefault()?.TrimEnd('/');
        torrent.RdName = !String.IsNullOrWhiteSpace(lastSegment) ? lastSegment : "Unknown NZB";
    }
    catch(Exception ex)
    {
        logger.LogError(ex, "{ex.Message}, trying to parse {nzbLink}", ex.Message, nzbLink);
        throw new ($"{ex.Message}, trying to parse {nzbLink}");
    }

    var nzbHash = ComputeMd5Hash(nzbLink);
    var nzbNewTorrent = await AddQueued(nzbHash, nzbLink, false, DownloadType.Nzb, torrent);
    Log($"Adding {nzbLink} with hash {nzbHash} (nzb link) to queue");

    await CopyAddedTorrent(nzbNewTorrent);




 ... (clipped 463 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Dec 29, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix watch folder check interval
Suggestion Impact:The commit implemented an interval gate by computing nextCheck from _prevCheck and delaying until that time before proceeding, then resetting _prevCheck to DateTime.Now. It also clamps the interval to a minimum of 10 seconds and uses a calculated delay instead of a fixed 1-second delay.

code diff:

-                if (_prevCheck != DateTime.MinValue)
+                var nextCheck = _prevCheck.AddSeconds(Math.Max(Settings.Get.Watch.Interval, 10));
+                if (DateTime.Now < nextCheck)
                 {
-                    await Task.Delay(1000, stoppingToken);
+                    var delay = nextCheck - DateTime.Now;
+                    await Task.Delay(delay, stoppingToken);
                 }
-                _prevCheck = DateTime.Now.AddSeconds(Settings.Get.Watch.Interval);
+                _prevCheck = DateTime.Now;

Reintroduce the time interval check in the WatchFolderChecker loop to respect
the configured Interval setting and prevent excessive scanning.

server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs [31-38]

 if (_prevCheck != DateTime.MinValue)
 {
     await Task.Delay(1000, stoppingToken);
 }
-_prevCheck = DateTime.Now.AddSeconds(Settings.Get.Watch.Interval);
+
+var nextCheck = _prevCheck.AddSeconds(Settings.Get.Watch.Interval);
+
+if (DateTime.Now < nextCheck)
+{
+    continue;
+}
+
+_prevCheck = DateTime.Now;
 
 if (String.IsNullOrWhiteSpace(Settings.Get.Watch.Path))
 {
     continue;
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a significant bug in the refactored timing logic for the watch folder, which would cause it to ignore the configured interval and run too frequently, leading to performance issues.

High
Ensure NZB files are deleted

When deleting an NZB from TorBox, call the ControlAsync method with the
deleteFiles parameter set to true to ensure the files are removed from the
service.

server/RdtClient.Service/Services/DebridClients/TorBoxDebridClient.cs [274-289]

 public async Task Delete(Torrent torrent)
 {
     if (torrent.RdId == null)
     {
         return;
     }
 
     if (torrent.Type == DownloadType.Nzb)
     {
-        await GetClient().Usenet.ControlAsync(torrent.RdId, "delete");
+        await GetClient().Usenet.ControlAsync(torrent.RdId, "delete", deleteFiles: true);
     }
     else
     {
         await GetClient().Torrents.ControlAsync(torrent.RdId, "delete");
     }
 }
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a potential resource leak where NZB files are not deleted from the debrid service. Explicitly setting deleteFiles: true fixes this bug and prevents unnecessary storage consumption.

Medium
Validate parameter before performing delete
Suggestion Impact:The commit added a `String.IsNullOrWhiteSpace(value)` check and returns a BadRequest with an error message before attempting the delete. However, it still calls `sabnzbd.Delete(value ?? "")` instead of `sabnzbd.Delete(value)` as suggested.

code diff:

         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 ?? "");

Add a null or whitespace check for the value parameter in the delete operation
and return a BadRequest if it is invalid.

server/RdtClient.Web/Controllers/SabnzbdController.cs [49-54]

 if (name == "delete")
 {
     var value = GetParam("value");
-    await sabnzbd.Delete(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 });
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies missing input validation for the delete operation, which improves API robustness by preventing potential unexpected behavior in the sabnzbd.Delete method.

Low
Security
Disable DTD processing for XML
Suggestion Impact:The commit adds System.Xml, creates an XmlReader with DtdProcessing.Prohibit, and loads the XDocument via that reader instead of directly from the stream, matching the security recommendation.

code diff:

+using System.Xml;
 using System.Xml.Linq;
 using Microsoft.Extensions.Logging;
 using MonoTorrent;
@@ -135,11 +136,14 @@
 
     public virtual async Task<Torrent> AddNzbFileToDebridQueue(Byte[] bytes, Torrent torrent)
     {
+        torrent.RdName = "Unknown NZB";
         torrent.RdStatus = TorrentStatus.Queued;
         try
         {
             using var stream = new MemoryStream(bytes);
-            var doc = XDocument.Load(stream);
+            var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit };
+            using var reader = XmlReader.Create(stream, settings);
+            var doc = XDocument.Load(reader);

To prevent potential XML External Entity (XXE) attacks, disable DTD processing
when loading the NZB file by using an XmlReader with DtdProcessing.Prohibit.

server/RdtClient.Service/Services/Torrents.cs [141-142]

 using var stream = new MemoryStream(bytes);
-var doc = XDocument.Load(stream);
+var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit };
+using var reader = XmlReader.Create(stream, settings);
+var doc = XDocument.Load(reader);

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: The suggestion addresses a critical XML External Entity (XXE) security vulnerability by recommending the disabling of DTD processing, which is a crucial security best practice when parsing untrusted XML.

High
High-level
Generalize core entities for future extensibility

To improve consistency and future extensibility, consider renaming the core
Torrent entity and Torrents service to more generic names like DebridItem and
DebridService, as they now handle NZBs in addition to torrents.

Examples:

server/RdtClient.Service/Services/Torrents.cs [112-134]
server/RdtClient.Data/Models/Data/Torrent.cs [8-112]
public class Torrent
{
    [Key]
    public Guid TorrentId { get; set; }

    public String Hash { get; set; } = null!;

    public String? Category { get; set; }
        

 ... (clipped 81 lines)

Solution Walkthrough:

Before:

// In RdtClient.Data.Models.Data.Torrent.cs
public class Torrent
{
    public Guid TorrentId { get; set; }
    public String Hash { get; set; }
    public DownloadType Type { get; set; } // Can be Torrent or Nzb
    // ... other properties
}

// In RdtClient.Service.Services.Torrents.cs
public class Torrents
{
    public async Task<Torrent> AddNzbLinkToDebridQueue(String nzbLink, Torrent torrent) { ... }

    public async Task DequeueFromDebridQueue(Torrent torrent)
    {
        if (torrent.Type == DownloadType.Nzb) { ... }
        else { ... }
    }
}

After:

// In RdtClient.Data.Models.Data.DebridItem.cs (renamed from Torrent.cs)
public class DebridItem
{
    public Guid DebridItemId { get; set; }
    public String Hash { get; set; }
    public DownloadType Type { get; set; }
    // ... other properties
}

// In RdtClient.Service.Services.DebridService.cs (renamed from Torrents.cs)
public class DebridService
{
    public async Task<DebridItem> AddNzbLinkToDebridQueue(String nzbLink, DebridItem item) { ... }

    public async Task DequeueFromDebridQueue(DebridItem item)
    {
        if (item.Type == DownloadType.Nzb) { ... }
        else { ... }
    }
}
Suggestion importance[1-10]: 8

__

Why: This is a significant architectural suggestion that correctly identifies a conceptual inconsistency in core components like the Torrent entity and Torrents service, which now handle non-torrent types, improving long-term maintainability.

Medium
General
Preserve original exception stack trace

In the catch block, use throw; to re-throw the original exception, preserving
its stack trace for easier debugging.

server/RdtClient.Service/Services/Torrents.cs [121-125]

 catch(Exception ex)
 {
-    logger.LogError(ex, "{ex.Message}, trying to parse {nzbLink}", ex.Message, nzbLink);
-    throw new ($"{ex.Message}, trying to parse {nzbLink}");
+    logger.LogError(ex, "Error trying to parse {nzbLink}", nzbLink);
+    throw;
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that re-throwing an exception with throw; preserves the original stack trace, which is crucial for debugging, and is a better practice than creating a new exception.

Medium
Set a default torrent name

Add an else block to set a default torrent.RdName to "Unknown NZB" when a title
cannot be extracted from an NZB file, ensuring consistency.

server/RdtClient.Service/Services/Torrents.cs [160-163]

 if (!String.IsNullOrWhiteSpace(title))
 {
     torrent.RdName = title.Trim();
 }
+else
+{
+    torrent.RdName = "Unknown NZB";
+}

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly points out that torrent.RdName might not be set, and proposes setting a default value to ensure consistent behavior, which improves the robustness of the new NZB file handling logic.

Low
  • Update

- Run immediately on entry (good for tests)
- Enforces min 10 seconds on loop to prevent high CPU usage on inadvertent low values, delays for the intended wait time as well.
@omgbeez
Copy link
Author

omgbeez commented Dec 29, 2025

Qodo has hallucinated the issue "Ensure NZB files are deleted", it works fine, and the recommendation isn't documented anywhere in TorBox's SDKs or API docs. It's not a valid parameter in that function.

@trustin
Copy link

trustin commented Jan 10, 2026

@omgbeez I tried this changeset using your fork (version 0.0.17), and it doesn't seem to handle the status messages related with unpack, like the following:

Received RealDebrid error: Direct Unpack: 63/69 - 0:01 left. 

How about relaxing the error detection logic a little bit so that it is OK with unpack/verify/recover progress messages?

@simonwahlstrom
Copy link

Tried it as well. The usenet download finished at Torbox but the client only said:

Received RealDebrid error: Waiting in queue to process.

and never managed to continue.

There are still a few strings that needs to be changed from torrent to download as well. Didn't write them down but they are there.

@omgbeez
Copy link
Author

omgbeez commented Jan 10, 2026

@trustin @simonwahlstrom Please note that my test branch has a LOT of other changes I'm testing at the same time, many of which are not part of this PR.

Received RealDebrid error: Direct Unpack: 63/69 - 0:01 left.

I saw this for the first time last night. I think it's a red herring and the error was probably triggered due to a bug I had in handling ACTIVE_LIMIT responses. I'll investigate if I see it again though.

Received RealDebrid error: Waiting in queue to process.

This isn't actually an error, it shows as one because the mapping of statuses from TorBox to RDTC is incomplete, and not documented, so it falls back to an error status. This is related to a feature I was testing (and disabled this morning) to queue downloads on TB instead of failing to add them when all your download slots are full. TB can be VERY slow at resuming a queued download, that's why I disabled it. Their docs claim an hour for their backend to check queued jobs and start them.

If you re-run the update script, latest version is 0.0.17 and should address both problems.

@omgbeez
Copy link
Author

omgbeez commented Jan 10, 2026

How about relaxing the error detection logic a little bit so that it is OK with unpack/verify/recover progress messages?

To be clear, the error status checks are not my code.... but yeah, I noticed this too, and unfortunately the codes are not documented for usenet so there will be some manual discovery to figure them out and improve it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Usenet support for Torbox?

4 participants