Skip to content

Repository files navigation

magnet

中文文档

A multi-provider torrent search CLI written in Rust, with a reusable library. Supports concurrent search, BTIH infohash dedup, tracker merging, and JSON output suited for scripts and agents.

Releases are published on crates.io. Built-in providers are Nyaa, Knaben, Sukebei (Nyaa NSFW), Bitsearch, BtGoogle, APIBay, DMHY, Anime Garden, and Mikan Project, plus configurable Torznab and RSS/Atom. Hand results off to any BitTorrent client; this project has no downloader, TUI, or MCP server.

Install & Quick Start

Install from crates.io (the command is magnet):

cargo install magnet-cli --locked
magnet --version
magnet search "ubuntu" --json

Or install from source:

git clone https://github.com/wustites/magnet-cli.git
cd magnet-cli
cargo install --path . --locked

The installed command is magnet. If your shell can't find it, make sure Cargo's bin directory is on PATH (usually ~/.cargo/bin).

Or just build and run inside the repo:

cargo build --release --locked
./target/release/magnet search "ubuntu"

Common Commands

# Query default-search sources, top 20 by seeders descending
magnet search "ubuntu"

# Pick sources; comma-separated or repeated --source
magnet search "ubuntu" --source knaben --json
magnet search "ubuntu" --source nyaa,knaben --jsonl

# One magnet per line, good for piping into scripts
magnet search "ubuntu" --magnet

# Filtering, sorting, result count
magnet search "ubuntu" --min-seeds 10 --min-size 1G --max-size 10G --sort seeds --limit 20

# Read back the previous search; IDs start at 1
magnet get 1
magnet get 1 --json

# List configured providers without searching
magnet providers --json

# Build a magnet from an infohash locally; the all-zero hash is demo-only
magnet resolve 0000000000000000000000000000000000000000 --name "Example" --json

resolve accepts a 40-char hex or 32-char base32 BTIH, plus repeatable --tracker URL. It only validates the hash and constructs a magnet: no DHT lookup, no detail-page fetch, no torrent metadata. get --json and resolve --json print a single object; providers --json prints an array with name, kind, default_search.

See magnet --help or magnet search --help for full command help.

Search Options

Option Default Description
QUERY required Non-empty search term; quote it when it contains spaces
--source NAME sources with default_search = true Select by provider name; unknown names exit 3
--json off Print a JSON array
--jsonl off One JSON object per line, emitted after aggregation
--magnet off One magnet per line, skipping results without one
--min-seeds N unlimited Seeders lower bound, inclusive
--min-size SIZE unlimited Size lower bound, inclusive
--max-size SIZE unlimited Size upper bound, inclusive
--sort seeds|size|date|title seeds First three descending, unknowns last; title ascending
--limit N 20 Cap on final aggregated results, must be > 0
--timeout SECONDS 15 Per-provider timeout, 1–300 seconds
--concurrency N 8 Providers searched concurrently, 1–64
--deadline SECONDS none Total search deadline, including queued providers, 1–3600 seconds
--pages N 1 Pages requested from providers with pagination support, 1–20
--no-title-filter off Keep results whose title omits a query term

--json, --jsonl, --magnet are mutually exclusive. Without them you get a table; unknown seeders, size, and date show as ?, sizes render as decimal GB, AGE counts days.

Sizes accept raw bytes, K/M/G/T, KB/MB/GB/TB (decimal), or KiB/MiB/GiB/TiB (binary), e.g. 10G, 1.5GiB. Results with unknown fields are excluded once a size or seeders filter is set. Filtering runs after dedup; --limit applies after sorting.

Global Options & Environment Variables

Option Env var Effect
--config PATH MAGNET_CONFIG Use a TOML config file
--cache PATH MAGNET_CACHE Use a snapshot file, handy for isolated sessions

CLI flags win over the matching env vars. Global options can go before or after the subcommand. The program never auto-reads ./config.toml; pass a path explicitly or set MAGNET_CONFIG.

Configuring Providers

With no config, Nyaa RSS, Knaben JSON, Sukebei RSS, Bitsearch, and BtGoogle are queried concurrently. An explicit config replaces the default provider list, it is not appended to it. Nine providers are built in; those five take part by default and APIBay, DMHY, Anime Garden, and Mikan are opt-in via --source.

