Architecture¶
Detailed overview of Torrentarr's system architecture and design patterns.
System Design¶
Torrentarr uses ASP.NET Core Generic Host with hosted background services, designed for reliability, scalability, and isolation:
graph TB
Host["ποΈ Generic Host<br/>(Torrentarr.Host)"]
Host -->|registers| WebAPI["π ASP.NET Core API<br/>(minimal API endpoints)"]
Host -->|registers| Radarr["π½οΈ Arr Manager Service<br/>(Radarr-4K)"]
Host -->|registers| Sonarr["πΊ Arr Manager Service<br/>(Sonarr-TV)"]
Host -->|registers| Lidarr["π΅ Arr Manager Service<br/>(Lidarr-Music)"]
WebAPI -->|API calls| QBT["βοΈ qBittorrent<br/>(Torrent Client)"]
Radarr -->|API calls| QBT
Sonarr -->|API calls| QBT
Lidarr -->|API calls| QBT
Radarr -->|API calls| RadarrAPI["π‘ Radarr API"]
Sonarr -->|API calls| SonarrAPI["π‘ Sonarr API"]
Lidarr -->|API calls| LidarrAPI["π‘ Lidarr API"]
WebAPI -.->|reads| DB[(ποΈ SQLite<br/>Database)]
Radarr -.->|writes| DB
Sonarr -.->|writes| DB
Lidarr -.->|writes| DB
subgraph "Generic Host Responsibilities"
H1["β
Lifetime management (start/stop)"]
H2["β
Dependency injection container"]
H3["β
Configuration pipeline"]
H4["β
Graceful shutdown on SIGTERM/SIGINT"]
end
subgraph "ASP.NET Core API Responsibilities"
W1["β
REST API (/api/*, /web/*)"]
W2["β
React SPA (static files)"]
W3["β
Token authentication"]
W4["β
Real-time log streaming"]
end
subgraph "Arr Manager Responsibilities"
A1["β
Independent background loop"]
A2["β
Health monitoring"]
A3["β
Import triggering"]
A4["β
Blacklist management"]
end
style Host fill:#4dabf7,stroke:#1971c2,color:#000
style WebAPI fill:#51cf66,stroke:#2f9e44,color:#000
style Radarr fill:#ffa94d,stroke:#fd7e14,color:#000
style Sonarr fill:#ffa94d,stroke:#fd7e14,color:#000
style Lidarr fill:#ffa94d,stroke:#fd7e14,color:#000
style QBT fill:#e599f7,stroke:#ae3ec9,color:#000
style DB fill:#74c0fc,stroke:#1c7ed6,color:#000 Key Architecture Principles:
- Service Isolation: Each Arr instance runs as an independent
BackgroundServiceβ one failure doesn't affect others - Fault Tolerance: The host monitors and restarts failed services via
CancellationTokenand retry policies - Simplicity: No complex IPC β coordination via SQLite and external APIs
- Dependency Injection: All components are registered in the DI container for testability
Core Components¶
Generic Host¶
Project: Torrentarr.Host
The entry point β Program.cs β configures and starts the ASP.NET Core Generic Host:
- Reads and validates configuration (TOML)
- Registers all services in the DI container
- Starts the ASP.NET Core HTTP server
- Starts all
IHostedServiceinstances - Handles SIGTERM/SIGINT for graceful shutdown
ASP.NET Core API¶
Project: Torrentarr.Host β minimal API endpoints in Program.cs
Responsibilities:
- Serves REST API on
/api/*(token-protected) and/web/*(public) routes - Hosts React SPA from
Torrentarr.Host/static/via static file middleware - Provides token-based authentication middleware
- Serves log files and process status to the WebUI
Arr Manager Services¶
Project: Torrentarr.Core β ArrManagerBase and subclasses
Each configured Arr instance (Radarr/Sonarr/Lidarr) runs as an IHostedService:
- Independent background loop checking qBittorrent every N seconds
- Queries Arr API for media information
- Performs health checks on torrents
- Triggers imports when torrents complete
- Manages blacklisting and re-searching
- Tracks state in SQLite database
Background Services¶
Auto-Update Service¶
- Checks GitHub releases for new versions on a schedule
- Downloads and validates release packages
- Triggers application restart when an update is available
Configuration Watcher¶
- Monitors
config.tomlfor file-system changes - Signals running services to reload configuration
- Triggers a
RestartLoopException-equivalent viaCancellationToken
Data Flow¶
Torrent Processing Pipeline¶
sequenceDiagram
participant QBT as βοΈ qBittorrent
participant AM as π‘ Arr Manager
participant DB as ποΈ Database
participant ARR as π¬ Arr API
Note over AM: Every N seconds (LoopSleepTimer)
rect rgb(230, 245, 255)
Note right of AM: 1. Detection Phase
AM->>QBT: GET /api/v2/torrents/info?category=radarr-4k
QBT-->>AM: List of torrents with tags
AM->>AM: Filter by configured categories
end
rect rgb(211, 249, 216)
Note right of AM: 2. Classification Phase
AM->>DB: SELECT * FROM Downloads WHERE Hash IN (...)
DB-->>AM: Tracked torrents
AM->>AM: Determine state:<br/>downloading, stalled,<br/>completed, seeding
end
rect rgb(255, 243, 191)
Note right of AM: 3. Health Check Phase
AM->>QBT: GET torrent details (ETA, stall time, trackers)
QBT-->>AM: Torrent health data
AM->>AM: Check ETA vs MaxETA<br/>Check stall time vs StallTimeout<br/>Verify tracker status
end
rect rgb(255, 230, 230)
Note right of AM: 4. Action Decision Phase
alt Completed + Valid
AM->>ARR: POST /api/v3/command (DownloadedMoviesScan)
ARR-->>AM: Import queued
Note over AM: β
Import triggered
else Failed Health Check
AM->>ARR: POST /api/v3/queue/blacklist (hash)
ARR-->>AM: Blacklisted
AM->>QBT: DELETE /api/v2/torrents/delete
Note over AM: β Blacklisted & deleted
else Blacklisted Item
AM->>ARR: POST /api/v3/command (MoviesSearch)
ARR-->>AM: Search queued
Note over AM: π Re-search triggered
else Seeded Enough
AM->>QBT: DELETE /api/v2/torrents/delete
Note over AM: ποΈ Cleaned up
end
end
rect rgb(243, 232, 255)
Note right of AM: 5. State Update Phase
AM->>DB: UPDATE Downloads SET State=?, UpdatedAt=?
AM->>DB: INSERT INTO EntryExpiry (EntryId, ExpiresAt)
DB-->>AM: State persisted
Note over AM: πΎ Audit trail updated
end Pipeline Stages:
- Detection β Poll qBittorrent for torrents matching configured categories/tags
- Classification β Query database to determine tracking state and history
- Health Check β Evaluate torrent health against configured thresholds
- Action Decision β Choose appropriate action (import/blacklist/re-search/cleanup)
- State Update β Persist state changes and actions to database for audit trail
Configuration Flow¶
flowchart TD
Start([π Application Start])
Start --> LoadTOML["π Load TOML File<br/>(config.toml)"]
LoadTOML --> ParseTOML["π Parse & Validate<br/>(ConfigurationLoader.cs)"]
ParseTOML --> CheckVersion{Config version<br/>matches?}
CheckVersion -->|No| Migrate["βοΈ Apply Migrations<br/>(MigrateConfig)"]
CheckVersion -->|Yes| EnvVars
Migrate --> EnvVars["π Config path override<br/>(TORRENTARR_CONFIG)"]
EnvVars --> CheckEnv{TORRENTARR_CONFIG<br/>set?}
CheckEnv -->|Yes| Override["βοΈ Use as config file path<br/>(e.g. Docker)"]
CheckEnv -->|No| Validate
Override --> Validate["β
Validation<br/>(ValidateConfig)"]
Validate --> CheckRequired{Required<br/>fields present?}
CheckRequired -->|No| Error["β Error: Missing Config"]
CheckRequired -->|Yes| DI["π¦ Register in DI Container<br/>(IOptions<TorrentarrConfig>)"]
DI --> StartHost["ποΈ Start Generic Host"]
StartHost --> StartWebAPI["Start β π ASP.NET Core API"]
StartHost --> SpawnArr1["Start β π‘ Arr Manager 1"]
StartHost --> SpawnArr2["Start β π‘ Arr Manager 2"]
StartWebAPI --> Runtime["β‘ Runtime"]
SpawnArr1 --> Runtime
SpawnArr2 --> Runtime
Error --> End([π₯ Exit])
Runtime --> End2([β
Running])
style Start fill:#dee2e6,stroke:#495057,color:#000
style LoadTOML fill:#e7f5ff,stroke:#1971c2,color:#000
style Migrate fill:#fff3bf,stroke:#fab005,color:#000
style Override fill:#d3f9d8,stroke:#2f9e44,color:#000
style Validate fill:#e7f5ff,stroke:#1971c2,color:#000
style Error fill:#ffe3e3,stroke:#c92a2a,color:#000
style DI fill:#f3f0ff,stroke:#7950f2,color:#000
style Runtime fill:#d3f9d8,stroke:#2f9e44,color:#000 Configuration Precedence (highest to lowest):
- Environment Variable (
TORRENTARR_CONFIG) β Config file path only; when set, used to locate config.toml - TOML File (
config.toml) β Standard configuration - Defaults (in
ConfigurationLoader.cs) β Fallback values
Key Files:
Torrentarr.Core/Configuration/ConfigurationLoader.csβ TOML parsing, validation, migrationsTorrentarr.Host/Program.csβ DI registration, host startup
API Request Flow¶
sequenceDiagram
participant Client as π» Client<br/>(React App/API)
participant Auth as π Auth Middleware
participant API as π ASP.NET Core API
participant Logic as βοΈ Handler Logic
participant DB as ποΈ Database
participant ARR as π‘ Arr APIs
Client->>API: HTTP Request<br/>GET /api/processes
rect rgb(255, 243, 191)
Note right of API: Authentication Phase
API->>Auth: Check Authorization header
alt Token Valid
Auth-->>API: β
Authenticated
else Token Missing/Invalid
Auth-->>Client: β 401 Unauthorized
Note over Client: Request rejected
end
end
rect rgb(230, 245, 255)
Note right of API: Request Processing Phase
API->>Logic: Route to handler
alt Read Operation
Logic->>DB: SELECT * FROM Downloads
DB-->>Logic: Query results
else Write Operation
Logic->>DB: INSERT/UPDATE/DELETE
DB-->>Logic: Rows affected
else External Query
Logic->>ARR: GET /api/v3/movie/123
ARR-->>Logic: Movie metadata
end
end
rect rgb(211, 249, 216)
Note right of API: Response Phase
Logic-->>API: Processed data
API->>API: Serialize to JSON
API-->>Client: 200 OK<br/>{ "data": [...] }
end API Endpoints:
/api/processesβ List all Arr manager services and their states/api/logsβ Read log files/api/configβ Read/update configuration/web/statusβ Public status endpoint/web/qbit/categoriesβ qBittorrent category information
Authentication:
All /api/* endpoints require Authorization: Bearer header matching WebUI.Token from config.toml.
Component Architecture¶
Hosted Services Model¶
Torrentarr uses .NET's IHostedService / BackgroundService pattern:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Generic Host (Program.cs) β
β - Configuration management β
β - Dependency injection container β
β - Service lifecycle orchestration β
β - Signal handling (SIGTERM, SIGINT) β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β hosts
βββββββββββΌββββββββββ¬ββββββββββββββββββ
β β β β
ββββββΌββββ ββββΌββββ βββββΌβββββ βββββββΌβββββββ
βASP.NET β βRadarrβ β Sonarr β ... β Lidarr β
β Core β β Mgr β β Mgr β β Mgr β
β β β β β β β β
βMinimal β βEvent β β Event β β Event β
β API β βLoop β β Loop β β Loop β
ββββββββββ ββββββββ ββββββββββ ββββββββββββββ
β β β β
βββββββββββ΄ββββββββββ΄ββββββββββββββββββ
β
ββββββββββΌββββββββββ
β Shared Resources β
β - SQLite DB β
β - Config file β
β - Log files β
βββββββββββββββββββββ
Service Registration (Program.cs):
var builder = WebApplication.CreateBuilder(args);
// Load Torrentarr configuration
builder.Services.Configure<TorrentarrConfig>(
builder.Configuration.GetSection("Torrentarr"));
// Register Arr manager services
foreach (var arrConfig in config.ArrInstances.Values)
{
builder.Services.AddHostedService(sp =>
ArrManagerFactory.Create(arrConfig, sp));
}
// Register auto-update service
builder.Services.AddHostedService<AutoUpdateService>();
var app = builder.Build();
// Map API endpoints
app.MapGet("/web/status", GetStatus);
app.MapGet("/api/processes", GetProcesses).RequireAuthorization();
// ...
await app.RunAsync();
Database Architecture¶
Torrentarr uses SQLite for state persistence:
Schema¶
erDiagram
Downloads ||--o| EntryExpiry : "has expiry"
Downloads {
string Hash PK "Torrent hash (SHA1)"
string Name "Torrent name"
string ArrType "radarr | sonarr | lidarr"
string ArrName "Arr instance name"
int MediaId "Movie/Series/Album ID in Arr"
string State "downloading | stalled | completed | seeding"
datetime AddedAt "When torrent was added to qBittorrent"
datetime UpdatedAt "Last state update"
}
Searches {
int Id PK "Auto-increment primary key"
string ArrType "radarr | sonarr | lidarr"
string ArrName "Arr instance name"
int MediaId "Movie/Series/Album ID in Arr"
string Query "Search query sent to Arr"
datetime SearchedAt "When search was executed"
int ResultCount "Number of results returned"
}
EntryExpiry {
string EntryId FK "Foreign key to Downloads.Hash"
datetime ExpiresAt "When to delete entry"
} Table Descriptions:
- Downloads β Tracks all torrents Torrentarr is managing. Primary key is the torrent hash. Lifecycle: created on detection β updated during health checks β deleted after expiry.
- Searches β Records all automated searches for audit and deduplication. Auto-cleaned after 30 days.
- EntryExpiry β Schedules delayed cleanup after seeding goals are met.
Event Loop Architecture¶
Each Arr manager's background service loop:
flowchart TD
Start([β‘ BackgroundService.ExecuteAsync])
Start --> Init["π§ Initialize<br/>(load config, connect APIs)"]
Init --> LoopStart{Cancellation<br/>requested?}
LoopStart -->|Yes| Shutdown([π Graceful Shutdown])
LoopStart -->|No| FetchTorrents["π₯ Fetch Torrents<br/>qbitClient.GetTorrentsAsync(category)"]
FetchTorrents --> QueryDB["ποΈ Query Database<br/>SELECT * FROM Downloads"]
QueryDB --> ProcessLoop["π Process Each Torrent"]
ProcessLoop --> CheckTorrent{Torrent<br/>healthy?}
CheckTorrent -->|Yes| Import["β
Trigger Import<br/>POST /api/v3/command"]
CheckTorrent -->|No| Blacklist["β Blacklist & Delete<br/>POST /api/v3/queue/blacklist"]
CheckTorrent -->|Stalled| Retry["β οΈ Retry or Re-search"]
Import --> UpdateDB
Blacklist --> UpdateDB
Retry --> UpdateDB
UpdateDB["πΎ Update State<br/>UPDATE Downloads SET State=?"]
UpdateDB --> Cleanup["ποΈ Cleanup Expired<br/>DELETE FROM Downloads WHERE ExpiresAt < NOW()"]
Cleanup --> Sleep["π€ Sleep<br/>await Task.Delay(LoopSleepTimer, ct)"]
Sleep --> LoopStart
style Start fill:#dee2e6,stroke:#495057,color:#000
style Shutdown fill:#ffe3e3,stroke:#c92a2a,color:#000
style FetchTorrents fill:#e7f5ff,stroke:#1971c2,color:#000
style Import fill:#d3f9d8,stroke:#2f9e44,color:#000
style Blacklist fill:#ffe3e3,stroke:#c92a2a,color:#000
style Retry fill:#fff3bf,stroke:#fab005,color:#000
style Sleep fill:#f3f0ff,stroke:#7950f2,color:#000 BackgroundService implementation:
public abstract class ArrManagerBase : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await InitializeAsync(stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{
try
{
var torrents = await FetchTorrentsAsync(stoppingToken);
var tracked = await GetTrackedTorrentsAsync(stoppingToken);
foreach (var torrent in torrents)
{
try
{
var health = await CheckHealthAsync(torrent, stoppingToken);
await (health switch
{
TorrentHealth.Completed => ImportAsync(torrent, stoppingToken),
TorrentHealth.Failed => BlacklistAsync(torrent, stoppingToken),
TorrentHealth.Stalled => HandleStalledAsync(torrent, stoppingToken),
_ => Task.CompletedTask
});
}
catch (SkipTorrentException)
{
continue;
}
}
await UpdateStatesAsync(torrents, stoppingToken);
await CleanupExpiredAsync(stoppingToken);
await Task.Delay(_config.LoopSleepTimer, stoppingToken);
}
catch (OperationCanceledException)
{
break; // Graceful shutdown
}
catch (ApiUnavailableException ex)
{
_logger.LogWarning("API unavailable: {Reason}. Retrying in {Delay}s",
ex.Reason, ex.RetryAfter.TotalSeconds);
await Task.Delay(ex.RetryAfter, stoppingToken);
}
}
}
}
Torrent State Machine¶
βββββββββββ
β Detectedβ (New torrent found in qBittorrent)
ββββββ¬βββββ
β
ββββββΌββββββββββ
β Downloading β
ββββββ¬ββββββββββ
β
ββββββββββ΄βββββββββ
β β
βββββΌβββββ ββββββΌββββββ
βStalled β βCompleted β
βββββ¬βββββ ββββββ¬ββββββ
β β
βββββΌβββββ ββββββΌββββββ
βFailed β βImporting β
βββββ¬βββββ ββββββ¬ββββββ
β β
βββββΌβββββββββ ββββββΌββββββ
βBlacklisted β βImported β
βββββ¬βββββββββ ββββββ¬ββββββ
β β
βββββΌβββββββββ ββββββΌββββββ
βRe-searchingβ β Seeding β
ββββββββββββββ ββββββ¬ββββββ
β
ββββββΌββββββ
β Deleted β (After seed goals met)
ββββββββββββ
Security Architecture¶
Authentication¶
WebUI Token:
- All
/api/*endpoints checkAuthorization: Bearerheader - Token stored in config.toml (not in database)
- React app reads token from localStorage
- Stateless β no session management needed
Middleware registration:
app.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/api"))
{
var token = context.Request.Headers.Authorization
.ToString().Replace("Bearer ", "");
if (token != cfg.WebUI.Token)
{
context.Response.StatusCode = 401;
return;
}
}
await next();
});
Network Binding¶
- Default:
0.0.0.0for Docker - Recommended:
127.0.0.1behind a reverse proxy for native installs - No TLS built-in β use nginx/Caddy for HTTPS
Performance Characteristics¶
Resource Usage¶
Typical Load (4 Arr instances, 50 torrents):
- CPU: 1-2% average, 5-10% during health checks
- RAM: 150-300 MB (.NET runtime + application)
- Disk I/O: Minimal (SQLite writes are infrequent)
- Network: 1-5 KB/s (API polling)
Scaling:
- Each Arr instance adds ~20-30 MB RAM (background service overhead)
- Check interval trades CPU for responsiveness
- Database size grows with torrent history
Bottlenecks¶
- SQLite Write Contention β Mitigated by short-lived transactions; future: PostgreSQL support
- Arr API Rate Limits β Batched requests, retry with backoff
- qBittorrent API Overhead β Fetch only needed fields, cache responses
Extensibility¶
Adding New Arr Types¶
- Subclass
ArrManagerBaseinTorrentarr.Core - Implement
CheckHealthAsync()andHandleFailedAsync() - Register as a hosted service in
Program.cs - Add config section to
TorrentarrConfig
Adding New API Endpoints¶
// In Program.cs β minimal API style
app.MapGet("/api/myfeature", async (IMyService svc) =>
{
var result = await svc.GetDataAsync();
return Results.Ok(result);
}).RequireAuthorization();
Further Reading¶
- Database Schema - Complete schema documentation
- Performance Troubleshooting - Optimization strategies