Open-skills torrent-search
Search for torrents by title or IMDB ID via a Torznab-compatible API. Use when: (1) User asks to find a torrent for a movie or show, (2) You need a magnet link for a given title, or (3) User provides an IMDB ID and wants download options.
git clone https://github.com/besoeasy/open-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/besoeasy/open-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/torrent-search" ~/.claude/skills/besoeasy-open-skills-torrent-search && rm -rf "$T"
skills/torrent-search/SKILL.mdTorrent Search
Search any Torznab-compatible indexer (e.g. bitmagnet) for torrents by title or IMDB ID. Returns magnet links, file sizes, seeders, resolution, and codec.
When to use
- User asks to find a torrent for a movie, TV show, or any other content
- User provides an IMDB ID (e.g.
) and wants download optionstt1234567 - You need to programmatically retrieve a magnet link for a given title
- User asks to compare available qualities (720p, 1080p, 2160p) for a release
Required tools / APIs
— HTTP requests (pre-installed on most systems)curl
— JSON parsing (used after XML→JSON conversion)jq
— XML parsing (optional, fromxmllint
)libxml2-utils- A running Torznab endpoint — examples use
https://bitmagnetfortheweebs.midnightignite.me/torznab/api
Install options:
# Ubuntu/Debian sudo apt-get install -y curl jq libxml2-utils # macOS brew install curl jq libxml2 # Node.js (no extra packages — uses native fetch + DOMParser via fast-xml-parser) npm install fast-xml-parser
Skills
search_by_title
Search for torrents using a free-text title query.
TORZNAB_URL="https://bitmagnetfortheweebs.midnightignite.me/torznab/api" QUERY="Breaking Bad" curl -fsS --max-time 15 \ "${TORZNAB_URL}?t=search&q=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$QUERY")" \ | xmllint --xpath "//item" - 2>/dev/null \ | grep -oP '(?<=<title>).*?(?=</title>)'
Full extraction with magnet links:
TORZNAB_URL="https://bitmagnetfortheweebs.midnightignite.me/torznab/api" QUERY="Inception 2010" xml=$(curl -fsS --max-time 15 \ "${TORZNAB_URL}?t=search&q=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$QUERY")") # Print title + magnet for each result echo "$xml" | python3 - << 'EOF' import sys, xml.etree.ElementTree as ET data = sys.stdin.read() root = ET.fromstring(data) ns = {'torznab': 'http://torznab.com/schemas/2015/feed'} for item in root.findall('.//item'): title = item.findtext('title', '') size = item.findtext('size', '0') enc = item.find('enclosure') magnet = enc.get('url') if enc is not None else '' attrs = {a.get('name'): a.get('value') for a in item.findall('torznab:attr', ns)} seeders = attrs.get('seeders', '?') resolution = attrs.get('resolution', '') codec = attrs.get('video', '') size_gb = round(int(size) / 1_073_741_824, 2) print(f"{title}") print(f" Size: {size_gb} GB Seeders: {seeders} {resolution} {codec}") print(f" Magnet: {magnet[:80]}...") print() EOF
Node.js:
import { XMLParser } from 'fast-xml-parser'; const TORZNAB_URL = 'https://bitmagnetfortheweebs.midnightignite.me/torznab/api'; async function searchTorrents(query) { const url = `${TORZNAB_URL}?t=search&q=${encodeURIComponent(query)}`; const res = await fetch(url, { signal: AbortSignal.timeout(15000) }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const xml = await res.text(); const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_' }); const doc = parser.parse(xml); const items = doc?.rss?.channel?.item ?? []; const list = Array.isArray(items) ? items : [items]; return list.map(item => { const attrs = {}; const rawAttrs = item['torznab:attr'] ?? []; const attrList = Array.isArray(rawAttrs) ? rawAttrs : [rawAttrs]; for (const a of attrList) attrs[a['@_name']] = a['@_value']; return { title: item.title, sizeBytes: Number(item.size ?? 0), sizeGB: +(Number(item.size ?? 0) / 1_073_741_824).toFixed(2), magnet: item.enclosure?.['@_url'] ?? attrs.magneturl ?? '', infohash: attrs.infohash ?? '', seeders: Number(attrs.seeders ?? 0), leechers: Number(attrs.leechers ?? 0), resolution: attrs.resolution ?? '', codec: attrs.video ?? '', year: attrs.year ?? '', imdb: attrs.imdb ? `tt${attrs.imdb}` : '', }; }); } // Usage searchTorrents('Inception 2010').then(results => { results.forEach(r => { console.log(`${r.title}`); console.log(` ${r.sizeGB} GB | ${r.resolution} ${r.codec} | ${r.seeders} seeders`); console.log(` ${r.magnet.slice(0, 80)}...`); console.log(); }); });
search_by_imdb_id
Search by exact IMDB ID to get all available releases for a specific title.
TORZNAB_URL="https://bitmagnetfortheweebs.midnightignite.me/torznab/api" IMDB_ID="tt12735488" # Kalki 2898 AD curl -fsS --max-time 15 \ "${TORZNAB_URL}?t=search&q=${IMDB_ID}" \ | python3 - << 'EOF' import sys, xml.etree.ElementTree as ET root = ET.fromstring(sys.stdin.read()) ns = {'torznab': 'http://torznab.com/schemas/2015/feed'} for item in root.findall('.//item'): title = item.findtext('title', '') size = int(item.findtext('size', '0')) enc = item.find('enclosure') magnet = enc.get('url') if enc is not None else '' attrs = {a.get('name'): a.get('value') for a in item.findall('torznab:attr', ns)} print(f"[{attrs.get('resolution','?'):6}] {round(size/1e9,1):5.1f}GB " f"S:{attrs.get('seeders','?'):>3} {title}") EOF
Node.js:
import { XMLParser } from 'fast-xml-parser'; const TORZNAB_URL = 'https://bitmagnetfortheweebs.midnightignite.me/torznab/api'; async function searchByImdb(imdbId) { // imdbId format: 'tt1234567' or just '1234567' const id = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`; const url = `${TORZNAB_URL}?t=search&q=${id}`; const res = await fetch(url, { signal: AbortSignal.timeout(15000) }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const xml = await res.text(); const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_' }); const doc = parser.parse(xml); const items = doc?.rss?.channel?.item ?? []; const list = Array.isArray(items) ? items : [items]; return list.map(item => { const attrs = {}; const rawAttrs = item['torznab:attr'] ?? []; for (const a of Array.isArray(rawAttrs) ? rawAttrs : [rawAttrs]) { attrs[a['@_name']] = a['@_value']; } return { title: item.title, sizeGB: +(Number(item.size ?? 0) / 1_073_741_824).toFixed(2), magnet: item.enclosure?.['@_url'] ?? attrs.magneturl ?? '', infohash: attrs.infohash ?? '', seeders: Number(attrs.seeders ?? 0), resolution: attrs.resolution ?? '', codec: attrs.video ?? '', year: attrs.year ?? '', }; }); } // Usage — find all releases for a specific IMDB title searchByImdb('tt12735488').then(results => { // Sort by seeders descending, then by size descending results.sort((a, b) => b.seeders - a.seeders || b.sizeGB - a.sizeGB); results.forEach(r => console.log(`[${r.resolution || '?':>6}] ${r.sizeGB}GB S:${r.seeders} ${r.title}`) ); });
pick_best_result
Filter and rank results by quality preference (resolution priority + seeder count).
Node.js:
function pickBest(results, { preferResolution = '1080p', minSeeders = 1 } = {}) { const resolutionRank = { '2160p': 4, '1080p': 3, '720p': 2, '480p': 1 }; const preferred = resolutionRank[preferResolution] ?? 3; return results .filter(r => r.seeders >= minSeeders) .sort((a, b) => { const ra = resolutionRank[a.resolution] ?? 0; const rb = resolutionRank[b.resolution] ?? 0; // Exact preferred resolution first, then by seeders const aMatch = ra === preferred ? 1 : 0; const bMatch = rb === preferred ? 1 : 0; if (bMatch !== aMatch) return bMatch - aMatch; return b.seeders - a.seeders; })[0] ?? null; } // Usage const results = await searchByImdb('tt12735488'); const best = pickBest(results, { preferResolution: '1080p', minSeeders: 1 }); if (best) { console.log(`Best pick: ${best.title}`); console.log(`Magnet: ${best.magnet}`); }
Output format
Each result object contains:
— string — release name as indexed (e.g.title
)"Inception.2010.1080p.BluRay.x264"
— number — file size in gigabytes (e.g.sizeGB
)3.15
— string — full magnet URI starting withmagnetmagnet:?xt=urn:btih:...
— string — 40-char hex SHA-1 info hashinfohash
— number — active seeders at index time (may be stale)seeders
— number — active leechers at index timeleechers
— string —resolution
,"720p"
,"1080p"
, or"2160p"
if unknown""
— string —codec
,"x264"
,"x265"
,"XviD"
, or"AV1"
if unknown""
— string — release year (e.g.year
)"2024"
— string — IMDB ID inimdb
format (e.g.tt
)"tt12735488"
Error shape:
{ "error": "HTTP 503", "fix": "Indexer is down — retry in 30s or use a different endpoint" }
Rate limits / Best practices
- The public endpoint has no documented rate limit; add a 1-second delay between batch queries
- IMDB ID search (
) is more precise than title search — prefer it when you have the ID?q=tt... - Seeder counts in the index may lag reality by hours — always surface them to the user but don't rely on them for availability
- Sort results by seeders descending before presenting to the user
- Filter out results with 0 seeders unless no others exist
- Cache results for at least 5 minutes — the index is not real-time
- For batch lookups, space requests at least 1 second apart
Agent prompt
You have torrent-search capability via a Torznab API. When a user asks to find a torrent for something: 1. If they provide an IMDB ID (tt...), use search_by_imdb_id for precise results. 2. Otherwise use search_by_title with the title and year if known. 3. Parse the XML response and extract: title, sizeGB, seeders, resolution, magnet link. 4. Sort results by seeders descending. Filter out 0-seeder results unless nothing else exists. 5. Present the top 3–5 results with: title, size, resolution, seeder count. 6. Ask the user which one they want, then return the full magnet link. 7. Never auto-start a download without user confirmation. Torznab endpoint: https://bitmagnetfortheweebs.midnightignite.me/torznab/api
Troubleshooting
Empty results / no
elements:<item>
- Symptom: The XML response has a
but no<channel>
children<item> - Solution: The indexer has no matches. Try a shorter query (just the title, no year). Try searching by IMDB ID instead.
not found:xmllint
- Symptom:
xmllint: command not found - Solution:
(Linux) orsudo apt-get install -y libxml2-utils
(macOS). Alternatively use the Pythonbrew install libxml2
approach which needs no extra tools.xml.etree
not available:fast-xml-parser
- Symptom:
Cannot find module 'fast-xml-parser' - Solution:
. As a zero-dependency alternative, use the Bash + Python script path which only needs the standard library.npm install fast-xml-parser
Endpoint unreachable (connection refused / timeout):
- Symptom:
orcurl: (7) Failed to connectcurl: (28) Operation timed out - Solution: The specific public instance may be offline. Self-host bitmagnet (
) and pointdocker run -d ghcr.io/bitmagnet-io/bitmagnet
to your local instance.TORZNAB_URL
Magnet link is empty:
- Symptom:
is missing or blank for some itemsenclosure url - Solution: Reconstruct from infohash:
magnet:?xt=urn:btih:<infohash>&dn=<encoded-title>
See also
- ../using-youtube-download/SKILL.md — Download videos from YouTube with yt-dlp
- ../anonymous-file-upload/SKILL.md — Upload files without an account (IPFS / transfer.sh)