Start from config.example.toml:

cp config.example.toml config.toml
magnet --config config.toml providers --json
magnet --config config.toml search "ubuntu" --json

Each [[providers]] entry needs name, kind, and an HTTP(S) url. name must be non-empty, unique, and ASCII letters, digits, hyphens, or underscores only. At least one source is required; unknown config fields are rejected. api_key_env is accepted only for Torznab and Bitsearch and must be a valid environment variable name. default_search defaults to true, so write default_search = false explicitly for any source you want excluded from default aggregate searches.

kind Request Search behavior
nyaa HTTP GET RSS Adds page=rss and query param q
sukebei HTTP GET RSS Nyaa RSS protocol, adds page=rss and q
knaben HTTP POST JSON Title search against the configured API URL, 150 rows per request
apibay HTTP GET JSON Searches APIBay across all categories; returns up to the API's fixed result set. For non-ASCII queries, the CLI filters APIBay fallback results locally by title
bitsearch HTTP GET JSON Searches Bitsearch, 100 rows per page; optional API key header
dmhy HTTP GET RSS DMHY keyword RSS search; opt in with --source dmhy
animegarden HTTP POST JSON Anime Garden full-text anime search; opt in with --source animegarden
mikan HTTP GET RSS Mikan Project keyword RSS search; opt in with --source mikan
btgoogle HTTP GET HTML BtGoogle search results; enabled in default aggregate searches
torznab HTTP GET RSS/XML Adds t=search, q, extended=1, optional API key
rss HTTP GET RSS/Atom Fetches the URL as-is, filters by title locally; the query is split on whitespace, case-insensitive, every word must match

--pages uses Knaben's from offset, Bitsearch's page, and Torznab's offset/limit. APIBay, Nyaa/Sukebei RSS, and configured RSS/Atom URLs are fetched once because they do not expose a reliable compatible pagination mechanism. Pagination stops early when a provider returns an empty/final page; the per-provider timeout covers all requested pages.

API Bay is archived from default aggregate searches because it pads queries it cannot match with popular rows. Title filtering removes those rows for every provider, but a source that ranks by seeders will still surface its loosest matches first. Select API Bay explicitly with --source apibay when needed; DMHY, Anime Garden, and Mikan are also opt-in sources.

DMHY is available as an opt-in source with --source dmhy; it uses DMHY's public keyword RSS endpoint and is disabled in default aggregated searches.

Anime Garden is available as an opt-in source with --source animegarden; it uses the project's public JSON API and is disabled in default aggregated searches.

Mikan Project is available as an opt-in source with --source mikan; it uses the site's keyword RSS search endpoint and is disabled in default aggregated searches.

BtGoogle uses the site's search-results endpoint and participates in default aggregate searches.

Bitsearch's anonymous tier currently allows 200 requests per IP per day. To use an account key, set api_key_env on that provider; the key is sent in the x-api-key header and never included in diagnostics.

Sukebei: Nyaa NSFW

magnet search "keyword" --source sukebei --json
magnet search "keyword" --source nyaa,sukebei --jsonl

Sukebei lives at https://sukebei.nyaa.si/. A custom config can add:

[[providers]]
name = "sukebei"
kind = "sukebei"
url = "https://sukebei.nyaa.si/"
default_search = true

Every provider accepts optional default_search (defaults to true). With false it still shows in providers but only searches when picked via --source. The built-in Sukebei and the example config set it to true, joining the default aggregated search; flip it to false to sit out. With no default-searchable sources you must pass --source, otherwise exit 3.

Torznab: Jackett / Prowlarr

See the Prowlarr integration guide for the local setup, Caddy reverse proxy, API key, and current test status. This project recommends http://127.0.0.1:9696/3/api on the same host.

The bitmagnet integration guide and its backup and restore guide document a self-hosted DHT indexer setup used during early development. Both are archived (2026-09-21) and no longer maintained; the guides remain readable for reference.

Add the entry below to your config, or uncomment the matching entry in the example file. Replace url with the full Torznab API endpoint your service gives you, not the service homepage:

[[providers]]
name = "local"
kind = "torznab"
url = "http://localhost:9117/api/v2.0/indexers/all/results/torznab/api"
api_key_env = "TORZNAB_API_KEY"

api_key_env names the environment variable holding the key, not the key itself; only Torznab uses this field. Sources without auth omit it.

