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.
@@ -0,0 +1,15 @@
1
+ Movies/
2
+ Torrents/
3
+ torr/
4
+ .venv/
5
+ magnets.txt
6
+ state.json
7
+ __pycache__/
8
+ *.pyc
9
+ .DS_Store
10
+
11
+ # build artifacts
12
+ build/
13
+ dist/
14
+ *.egg-info/
15
+ .pytest_cache/
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.
@@ -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.
@@ -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