torrdown 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- torrdown-0.1.0/.gitignore +15 -0
- torrdown-0.1.0/LICENSE +21 -0
- torrdown-0.1.0/PKG-INFO +194 -0
- torrdown-0.1.0/README.md +165 -0
- torrdown-0.1.0/ROADMAP.md +79 -0
- torrdown-0.1.0/pyproject.toml +59 -0
- torrdown-0.1.0/tests/test_config.py +57 -0
- torrdown-0.1.0/tests/test_magnets.py +48 -0
- torrdown-0.1.0/tests/test_sources.py +61 -0
- torrdown-0.1.0/tests/test_state.py +90 -0
- torrdown-0.1.0/torrdown/__init__.py +3 -0
- torrdown-0.1.0/torrdown/__main__.py +5 -0
- torrdown-0.1.0/torrdown/cli.py +330 -0
- torrdown-0.1.0/torrdown/config.py +149 -0
- torrdown-0.1.0/torrdown/engine.py +334 -0
- torrdown-0.1.0/torrdown/magnets.py +46 -0
- torrdown-0.1.0/torrdown/sources.py +80 -0
- torrdown-0.1.0/torrdown/state.py +90 -0
torrdown-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aryannxroot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
torrdown-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: torrdown
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A single-session libtorrent download CLI with resume, dedup, and batch support.
|
|
5
|
+
Author: aryannxroot
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: bittorrent,cli,downloader,libtorrent,torrent
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Communications :: File Sharing
|
|
22
|
+
Classifier: Topic :: Internet
|
|
23
|
+
Requires-Python: <3.14,>=3.9
|
|
24
|
+
Requires-Dist: libtorrent<2.1,>=2.0
|
|
25
|
+
Requires-Dist: rich>=13
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# TorrDown
|
|
31
|
+
|
|
32
|
+
A command-line torrent downloader built on a single shared `libtorrent` session.
|
|
33
|
+
It downloads several torrents at once, survives restarts, and installs as a real
|
|
34
|
+
`torrdown` command you can run from anywhere.
|
|
35
|
+
|
|
36
|
+
TorrDown started as a Google Colab notebook (still in the repo as
|
|
37
|
+
`legacy_download_gcollab.ipynb`) and grew into a proper, installable CLI.
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **Single shared libtorrent session** — all torrents run in one session (the way
|
|
42
|
+
libtorrent is meant to be used), not one session per torrent.
|
|
43
|
+
- **Concurrent downloads** with a fairness gate — only a bounded number compete
|
|
44
|
+
for metadata/peers at once; the rest queue, so a stuck magnet can't starve the
|
|
45
|
+
others or unfairly burn its own timeout.
|
|
46
|
+
- **Magnet links and `.torrent` files**, individually or in batches.
|
|
47
|
+
- **Automatic resume** — Ctrl+C, a crash, or a reboot doesn't lose progress;
|
|
48
|
+
downloads pick up where they left off on the next run (libtorrent resume data
|
|
49
|
+
is checkpointed to disk).
|
|
50
|
+
- **Info-hash dedup** — the same torrent won't be added twice.
|
|
51
|
+
- **Metadata-fetch timeout** — a magnet with no metadata-holding peers is skipped
|
|
52
|
+
after a while instead of hanging forever.
|
|
53
|
+
- **Batch mode** — download every magnet in a file; completed/failed entries are
|
|
54
|
+
commented out in place so re-runs don't repeat them.
|
|
55
|
+
- **Graceful shutdown** — Ctrl+C saves state cleanly before exiting.
|
|
56
|
+
- **Live progress bars** (via `rich`) for every active download.
|
|
57
|
+
- **Per-user config** with sensible defaults and clear override precedence.
|
|
58
|
+
|
|
59
|
+
## Requirements
|
|
60
|
+
|
|
61
|
+
- **macOS or Linux.** (Windows isn't supported yet.)
|
|
62
|
+
- **Python 3.9-3.13** somewhere on the machine. `libtorrent` publishes binary
|
|
63
|
+
wheels only for these versions, so TorrDown caps at `<3.14`. The installer
|
|
64
|
+
finds a compatible interpreter automatically; if you only have 3.14+, it tells
|
|
65
|
+
you how to get a supported one.
|
|
66
|
+
|
|
67
|
+
## Install
|
|
68
|
+
|
|
69
|
+
The easiest way, on a machine with Python 3.9-3.13:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pipx install torrdown # isolated, puts `torrdown` on your PATH
|
|
73
|
+
# or
|
|
74
|
+
pip install torrdown
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
That's it — `torrdown` is now a command you can run from anywhere.
|
|
78
|
+
|
|
79
|
+
### From source
|
|
80
|
+
|
|
81
|
+
The source repo is currently private. If you have access, you can clone it and
|
|
82
|
+
use the bundled installer (creates an isolated venv and symlinks the command):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
git clone https://github.com/aryannxroot/TorrDown.git
|
|
86
|
+
cd TorrDown
|
|
87
|
+
./install.sh # then ./update.sh / ./uninstall.sh to manage it
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For development:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
python3.13 -m venv torr
|
|
94
|
+
source torr/bin/activate
|
|
95
|
+
pip install -e ".[dev]" # editable install + test deps
|
|
96
|
+
pytest # run the test suite
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
You can also run it without installing via `python -m torrdown`.
|
|
100
|
+
|
|
101
|
+
## Usage
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
torrdown # interactive menu (list files / paste magnet / batch)
|
|
105
|
+
torrdown add "magnet:?xt=..." # add a magnet (quote it - the & chars matter)
|
|
106
|
+
torrdown add movie.torrent # add a local .torrent file
|
|
107
|
+
torrdown add 1,3 # add local .torrent files by menu index
|
|
108
|
+
torrdown batch # download every magnet in the configured magnets.txt
|
|
109
|
+
torrdown batch links.txt # ...or from a specific file
|
|
110
|
+
torrdown resume # resume downloads interrupted by a crash/Ctrl+C
|
|
111
|
+
torrdown status # show tracked torrents with progress (ID, %, size)
|
|
112
|
+
torrdown remove <id> # drop a tracked entry by ID (from status) or label
|
|
113
|
+
torrdown clear # clear completed/failed entries (--all clears everything)
|
|
114
|
+
torrdown list # list local .torrent files
|
|
115
|
+
torrdown init # create the config/download/torrents directories
|
|
116
|
+
torrdown config # show resolved config and file locations
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`status` shows how far each download got; `remove`/`clear` only edit the tracking
|
|
120
|
+
records — your downloaded files are never touched. For live speed on an active
|
|
121
|
+
download, use `torrdown resume` (it shows the running progress bars).
|
|
122
|
+
|
|
123
|
+
Downloads and interrupted work resume automatically on the next run.
|
|
124
|
+
|
|
125
|
+
## Configuration
|
|
126
|
+
|
|
127
|
+
Defaults (all overridable):
|
|
128
|
+
|
|
129
|
+
| Setting | Default |
|
|
130
|
+
|---|---|
|
|
131
|
+
| Download directory | `~/Downloads/torrdown` |
|
|
132
|
+
| Torrents directory (`.torrent` files for the menu / `list`) | `~/Downloads/torrdown/torrents` |
|
|
133
|
+
| Config / state directory | `~/.config/torrdown` (`%APPDATA%\torrdown` on Windows) |
|
|
134
|
+
| Listen port | 6881 |
|
|
135
|
+
| Max concurrent (metadata slots) | 3 |
|
|
136
|
+
| Metadata fetch timeout | 180s |
|
|
137
|
+
|
|
138
|
+
Drop `.torrent` files into the torrents directory to have them show up in the
|
|
139
|
+
interactive menu and `torrdown list`. Magnet links don't need any folder — just
|
|
140
|
+
`torrdown add "magnet:?..."`.
|
|
141
|
+
|
|
142
|
+
Precedence, highest first: **CLI flags** (`--download-dir`, `--config-dir`) >
|
|
143
|
+
**environment** (`TORRDOWN_DOWNLOAD_DIR`, `TORRDOWN_PORT`, ...) >
|
|
144
|
+
**`config.json`** (in the config dir) > **built-in defaults**.
|
|
145
|
+
|
|
146
|
+
Run `torrdown config` to see the resolved values and where the files live.
|
|
147
|
+
|
|
148
|
+
## How it works
|
|
149
|
+
|
|
150
|
+
- **`engine.py`** owns one `libtorrent` session and runs a single alert-driven
|
|
151
|
+
loop on the main thread: it adds torrents, watches libtorrent alerts
|
|
152
|
+
(metadata received, finished, error, resume-data saved), and polls each
|
|
153
|
+
torrent's status for the progress bars. Per-torrent status is read one handle
|
|
154
|
+
at a time on purpose — libtorrent's batched `get_torrent_status()` invokes a
|
|
155
|
+
Python callback from an internal worker thread and segfaults in this build.
|
|
156
|
+
- **`state.py`** persists a small `state.json` keyed by info-hash, including
|
|
157
|
+
base64-encoded libtorrent resume data, so interrupted torrents resume without
|
|
158
|
+
re-fetching metadata or re-hashing completed pieces. Writes are atomic and a
|
|
159
|
+
corrupt file is set aside rather than silently wiped.
|
|
160
|
+
- **`sources.py`** turns a user token (a magnet, a `.torrent` path, or a menu
|
|
161
|
+
index) into a concrete download source. This is the single seam where future
|
|
162
|
+
fetch adapters (e.g. searching a legitimate index) will plug in.
|
|
163
|
+
- **`config.py`** resolves paths and tunables across the precedence layers above.
|
|
164
|
+
- **`cli.py`** is the argparse front end (the subcommands above) plus the
|
|
165
|
+
interactive menu when run bare.
|
|
166
|
+
|
|
167
|
+
## Project layout
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
torrdown/ the package
|
|
171
|
+
cli.py argparse subcommands + interactive menu
|
|
172
|
+
config.py path + settings resolution
|
|
173
|
+
sources.py token -> download source (future fetch seam)
|
|
174
|
+
engine.py the shared-session libtorrent engine
|
|
175
|
+
state.py resume-state persistence
|
|
176
|
+
magnets.py batch magnets file read + archive
|
|
177
|
+
tests/ pure-logic pytest suite
|
|
178
|
+
install.sh installer (macOS + Linux)
|
|
179
|
+
uninstall.sh uninstaller
|
|
180
|
+
update.sh updater
|
|
181
|
+
pyproject.toml packaging + dependencies
|
|
182
|
+
PLAN.md what we're building
|
|
183
|
+
PROGRESS.md living status tracker
|
|
184
|
+
ROADMAP.md deferred work (auto-fetch, PyPI, perf notes)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Notes & limitations
|
|
188
|
+
|
|
189
|
+
- **Speed** is bounded by your internet connection and the swarm, not by
|
|
190
|
+
TorrDown — see [ROADMAP.md](ROADMAP.md) for the full performance discussion.
|
|
191
|
+
- **Automated fetching** of torrents from external sources isn't built yet;
|
|
192
|
+
when it is, it will target only legitimate sources (Internet Archive, Linux
|
|
193
|
+
distros, Creative Commons). See [ROADMAP.md](ROADMAP.md).
|
|
194
|
+
- **Windows** is not supported yet.
|
torrdown-0.1.0/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# TorrDown
|
|
2
|
+
|
|
3
|
+
A command-line torrent downloader built on a single shared `libtorrent` session.
|
|
4
|
+
It downloads several torrents at once, survives restarts, and installs as a real
|
|
5
|
+
`torrdown` command you can run from anywhere.
|
|
6
|
+
|
|
7
|
+
TorrDown started as a Google Colab notebook (still in the repo as
|
|
8
|
+
`legacy_download_gcollab.ipynb`) and grew into a proper, installable CLI.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **Single shared libtorrent session** — all torrents run in one session (the way
|
|
13
|
+
libtorrent is meant to be used), not one session per torrent.
|
|
14
|
+
- **Concurrent downloads** with a fairness gate — only a bounded number compete
|
|
15
|
+
for metadata/peers at once; the rest queue, so a stuck magnet can't starve the
|
|
16
|
+
others or unfairly burn its own timeout.
|
|
17
|
+
- **Magnet links and `.torrent` files**, individually or in batches.
|
|
18
|
+
- **Automatic resume** — Ctrl+C, a crash, or a reboot doesn't lose progress;
|
|
19
|
+
downloads pick up where they left off on the next run (libtorrent resume data
|
|
20
|
+
is checkpointed to disk).
|
|
21
|
+
- **Info-hash dedup** — the same torrent won't be added twice.
|
|
22
|
+
- **Metadata-fetch timeout** — a magnet with no metadata-holding peers is skipped
|
|
23
|
+
after a while instead of hanging forever.
|
|
24
|
+
- **Batch mode** — download every magnet in a file; completed/failed entries are
|
|
25
|
+
commented out in place so re-runs don't repeat them.
|
|
26
|
+
- **Graceful shutdown** — Ctrl+C saves state cleanly before exiting.
|
|
27
|
+
- **Live progress bars** (via `rich`) for every active download.
|
|
28
|
+
- **Per-user config** with sensible defaults and clear override precedence.
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- **macOS or Linux.** (Windows isn't supported yet.)
|
|
33
|
+
- **Python 3.9-3.13** somewhere on the machine. `libtorrent` publishes binary
|
|
34
|
+
wheels only for these versions, so TorrDown caps at `<3.14`. The installer
|
|
35
|
+
finds a compatible interpreter automatically; if you only have 3.14+, it tells
|
|
36
|
+
you how to get a supported one.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
The easiest way, on a machine with Python 3.9-3.13:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pipx install torrdown # isolated, puts `torrdown` on your PATH
|
|
44
|
+
# or
|
|
45
|
+
pip install torrdown
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
That's it — `torrdown` is now a command you can run from anywhere.
|
|
49
|
+
|
|
50
|
+
### From source
|
|
51
|
+
|
|
52
|
+
The source repo is currently private. If you have access, you can clone it and
|
|
53
|
+
use the bundled installer (creates an isolated venv and symlinks the command):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
git clone https://github.com/aryannxroot/TorrDown.git
|
|
57
|
+
cd TorrDown
|
|
58
|
+
./install.sh # then ./update.sh / ./uninstall.sh to manage it
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For development:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python3.13 -m venv torr
|
|
65
|
+
source torr/bin/activate
|
|
66
|
+
pip install -e ".[dev]" # editable install + test deps
|
|
67
|
+
pytest # run the test suite
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
You can also run it without installing via `python -m torrdown`.
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
torrdown # interactive menu (list files / paste magnet / batch)
|
|
76
|
+
torrdown add "magnet:?xt=..." # add a magnet (quote it - the & chars matter)
|
|
77
|
+
torrdown add movie.torrent # add a local .torrent file
|
|
78
|
+
torrdown add 1,3 # add local .torrent files by menu index
|
|
79
|
+
torrdown batch # download every magnet in the configured magnets.txt
|
|
80
|
+
torrdown batch links.txt # ...or from a specific file
|
|
81
|
+
torrdown resume # resume downloads interrupted by a crash/Ctrl+C
|
|
82
|
+
torrdown status # show tracked torrents with progress (ID, %, size)
|
|
83
|
+
torrdown remove <id> # drop a tracked entry by ID (from status) or label
|
|
84
|
+
torrdown clear # clear completed/failed entries (--all clears everything)
|
|
85
|
+
torrdown list # list local .torrent files
|
|
86
|
+
torrdown init # create the config/download/torrents directories
|
|
87
|
+
torrdown config # show resolved config and file locations
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`status` shows how far each download got; `remove`/`clear` only edit the tracking
|
|
91
|
+
records — your downloaded files are never touched. For live speed on an active
|
|
92
|
+
download, use `torrdown resume` (it shows the running progress bars).
|
|
93
|
+
|
|
94
|
+
Downloads and interrupted work resume automatically on the next run.
|
|
95
|
+
|
|
96
|
+
## Configuration
|
|
97
|
+
|
|
98
|
+
Defaults (all overridable):
|
|
99
|
+
|
|
100
|
+
| Setting | Default |
|
|
101
|
+
|---|---|
|
|
102
|
+
| Download directory | `~/Downloads/torrdown` |
|
|
103
|
+
| Torrents directory (`.torrent` files for the menu / `list`) | `~/Downloads/torrdown/torrents` |
|
|
104
|
+
| Config / state directory | `~/.config/torrdown` (`%APPDATA%\torrdown` on Windows) |
|
|
105
|
+
| Listen port | 6881 |
|
|
106
|
+
| Max concurrent (metadata slots) | 3 |
|
|
107
|
+
| Metadata fetch timeout | 180s |
|
|
108
|
+
|
|
109
|
+
Drop `.torrent` files into the torrents directory to have them show up in the
|
|
110
|
+
interactive menu and `torrdown list`. Magnet links don't need any folder — just
|
|
111
|
+
`torrdown add "magnet:?..."`.
|
|
112
|
+
|
|
113
|
+
Precedence, highest first: **CLI flags** (`--download-dir`, `--config-dir`) >
|
|
114
|
+
**environment** (`TORRDOWN_DOWNLOAD_DIR`, `TORRDOWN_PORT`, ...) >
|
|
115
|
+
**`config.json`** (in the config dir) > **built-in defaults**.
|
|
116
|
+
|
|
117
|
+
Run `torrdown config` to see the resolved values and where the files live.
|
|
118
|
+
|
|
119
|
+
## How it works
|
|
120
|
+
|
|
121
|
+
- **`engine.py`** owns one `libtorrent` session and runs a single alert-driven
|
|
122
|
+
loop on the main thread: it adds torrents, watches libtorrent alerts
|
|
123
|
+
(metadata received, finished, error, resume-data saved), and polls each
|
|
124
|
+
torrent's status for the progress bars. Per-torrent status is read one handle
|
|
125
|
+
at a time on purpose — libtorrent's batched `get_torrent_status()` invokes a
|
|
126
|
+
Python callback from an internal worker thread and segfaults in this build.
|
|
127
|
+
- **`state.py`** persists a small `state.json` keyed by info-hash, including
|
|
128
|
+
base64-encoded libtorrent resume data, so interrupted torrents resume without
|
|
129
|
+
re-fetching metadata or re-hashing completed pieces. Writes are atomic and a
|
|
130
|
+
corrupt file is set aside rather than silently wiped.
|
|
131
|
+
- **`sources.py`** turns a user token (a magnet, a `.torrent` path, or a menu
|
|
132
|
+
index) into a concrete download source. This is the single seam where future
|
|
133
|
+
fetch adapters (e.g. searching a legitimate index) will plug in.
|
|
134
|
+
- **`config.py`** resolves paths and tunables across the precedence layers above.
|
|
135
|
+
- **`cli.py`** is the argparse front end (the subcommands above) plus the
|
|
136
|
+
interactive menu when run bare.
|
|
137
|
+
|
|
138
|
+
## Project layout
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
torrdown/ the package
|
|
142
|
+
cli.py argparse subcommands + interactive menu
|
|
143
|
+
config.py path + settings resolution
|
|
144
|
+
sources.py token -> download source (future fetch seam)
|
|
145
|
+
engine.py the shared-session libtorrent engine
|
|
146
|
+
state.py resume-state persistence
|
|
147
|
+
magnets.py batch magnets file read + archive
|
|
148
|
+
tests/ pure-logic pytest suite
|
|
149
|
+
install.sh installer (macOS + Linux)
|
|
150
|
+
uninstall.sh uninstaller
|
|
151
|
+
update.sh updater
|
|
152
|
+
pyproject.toml packaging + dependencies
|
|
153
|
+
PLAN.md what we're building
|
|
154
|
+
PROGRESS.md living status tracker
|
|
155
|
+
ROADMAP.md deferred work (auto-fetch, PyPI, perf notes)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Notes & limitations
|
|
159
|
+
|
|
160
|
+
- **Speed** is bounded by your internet connection and the swarm, not by
|
|
161
|
+
TorrDown — see [ROADMAP.md](ROADMAP.md) for the full performance discussion.
|
|
162
|
+
- **Automated fetching** of torrents from external sources isn't built yet;
|
|
163
|
+
when it is, it will target only legitimate sources (Internet Archive, Linux
|
|
164
|
+
distros, Creative Commons). See [ROADMAP.md](ROADMAP.md).
|
|
165
|
+
- **Windows** is not supported yet.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Roadmap / deferred work
|
|
2
|
+
|
|
3
|
+
Things intentionally left for later, with the reasoning, so picking them up
|
|
4
|
+
doesn't mean re-deriving the context. Current status lives in
|
|
5
|
+
[PROGRESS.md](PROGRESS.md); the CLI plan is in [PLAN.md](PLAN.md).
|
|
6
|
+
|
|
7
|
+
## Deferred: auto-fetch torrents from external sources
|
|
8
|
+
|
|
9
|
+
Today sources are supplied manually (magnet link, `.torrent` file, or file
|
|
10
|
+
index). The next big feature is a `search` subcommand that fetches magnets from
|
|
11
|
+
**legitimate** sources and hands them to the existing engine.
|
|
12
|
+
|
|
13
|
+
- **Where it plugs in**: `torrdown/sources.py` already centralizes "token →
|
|
14
|
+
`[(source, label)]`" resolution. A fetch adapter resolves a `search:<query>`
|
|
15
|
+
token (or a dedicated `torrdown search` subcommand) into magnet sources - no
|
|
16
|
+
change to `engine.py` or the download path is needed.
|
|
17
|
+
- **Sources to target** (legal/legitimately-distributable only):
|
|
18
|
+
- **Internet Archive** (archive.org) - the standout: millions of public-domain
|
|
19
|
+
and openly-licensed items, every item exposes a torrent, and it has a real
|
|
20
|
+
documented search API. Best fit for a clean search→download flow.
|
|
21
|
+
- **Linux distribution torrents** - Ubuntu/Debian/Fedora/Arch official ISOs;
|
|
22
|
+
stable URLs, no unified API.
|
|
23
|
+
- **Creative Commons / open media** - Blender open movies, Librivox, Wikimedia.
|
|
24
|
+
- **Explicitly out of scope**: general piracy index sites (1337x, YTS, TPB, RARBG
|
|
25
|
+
successors, etc.). This tool will not integrate scraping/searching those.
|
|
26
|
+
- **Why deferred**: it's a substantially larger task (HTTP clients, per-source
|
|
27
|
+
adapters, result ranking/selection UX) and was split off deliberately to ship
|
|
28
|
+
the CLI first without losing momentum.
|
|
29
|
+
|
|
30
|
+
## Done: publish to PyPI (pipx install)
|
|
31
|
+
|
|
32
|
+
Published to PyPI, so `pipx install torrdown` / `pip install torrdown` works for
|
|
33
|
+
anyone without the repo being public. Remaining optional follow-ups:
|
|
34
|
+
- Make the repo public and add `[project.urls]` (Homepage/Repository/Issues) so the
|
|
35
|
+
PyPI page links back to real source (currently omitted — the repo is private).
|
|
36
|
+
- Automate releases via a GitHub Actions workflow (build + `twine upload` on tag).
|
|
37
|
+
- Revisit Windows support.
|
|
38
|
+
|
|
39
|
+
## Deferred: broader hardening
|
|
40
|
+
|
|
41
|
+
- Richer `status` output (live dashboard rather than a snapshot table).
|
|
42
|
+
- Full engine/network **integration tests** (the current suite covers pure logic
|
|
43
|
+
only; end-to-end download tests need real torrents/network).
|
|
44
|
+
- Rotating-file **logging** for diagnostics, alongside the current console UX.
|
|
45
|
+
|
|
46
|
+
## Performance tuning (revisit only if evidence changes)
|
|
47
|
+
|
|
48
|
+
From an earlier investigation into why downloads cap around ~4 MB/s.
|
|
49
|
+
|
|
50
|
+
**Diagnosis**: measured home connection was **35.75 Mbps down / 36.62 Mbps up**
|
|
51
|
+
(≈4.47 MB/s ceiling); ~4 MB/s is ~89% of that - i.e. already near the physical
|
|
52
|
+
line limit, not a code/settings problem. The old Google Colab notebook's
|
|
53
|
+
10-40 MB/s came from the swarm transfer happening on Google Cloud's network (to a
|
|
54
|
+
Colab VM / Google Drive), which never touched the home connection - so that number
|
|
55
|
+
reflects Google's bandwidth, not this client or connection.
|
|
56
|
+
|
|
57
|
+
**Settings checked (libtorrent 2.0.13, live introspection) - no hidden slack**:
|
|
58
|
+
rate limits already unlimited; `enable_upnp`/`enable_natpmp`/`enable_lsd` already
|
|
59
|
+
on; TCP and uTP both enabled; `connections_limit` 200 (generous for personal use).
|
|
60
|
+
|
|
61
|
+
**Candidate tweaks, not applied - each with its trade-off**:
|
|
62
|
+
|
|
63
|
+
1. **Raise `connections_limit`** (200 → ~400): free, harmless, but unlikely to
|
|
64
|
+
matter - a single well-seeded torrent rarely has 200+ usable peers anyway.
|
|
65
|
+
2. **Force encryption** (`out_enc_policy`/`in_enc_policy`: `pe_enabled` →
|
|
66
|
+
`pe_forced`, `allowed_enc_level` → `pe_rc4`): some ISPs throttle recognizable
|
|
67
|
+
plaintext BitTorrent handshakes via DPI; forcing encrypted-only can sometimes
|
|
68
|
+
evade that. **Cost**: rejects peers that don't support obfuscation, shrinking
|
|
69
|
+
the usable pool. Not justified now (already ~89% of line speed = no clear
|
|
70
|
+
throttling). Also note this is legacy RC4 traffic-obfuscation, not real
|
|
71
|
+
security.
|
|
72
|
+
3. **`mixed_mode_algorithm` → `prefer_tcp`**: *not recommended* - lets torrenting
|
|
73
|
+
monopolize the connection at the expense of other traffic (calls, browsing),
|
|
74
|
+
for a gain unlikely to matter since the bottleneck is total bandwidth.
|
|
75
|
+
|
|
76
|
+
**When to revisit**: the internet plan/ISP changes or the line gets materially
|
|
77
|
+
faster; or torrent speeds start showing up *consistently well below* a fresh
|
|
78
|
+
speedtest number (that pattern - not the current one - indicates real throttling
|
|
79
|
+
worth evading via forced encryption).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "torrdown"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "A single-session libtorrent download CLI with resume, dedup, and batch support."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9,<3.14"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "aryannxroot" }]
|
|
13
|
+
keywords = ["torrent", "bittorrent", "libtorrent", "cli", "downloader"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: End Users/Desktop",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: MacOS",
|
|
20
|
+
"Operating System :: POSIX :: Linux",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Internet",
|
|
28
|
+
"Topic :: Communications :: File Sharing",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
# libtorrent ships binary wheels only for CPython 3.9-3.13, which is why
|
|
32
|
+
# requires-python caps at <3.14 (there is no 3.14 wheel).
|
|
33
|
+
"libtorrent>=2.0,<2.1",
|
|
34
|
+
"rich>=13",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
torrdown = "torrdown.cli:main"
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
dev = ["pytest>=7"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.version]
|
|
44
|
+
path = "torrdown/__init__.py"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["torrdown"]
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.sdist]
|
|
50
|
+
# Keep the source distribution to just what belongs in a release - not the
|
|
51
|
+
# legacy Colab notebook or any local working files.
|
|
52
|
+
include = [
|
|
53
|
+
"torrdown",
|
|
54
|
+
"tests",
|
|
55
|
+
"README.md",
|
|
56
|
+
"ROADMAP.md",
|
|
57
|
+
"LICENSE",
|
|
58
|
+
"pyproject.toml",
|
|
59
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from torrdown.config import Config
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_defaults_when_no_file(tmp_path):
|
|
8
|
+
cfg = Config(config_dir=tmp_path)
|
|
9
|
+
d = cfg.as_dict()
|
|
10
|
+
assert d["port"] == 6881
|
|
11
|
+
assert d["max_concurrent"] == 3
|
|
12
|
+
assert d["download_dir"].endswith("Downloads/torrdown") or "torrdown" in d["download_dir"]
|
|
13
|
+
# torrents_dir defaults to a dedicated subfolder of download_dir
|
|
14
|
+
assert d["torrents_dir"] == str(Path(d["download_dir"]) / "torrents")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_config_file_overrides_defaults(tmp_path):
|
|
18
|
+
(tmp_path / "config.json").write_text(json.dumps({"port": 7000, "max_concurrent": 9}))
|
|
19
|
+
cfg = Config(config_dir=tmp_path)
|
|
20
|
+
d = cfg.as_dict()
|
|
21
|
+
assert d["port"] == 7000
|
|
22
|
+
assert d["max_concurrent"] == 9
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_env_overrides_file(tmp_path, monkeypatch):
|
|
26
|
+
(tmp_path / "config.json").write_text(json.dumps({"port": 7000}))
|
|
27
|
+
monkeypatch.setenv("TORRDOWN_PORT", "8123")
|
|
28
|
+
cfg = Config(config_dir=tmp_path)
|
|
29
|
+
assert cfg.as_dict()["port"] == 8123
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_cli_override_beats_env_and_file(tmp_path, monkeypatch):
|
|
33
|
+
(tmp_path / "config.json").write_text(json.dumps({"download_dir": "/from/file"}))
|
|
34
|
+
monkeypatch.setenv("TORRDOWN_DOWNLOAD_DIR", "/from/env")
|
|
35
|
+
cfg = Config(config_dir=tmp_path, overrides={"download_dir": "/from/cli"})
|
|
36
|
+
assert cfg.as_dict()["download_dir"] == "/from/cli"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_none_override_is_ignored(tmp_path):
|
|
40
|
+
# argparse passes None for unspecified flags - must not clobber lower layers.
|
|
41
|
+
(tmp_path / "config.json").write_text(json.dumps({"port": 7001}))
|
|
42
|
+
cfg = Config(config_dir=tmp_path, overrides={"download_dir": None})
|
|
43
|
+
assert cfg.as_dict()["port"] == 7001
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_corrupt_config_falls_back_to_defaults(tmp_path, capsys):
|
|
47
|
+
(tmp_path / "config.json").write_text("{ not json !!!")
|
|
48
|
+
cfg = Config(config_dir=tmp_path)
|
|
49
|
+
assert cfg.as_dict()["port"] == 6881 # default, no crash
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_ensure_config_dir_writes_default(tmp_path):
|
|
53
|
+
cfg = Config(config_dir=tmp_path / "new")
|
|
54
|
+
cfg.ensure_config_dir()
|
|
55
|
+
assert cfg.config_file.exists()
|
|
56
|
+
data = json.loads(cfg.config_file.read_text())
|
|
57
|
+
assert "download_dir" in data
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from torrdown.magnets import read_magnets_file, archive_results
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_read_skips_comments_and_blanks(tmp_path):
|
|
5
|
+
f = tmp_path / "magnets.txt"
|
|
6
|
+
f.write_text(
|
|
7
|
+
"# a comment\n"
|
|
8
|
+
"\n"
|
|
9
|
+
"magnet:?xt=urn:btih:AAA&dn=one\n"
|
|
10
|
+
" \n"
|
|
11
|
+
"magnet:?xt=urn:btih:BBB&dn=two\n"
|
|
12
|
+
)
|
|
13
|
+
assert read_magnets_file(str(f)) == [
|
|
14
|
+
"magnet:?xt=urn:btih:AAA&dn=one",
|
|
15
|
+
"magnet:?xt=urn:btih:BBB&dn=two",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_read_missing_file_returns_empty(tmp_path):
|
|
20
|
+
assert read_magnets_file(str(tmp_path / "nope.txt")) == []
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_archive_comments_done_and_failed_leaves_others(tmp_path):
|
|
24
|
+
f = tmp_path / "magnets.txt"
|
|
25
|
+
one = "magnet:?xt=urn:btih:AAA&dn=one"
|
|
26
|
+
two = "magnet:?xt=urn:btih:BBB&dn=two"
|
|
27
|
+
three = "magnet:?xt=urn:btih:CCC&dn=three"
|
|
28
|
+
f.write_text(f"# keep me\n{one}\n{two}\n{three}\n")
|
|
29
|
+
|
|
30
|
+
archive_results(str(f), {
|
|
31
|
+
one: ("done", None),
|
|
32
|
+
two: ("failed", "metadata timeout"),
|
|
33
|
+
# three intentionally left active
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
lines = f.read_text().splitlines()
|
|
37
|
+
assert lines[0] == "# keep me"
|
|
38
|
+
assert lines[1].startswith("# [done ") and one in lines[1]
|
|
39
|
+
assert lines[2].startswith("# [failed ") and "metadata timeout" in lines[2] and two in lines[2]
|
|
40
|
+
assert lines[3] == three # untouched
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_archive_noop_on_empty_results(tmp_path):
|
|
44
|
+
f = tmp_path / "magnets.txt"
|
|
45
|
+
original = "magnet:?xt=urn:btih:AAA&dn=one\n"
|
|
46
|
+
f.write_text(original)
|
|
47
|
+
archive_results(str(f), {})
|
|
48
|
+
assert f.read_text() == original
|