export TORZNAB_API_KEY='your-key'
magnet --config config.toml search "ubuntu" --source local --json

providers shows names and kinds only, never URLs or keys. HTTP error diagnostics never print request URLs either.

RSS

[[providers]]
name = "linux"
kind = "rss"
url = "https://example.org/torrents.rss"

The URL above is a placeholder; substitute a real RSS or Atom feed. The parser understands magnet links and enclosures in RSS items and Atom entries, plus Nyaa/Torznab extension fields. A bare .torrent download link is never fetched to compute a hash. Plain Newznab NZB results can't become BitTorrent magnets.

Protocol references: Knaben API, Bitsearch API, Nyaa RSS template, Torznab spec.

JSON Results & Aggregation Rules

search --json returns an array; --jsonl uses the same object shape. Sample data:

[
  {
    "id": 1,
    "title": "Example",
    "info_hash": "0000000000000000000000000000000000000000",
    "magnet": "magnet:?xt=urn%3Abtih%3A0000000000000000000000000000000000000000&dn=Example",
    "size": null,
    "seeders": null,
    "leechers": null,
    "published_at": null,
    "sources": ["local"],
    "detail_url": null,
    "trackers": []
  }
]
Field Type Meaning
id integer Position in the current search snapshot, from 1; local resolve prints 0
title string Torrent title
info_hash string or null BTIH normalized to lowercase hex
magnet string or null URL-encoded magnet URI
size integer or null Bytes
seeders / leechers integer or null Seeder / leecher counts as reported
published_at string or null UTC RFC 3339 timestamp; null when unparseable or zone-less
sources string array Merged provider names, sorted and deduped
detail_url string or null Detail or related link from the source
trackers string array Merged tracker URLs, sorted and deduped

Aggregation keys on BTIH, never on title. Base32 hashes convert to hex; hash-less results never merge even with identical names. For one hash, sources and trackers union, seeders/leechers take the max reported value, never summed.

Title Filtering

Public indexes tokenize CJK queries per character, and when a query has no real match they pad the page with their most popular rows. Search therefore drops any result whose title does not contain every whitespace-separated term of the query, ignoring case. This runs per provider, after magnet normalization and before dedup, and covers every query — not just non-ASCII ones. Each drop is reported on stderr as NAME: dropped N result(s) that do not match the query, so filtering is never silent.

The check is a plain substring test. It is deliberately predictable rather than clever, which means a query spelled differently from the titles it should match can be filtered out: searching C++ drops Microsoft Visual C+ Redistributable while keeping Visual C++ [2005-2015]. Pass --no-title-filter when a query is written in a form the titles never use literally.

The title comes from the first record in config order; size, publish time, and detail link keep that record's values, backfilled from later records when missing. Provider completion order doesn't affect this priority. Invalid or conflicting hashes are skipped with a warning.

Rebuilt magnets keep BTIH, display name, and trackers; other magnet params are dropped. BTIH/v1 is supported; v2-only magnets are not.

Search Snapshots & get

Results hit the snapshot after filtering, sorting, and truncation, then print. Linux default is ~/.cache/magnet/last-search.json; with XDG_CACHE_HOME set it lives under magnet/last-search.json there. Other platforms use the platform cache dir; when none is found it falls back to ./.magnet-last-search.json.

get ID only reads the snapshot, never the network. IDs are valid for that one search; the next search renumbers. Any search reaching the snapshot stage — empty results and all-provider-failed included — atomically replaces the old snapshot; argument validation failures don't wipe it, write failures exit 2.

Records without hash/magnet can still appear in tables and JSON. get ID on one exits 1, while get ID --json reads the full record. A missing or corrupt cache file exits 2; search again.

Concurrent searches use the last successfully written snapshot. Scripts or agent sessions can isolate the cache explicitly:

export MAGNET_CACHE="$PWD/session-search.json"
magnet search "ubuntu" --json
magnet get 1 --json

Exit Codes & Scripting

Results go to stdout, warnings and errors to stderr. Scripts should capture both separately and check the exit code.

Code Meaning
0 Search returned results, or other command succeeded
1 No matches, no such ID in the snapshot, or the result has no printable magnet
2 All sources failed including parse/runtime config errors, or local I/O failed
3 Bad arguments or config, including unknown source, unreadable config file
4 Every provider failed with HTTP, network, or timeout errors

