Client library to interact with gazelle based trackers
Project description
python-gazelle
A typed, async-first Python client library for Gazelle-based music trackers, with built-in support for Orpheus and Redacted (RED).
📖 Full documentation — guides + auto-generated API reference.
- Async-first, built on httpx — with synchronous wrappers if you prefer blocking calls.
- Typed responses via pydantic v2 models.
- API-key or cookie/login authentication.
- Built-in rate limiting (token bucket) and automatic retries on transient errors.
- Per-tracker handling of the quirks that differ between Orpheus and RED (auth header, base URL, required headers).
- Automation helpers: upload/snatch monitoring, cross-seeding, and cross-uploading between trackers.
Status: Beta. The core API client (torrents, artists, user, notifications, requests, collages, inbox) and the automation helpers — upload/snatch monitoring, cross-seed, and cross-upload — are implemented. Note: cross-upload's tracker upload-form fields are best-effort pending verification against the live API.
Installation
uv add python-gazelle # or: pip install python-gazelle
Requires Python 3.11+.
Quick start (async)
import asyncio
from pygazelle import OrpheusClient
async def main():
async with OrpheusClient(api_key="YOUR_API_KEY") as client:
me = await client.user.me()
print(me.username, me.id)
# Search returns group-level results
results = await client.torrents.search("Daft Punk", format="FLAC")
for r in results:
print(f"{r.artist} - {r.group_name} ({r.group_year})")
# Fetch a specific torrent by id, then download its .torrent bytes
for n in await client.notifications.list():
print(n.notification_type, n.group_name)
torrent = await client.torrents.get(n.torrent_id)
print(torrent.format, torrent.size)
data = await client.torrents.download(torrent.id)
break
asyncio.run(main())
RedactedClient has the same interface — just swap the class:
from pygazelle import RedactedClient
async with RedactedClient(api_key="YOUR_RED_API_KEY") as client:
...
Synchronous usage
If you're not in an async context, use the *Sync clients. Every method is the
same, just without await:
from pygazelle import OrpheusClientSync
client = OrpheusClientSync(api_key="YOUR_API_KEY")
try:
me = client.user.me()
results = client.torrents.search("Daft Punk", format="FLAC")
finally:
client.close()
Monitoring your uploads & snatches
Track when your uploaded or snatched torrents disappear (deleted, trumped, or otherwise removed). The monitor is stateless and caller-paced — no background threads or timers:
from pygazelle import OrpheusClient
async with OrpheusClient(api_key="...") as client:
monitor = client.monitor() # watches uploaded + snatched by default
await monitor.poll() # first call establishes the baseline -> []
# ... later (you control the cadence) ...
for event in await monitor.poll():
print(event.kind, event.source, event.torrent_id, event.name)
if event.kind == "trumped":
print("replaced by", event.replacement_torrent_id)
# Persist the baseline across restarts (storage is yours):
state = monitor.dump_state() # json-serializable
# monitor.load_state(state)
client.monitor() is also available on the sync client (OrpheusClientSync).
Cross-seeding a release to another tracker
from pygazelle import OrpheusClient, RedactedClient, cross_seed
async with OrpheusClient(api_key="...") as source, RedactedClient(api_key="...") as target:
result = await cross_seed(source, 12345, target) # source torrent id 12345
if result:
# Same release found on the target tracker; write its .torrent and add to your client.
with open("match.torrent", "wb") as fh:
fh.write(result.torrent_file)
print("matched target torrent", result.target_torrent_id)
else:
print("no exact match on the target tracker")
A synchronous cross_seed_sync(source_sync_client, source_torrent_id, target_sync_client) is also available.
Cross-uploading a release to another tracker
Cross-upload is a deliberate two-step flow: a read-only prepare_upload (maps
metadata, checks the target for duplicates, builds a draft — no write), then an
explicit submit_upload (the live upload). You build the target .torrent
yourself (the library never generates or parses it).
from pygazelle import OrpheusClient, RedactedClient, prepare_upload, submit_upload
async with OrpheusClient(api_key="...") as source, RedactedClient(api_key="...") as target:
# Build a .torrent for the target's announce URL with your own tool, then:
announce = await target.user.announce_url() # e.g. mktorrent --announce <announce> <data>
torrent_bytes = open("for-target.torrent", "rb").read()
draft = await prepare_upload(source, 12345, target, torrent_file=torrent_bytes)
# Review before the live write:
for field in draft.unmapped: # fields the mapper couldn't resolve
draft.form[field] = ... # fill them in
if any(d.kind == "exact" for d in draft.duplicates):
print("already on target; skipping")
else:
result = await submit_upload(target, draft) # the live upload
print("uploaded:", result.torrent_id)
submit_upload refuses if a required field is still missing or an exact
duplicate exists (pass allow_duplicate=True to override). Synchronous
prepare_upload_sync / submit_upload_sync are also available.
Authentication
Pass either an API key or username/password (cookie/login auth):
OrpheusClient(api_key="...") # API-key auth
OrpheusClient(username="user", password="pass") # cookie/login auth
Generate an API key in your tracker's user settings. Note that Orpheus and RED
expect different Authorization header formats — the client handles this for you.
Resources
Each client exposes resource namespaces:
| Namespace | Methods |
|---|---|
client.torrents |
get(id), get_group(id), search(query, **params), download(id), add_tag(group_id, tags), add_log(torrent_id, logfiles) |
client.artists |
get(id), search(name), similar(id, limit=None) |
client.user |
me(), get(id), search(query, **params), torrents(id, type, ...) |
client.notifications |
list(**params) |
client.bookmarks |
torrents(), artists() |
client.subscriptions |
list() |
client.site |
top10(type, limit), announcements() |
client.requests |
get(id), search(query, **params), fill(request_id, torrent_id=…) |
client.collages, client.inbox |
see source |
**params are passed through to the underlying Gazelle ajax.php action
(e.g. format="FLAC", page=1).
Rate limiting & retries
Both are on by default and configurable per client:
OrpheusClient(api_key="...", rate=3.0, max_retries=3)
rate— requests per second (token bucket).max_retries— retries with exponential backoff on429and5xxresponses.
Error handling
All exceptions derive from GazelleError:
from pygazelle import (
GazelleError, GazelleAuthError, GazelleRateLimitError,
GazelleNotFoundError, GazelleAPIError,
)
try:
torrent = await client.torrents.get(123)
except GazelleNotFoundError:
... # 404 / unknown id
except GazelleAuthError:
... # bad credentials / 401 / 403
except GazelleRateLimitError:
... # 429 after retries exhausted
except GazelleAPIError as e:
... # other API failure (e.status_code, str(e))
Development
See CLAUDE.md for the architecture overview, conventions, and how to
run the test suite (including .env setup and capturing API fixtures for the
model tests).
Project Docs
The full docs site (user guides + auto-generated API reference) is published at https://technophile-musicfan.github.io/python-gazelle/. It's built with MkDocs
- Material + mkdocstrings; build it locally with
make docsor preview withmake docs-serve.
For how to install uv and Python, see installation.md.
For development workflows, see development.md.
For instructions on publishing to PyPI, see publishing.md.
This project was built from simple-modern-uv.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file python_gazelle-1.0.0.tar.gz.
File metadata
- Download URL: python_gazelle-1.0.0.tar.gz
- Upload date:
- Size: 27.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
98fab6a713dbb606bceb82577ae26b370cac98abceb5a598651e99b5d2af4920
|
|
| MD5 |
7bcd8b75122e347ea62bc791693ec9b6
|
|
| BLAKE2b-256 |
7c4a57c1235e2fbb9e2a312629e3e6e4a641cddd019db9b89fc47c2f3513e095
|
File details
Details for the file python_gazelle-1.0.0-py3-none-any.whl.
File metadata
- Download URL: python_gazelle-1.0.0-py3-none-any.whl
- Upload date:
- Size: 36.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9bf012684e085bd8a45083020bcdb9fd683a75b913d07bddbe03ffd90b679576
|
|
| MD5 |
7ef275ea0cf11c9a042915a7e34eefad
|
|
| BLAKE2b-256 |
91d92511a3207dc794538c1c23361d4a7c04cae496060f4462b80b03cb408bf0
|