Skip to main content

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 on 429 and 5xx responses.

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 docs or preview with make 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

python_gazelle-1.0.0.tar.gz (27.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

python_gazelle-1.0.0-py3-none-any.whl (36.3 kB view details)

Uploaded Python 3

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

Hashes for python_gazelle-1.0.0.tar.gz
Algorithm Hash digest
SHA256 98fab6a713dbb606bceb82577ae26b370cac98abceb5a598651e99b5d2af4920
MD5 7bcd8b75122e347ea62bc791693ec9b6
BLAKE2b-256 7c4a57c1235e2fbb9e2a312629e3e6e4a641cddd019db9b89fc47c2f3513e095

See more details on using hashes here.

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

Hashes for python_gazelle-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9bf012684e085bd8a45083020bcdb9fd683a75b913d07bddbe03ffd90b679576
MD5 7ef275ea0cf11c9a042915a7e34eefad
BLAKE2b-256 91d92511a3207dc794538c1c23361d4a7c04cae496060f4462b80b03cb408bf0

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page