Partial source failures print warnings; the exit stays 0 while results remain. At least one success but nothing after filtering exits 1. All failed with mixed network and parse errors exits 2. A missing api_key_env variable counts as that provider's runtime failure.

A search reaching the output stage prints [] for empty JSON and zero lines for empty JSONL/magnet; on argument, config, or cache-write failure, stdout JSON is not guaranteed. JSONL emits once after aggregation, not as a per-provider stream.

The Python example below needs no extra dependencies and tells "no results" apart from failure:

import json
import subprocess
import sys

result = subprocess.run(
    ["magnet", "--cache", "session-search.json", "search", "ubuntu", "--json"],
    capture_output=True,
    text=True,
)
if result.stderr:
    print(result.stderr, end="", file=sys.stderr)
if result.returncode not in (0, 1):
    raise SystemExit(result.returncode)
rows = json.loads(result.stdout)
for row in rows:
    if row["magnet"]:
        print(row["magnet"])

Piping into pikpaktui

Install jq and pikpaktui and finish pikpaktui login first. Pipe the first magnet of a search into pikpaktui offline to create a PikPak cloud task:

magnet search "odv-534" --json | jq -r '.[0].magnet' | xargs pikpaktui offline

.[0] picks the top result (seeders descending by default), jq -r strips the JSON quotes, xargs passes the magnet as the argument.

For scripts, confirm success first, then extract a non-empty magnet and quote the full URI. Nothing is created when the search fails, results are empty, or the first row lacks a magnet:

if results=$(magnet search "odv-534" --json) &&
   uri=$(printf '%s' "$results" | jq -er '.[0].magnet | select(type == "string" and startswith("magnet:?"))'); then
    pikpaktui offline "$uri"
fi

Or read a picked ID from the last search:

if uri=$(magnet get 1); then
    pikpaktui offline "$uri" --to "/Downloads"
fi

pikpaktui offline takes --to for the target dir, --name for the task name, and --dry-run to preview; e.g. swap the last call for pikpaktui offline "$uri" --dry-run. Accounts, target dirs, and offline tasks belong to pikpaktui; magnet handles search and magnet output.

Current Limits

Up to 8 providers run concurrently (configurable from 1 to 64), with a 15 s default timeout per provider and an 8 MiB cap per response. Extra sources queue. --timeout covers one provider and all of its requested pages; use --deadline when the whole command needs a bound.

Each source reads one page by default. --pages can request up to 20 pages from Knaben, Bitsearch, and Torznab; APIBay, Nyaa/Sukebei RSS, and generic RSS/Atom feeds are fetched once. --limit caps final output only and may return fewer rows than asked. Public source availability, rate limits, and completeness depend on upstream services.

BtGoogle is the one provider read by scraping HTML rather than a documented API, so it is the most likely to break when that page changes. No TUI, MCP, DHT lookup, or downloads.

Develop & Extend

src/
├── main.rs             # command execution, output, filtering, sorting
├── cli.rs              # clap argument definitions
├── model.rs            # Torrent model, hash/magnet normalization
├── search.rs           # concurrent scheduling, error summary, dedup
├── cache.rs            # atomic snapshot writes and reads
├── lib.rs              # library module exports
└── providers/
    ├── mod.rs          # Provider trait, config, HTTP wrapper
    ├── feeds.rs        # Nyaa / Sukebei / Torznab / RSS / DMHY / Mikan
    ├── knaben.rs       # Knaben JSON API
    ├── apibay.rs       # APIBay JSON API
    ├── bitsearch.rs    # Bitsearch JSON API
    ├── btgoogle.rs     # BtGoogle HTML search endpoint
    └── animegarden.rs  # Anime Garden JSON API

The crate root re-exports Provider, Torrent, SearchOptions, and both search entry points for new providers or other front ends. Implement Provider::search for a basic source and optionally override search_pages; call search for backward-compatible defaults or search_with_options for concurrency, deadline, and pagination controls. To support a new kind in CLI TOML configs, also update Kind and HttpProvider dispatch.

cargo fmt --check
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked

Tests use a local HTTP stub; no public providers or API keys needed. CI runs the format check, Clippy, and tests across Linux, Windows, and macOS.

License

MIT

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages