A lightweight, self-hosted torrent downloader designed for a headless Linux PC.
- Recovery after reboot
- Existing files
- Disk usage
- Adding a second torrent
- Checking all torrent services
- Troubleshooting
- The service is not running
- The runner is enabled but a torrent does not start
- Manually test the runner
- Check aria2 processes
- Check the downloader journal
TorrentDownloader is designed for an unattended server/home-server workflow where you want more control over how files inside torrents are downloaded and recovered.
The main goals are:
- Download files sequentially within each torrent.
- Run multiple torrent instances in parallel.
- Preserve partial files after interruptions.
- Resume downloads after reboot or service restart.
- Verify existing files before treating them as valid.
- Detect corrupt files and re-download them.
- Keep persistent per-file logs and recovery state.
- Monitor torrents from a terminal dashboard.
- Automatically start selected torrent instances at boot using a
.runnerfile.
The intended workflow is:
Torrent
↓
Sequential file download
↓
Integrity verification
↓
Mark file VERIFIED
↓
Next file
This is particularly useful on a headless Debian/Linux machine where reliability and unattended operation are more important than having a graphical torrent client.
Files within a torrent are processed one at a time:
File 1 → verify
↓
File 2 → verify
↓
File 3 → verify
↓
...
Multiple independent torrent instances can still run at the same time.
For example:
torrent1 → downloading file 7
torrent2 → downloading file 2
torrent3 → stopped
If a download is interrupted, the partial file is preserved.
For example:
Expected: 10 GiB
Downloaded: 4 GiB
Power failure
↓
System boots
↓
Resume from existing partial data
The goal is to avoid unnecessarily downloading the same data again.
Each torrent keeps state under:
lib/sequential-torrent/
Per-file logs are stored as:
lib/sequential-torrent/files/1.log
lib/sequential-torrent/files/2.log
lib/sequential-torrent/files/3.log
The logs record important events such as:
DOWNLOADING
RESUMING
DOWNLOADED
VERIFYING
VERIFIED
CORRUPT
RETRYING
INTERRUPTED
FILE_MISSING
A completed aria2 download is not automatically considered valid.
The intended lifecycle is:
aria2 download completes
↓
integrity verification
↓
VERIFIED
↓
next file
This helps protect against incomplete or corrupt files.
torrent-progress.sh provides a terminal dashboard with information such as:
- Service status
- Download status
- Completed files
- Current file
- Torrent progress
- Current-file progress
- Downloaded bytes
- Total bytes
- Speed
- ETA
- Disk usage
- Last update time
Designed for Debian/Linux.
Required software:
- Bash
- aria2
- systemd
- standard Linux utilities such as
awk,sed,grep,find,stat, anddate
Install aria2:
sudo apt update
sudo apt install aria2Verify:
aria2c --versionThe project uses one directory per torrent instance.
Example:
TorrentDownloader/
├── .runner
├── torrent-runner.sh
├── torrent-downloader.sh
├── torrent-progress.sh
│
├── torrent1/
│ ├── .torrent_magnets
│ ├── metadata/
│ ├── torrents/
│ └── lib/
│ └── sequential-torrent/
│ ├── files/
│ ├── progress
│ └── current.state
│
└── torrent2/
├── .torrent_magnets
├── metadata/
├── torrents/
└── lib/
└── sequential-torrent/
├── files/
├── progress
└── current.state
The main project directory in the current setup is:
/home/debian/Projects/TorrentDownloader/
Create an instance directory:
cd /home/debian/Projects/TorrentDownloader
mkdir -p torrent1For another torrent:
mkdir -p torrent2The downloader creates the required internal directories when it runs.
Each instance uses:
.torrent_magnets
For torrent1:
nano torrent1/.torrent_magnetsPut the magnet URI on the first line:
magnet:?xt=urn:btih:...
Save and exit.
The downloader reads the first line of this file.
Run torrent1:
./torrent-downloader.sh /home/debian/Projects/TorrentDownloader/torrent1Run torrent2:
./torrent-downloader.sh /home/debian/Projects/TorrentDownloader/torrent2The downloader will:
- Resolve the magnet metadata.
- Locate the generated
.torrentfile. - Read the torrent file list.
- Inspect existing files and recovery logs.
- Determine what needs to be downloaded.
- Resume partial files where possible.
- Download the current file.
- Verify the completed file.
- Mark it as verified.
- Continue to the next file.
The project uses a systemd template service:
/etc/systemd/system/torrent-downloader@.service
The instance name becomes %i.
Therefore:
torrent-downloader@torrent1.service
operates on:
/home/debian/Projects/TorrentDownloader/torrent1
and:
torrent-downloader@torrent2.service
operates on:
/home/debian/Projects/TorrentDownloader/torrent2
After changing a systemd unit:
sudo systemctl daemon-reloadsudo systemctl start torrent-downloader@torrent1.servicesudo systemctl stop torrent-downloader@torrent1.servicesudo systemctl restart torrent-downloader@torrent1.servicesystemctl status torrent-downloader@torrent1.serviceThe .runner file controls which torrent instances should start automatically after boot.
Example:
torrent1
torrent2
This means:
System boot
↓
torrent-downloader-runner.service
↓
read .runner
↓
start torrent1
↓
start torrent2
Edit:
nano /home/debian/Projects/TorrentDownloader/.runnerExample:
torrent1
torrent2
torrent3
One instance name goes on each line.
Comments can be used:
# Main torrents
torrent1
torrent2
# Additional torrent
torrent3
Blank lines are ignored.
Simply remove its line.
Change:
torrent1
torrent2
torrent3
to:
torrent1
torrent3
This does not delete torrent2 or its downloaded files. It only means torrent2 will no longer be selected by the boot runner.
With the .runner architecture, the individual services do not need to be enabled independently.
For example:
systemctl is-enabled torrent-downloader@torrent2.servicemay return:
disabled
That is normal.
The boot-enabled service is:
torrent-downloader-runner.service
Check it:
systemctl is-enabled torrent-downloader-runner.serviceExpected:
enabled
The architecture is:
systemd
│
▼
torrent-downloader-runner.service
│
▼
.runner
│
├── torrent1
│ ↓
│ torrent-downloader@torrent1.service
│
├── torrent2
│ ↓
│ torrent-downloader@torrent2.service
│
└── torrent3
↓
torrent-downloader@torrent3.service
Reload systemd:
sudo systemctl daemon-reloadEnable the runner:
sudo systemctl enable torrent-downloader-runner.serviceStart it immediately for testing:
sudo systemctl start torrent-downloader-runner.serviceCheck:
systemctl status torrent-downloader-runner.serviceA successful runner normally shows:
active (exited)
This is expected. The runner starts the individual torrent services and then exits.
The actual downloads continue under:
torrent-downloader@torrent1.service
torrent-downloader@torrent2.service
...
The project includes:
torrent-progress.sh
./torrent-progress.sh./torrent-progress.sh torrent1./torrent-progress.sh torrent1 torrent2./torrent-progress.sh -aor:
./torrent-progress.sh --all./torrent-progress.sh torrent1 -nThe monitor is intended to be used while the downloader services continue running in the background.
There are two different progress measurements.
Example:
File progress : [##########------------------------------] 25.00%
Downloaded : 2.50 GiB
Total size : 10.00 GiB
This describes the file currently being downloaded or verified.
Example:
Torrent progress : [####################--------------------] 50.00%
Downloaded : 500 GiB
Total size : 1.00 TiB
This describes the entire torrent.
Torrent-wide downloaded bytes are based on the physical files present in the download directory.
For torrent1:
sudo journalctl -u torrent-downloader@torrent1.serviceShow the last 100 lines:
sudo journalctl -u torrent-downloader@torrent1.service -n 100 --no-pagerFollow live:
sudo journalctl -u torrent-downloader@torrent1.service -fFor torrent2:
sudo journalctl -u torrent-downloader@torrent2.service -fExample:
cat torrent1/lib/sequential-torrent/files/1.logFind the latest state:
grep '^STATE' torrent1/lib/sequential-torrent/files/1.log |
tail -n 1A typical recovery sequence is:
Download file 7
↓
Power failure / reboot
↓
Partial file remains
↓
System boots
↓
runner reads .runner
↓
torrent service starts
↓
downloader reads persistent state
↓
partial file detected
↓
aria2 resumes
↓
download completes
↓
integrity verification
↓
file marked VERIFIED
↓
next file
This is one of the primary reasons persistent state and per-file logs exist.
The downloader distinguishes between different situations.
If the file exists and its state says VERIFIED, it can be skipped when it still matches the expected state.
The downloader can verify it before considering it complete.
A smaller-than-expected file is treated as potentially resumable data rather than automatically being considered corrupt.
A file that fails integrity verification is treated as corrupt and can be removed so it can be downloaded again.
The intended principle is:
Valid file
↓
Keep it
Partial file
↓
Preserve and resume
Corrupt file
↓
Delete and redownload
Downloads are stored under:
<instance>/torrents/
For example:
torrent1/torrents/
torrent2/torrents/
The monitor displays disk information including:
Disk free
Disk used
Disk total
Disk usage
Always ensure enough free disk space is available for the torrents you intend to download.
Create the instance:
mkdir -p torrent2Create its magnet file:
nano torrent2/.torrent_magnetsAdd the magnet link.
Test it manually:
./torrent-downloader.sh /home/debian/Projects/TorrentDownloader/torrent2Then add it to .runner:
torrent1
torrent2
For immediate startup without reboot:
sudo systemctl start torrent-downloader@torrent2.serviceList active torrent services:
systemctl list-units 'torrent-downloader@*.service'Check a specific instance:
systemctl status torrent-downloader@torrent1.serviceCheck:
systemctl status torrent-downloader@torrent1.serviceThen:
sudo journalctl -u torrent-downloader@torrent1.service -n 100 --no-pagerCheck the runner:
cat .runnerMake sure every name corresponds to an actual directory.
For example:
torrent1
torrent2
must correspond to:
torrent1/
torrent2/
Check the runner:
systemctl status torrent-downloader-runner.servicesudo systemctl start torrent-downloader-runner.serviceThen check:
systemctl status torrent-downloader@torrent1.service
systemctl status torrent-downloader@torrent2.serviceps -ef | grep '[a]ria2c'sudo journalctl -u torrent-downloader@torrent1.service -n 100 --no-pagerThe project can be maintained as a Git repository.
Check status:
git statusAdd changes:
git add .Commit:
git commit -m "Update torrent downloader"Push:
git pushAvoid committing downloaded data and generated state unless intentionally required.
Use .gitignore for generated download/state files.
The project has three main application components.
Responsible for:
Magnet
↓
Torrent metadata
↓
File list
↓
Recovery
↓
Sequential download
↓
Integrity verification
↓
Persistent state
Responsible for:
Reading state
↓
Calculating/displaying progress
↓
Showing service and disk information
It does not perform the torrent download itself.
Responsible for:
.runner
↓
Read selected instances
↓
systemctl start torrent-downloader@INSTANCE.service
The template unit:
torrent-downloader@.service
allows the same service definition to be used for many instances.
For example:
torrent-downloader@torrent1.service
torrent-downloader@torrent2.service
torrent-downloader@torrent3.service
Each instance runs against its own directory.
The runner adds another layer:
.runner
│
▼
torrent-downloader-runner
│
┌──────────┼──────────┐
▼ ▼ ▼
torrent1 torrent2 torrent3
│ │ │
▼ ▼ ▼
systemd systemd systemd
│ │ │
▼ ▼ ▼
aria2c aria2c aria2c
The complete boot sequence is:
Linux boots
│
▼
network-online.target
│
▼
torrent-downloader-runner.service
│
▼
.runner
│
├───────────────┐
▼ ▼
torrent1 torrent2
│ │
▼ ▼
systemd systemd
service service
│ │
▼ ▼
torrent-downloader.sh
│
▼
aria2c
│
▼
Sequential download
│
▼
Integrity verification
The project intentionally separates responsibilities:
torrent-downloader.sh
= download, recovery, verification
torrent-progress.sh
= monitoring
torrent-runner.sh
= boot selection
systemd
= process supervision
aria2
= torrent transfer
This separation makes the system easier to operate and troubleshoot on a headless machine.
Once configured, normal operation is simple.
Check running torrents:
systemctl list-units 'torrent-downloader@*.service'Open the monitor:
./torrent-progress.sh --allCheck one service:
systemctl status torrent-downloader@torrent1.serviceFollow its logs:
sudo journalctl -u torrent-downloader@torrent1.service -fAdd another torrent to boot startup:
nano .runnerThen add:
torrentN
For a new installation:
# 1. Install aria2
sudo apt update
sudo apt install aria2
# 2. Enter the project
cd /home/debian/Projects/TorrentDownloader
# 3. Create an instance
mkdir -p torrent1
# 4. Add the magnet
nano torrent1/.torrent_magnets
# 5. Test manually
./torrent-downloader.sh /home/debian/Projects/TorrentDownloader/torrent1
# 6. Configure boot startup
nano .runner
# Example:
# torrent1
# torrent2
# 7. Reload systemd
sudo systemctl daemon-reload
# 8. Enable the runner
sudo systemctl enable torrent-downloader-runner.service
# 9. Start the runner
sudo systemctl start torrent-downloader-runner.service
# 10. Check the torrent
systemctl status torrent-downloader@torrent1.service
# 11. Monitor it
./torrent-progress.sh torrent1TorrentDownloader exists to provide a reliable, unattended torrent workflow for a headless Linux machine.
Its core principles are:
- One torrent instance per directory.
- Sequential files within each torrent.
- Multiple torrents can run in parallel.
- Partial files are preserved.
- Downloads can resume after interruption.
- Existing files are verified rather than blindly replaced.
- Completed files are integrity-verified.
- Per-file state is persisted.
- Progress can be monitored from the terminal.
.runnercontrols which instances start at boot.- systemd supervises individual torrent instances.
- aria2 performs the actual torrent transfer.
The result is a downloader that can run unattended for long periods while retaining enough persistent state to recover from normal interruptions and reboots.