// Minimal bencode encoder (Buffers for binary fields) function bencode(v) { if (Number.isInteger(v)) return Buffer.from(`i${v}e`); if (Buffer.isBuffer(v)) return Buffer.concat([Buffer.from(`${v.length}:`), v]); if (typeof v === 'string') { const b = Buffer.from(v, 'utf8'); return Buffer.concat([Buffer.from(`${b.length}:`), b]); } if (Array.isArray(v)) return Buffer.concat([Buffer.from('l'), ...v.map(bencode), Buffer.from('e')]); // dict — keys must be sorted for valid bencoding const keys = Object.keys(v).sort(); return Buffer.concat([ Buffer.from('d'), ...keys.flatMap(k => [bencode(k), bencode(v[k])]), Buffer.from('e'), ]); } // Minimal bencode decoder — returns Buffers for byte strings function bdecode(buf, pos = { i: 0 }) { const c = buf[pos.i]; if (c === 0x69) { // 'i' pos.i++; const end = buf.indexOf(0x65, pos.i); // 'e' const n = parseInt(buf.slice(pos.i, end).toString(), 10); pos.i = end + 1; return n; } if (c === 0x6c) { // 'l' pos.i++; const arr = []; while (buf[pos.i] !== 0x65) arr.push(bdecode(buf, pos)); pos.i++; return arr; } if (c === 0x64) { // 'd' pos.i++; const obj = {}; while (buf[pos.i] !== 0x65) { const key = bdecode(buf, pos).toString('utf8'); obj[key] = bdecode(buf, pos); } pos.i++; return obj; } // byte string: "N:..." const colon = buf.indexOf(0x3a, pos.i); // ':' const len = parseInt(buf.slice(pos.i, colon).toString(), 10); pos.i = colon + 1 + len; return buf.slice(colon + 1, pos.i); } // Build a minimal .torrent with an HTTP webseed and zeroed piece hashes. // Our fake qBittorrent skips hash verification so the zeroed hashes are fine. function makeTorrent({ name, size, webseedUrl }) { const PIECE_LEN = 512 * 1024; // 512 KB const numPieces = Math.max(1, Math.ceil((size || 1) / PIECE_LEN)); const pieces = Buffer.alloc(20 * numPieces, 0); return bencode({ 'comment': 'Webshare direct download', 'created by': 'webshare-api', 'info': { 'length': size || 0, 'name': name, 'piece length': PIECE_LEN, 'pieces': pieces, }, 'url-list': webseedUrl, }); } // Extract the url-list value from a raw .torrent buffer function extractWebseed(torrentBuf) { try { const d = bdecode(torrentBuf); const ul = d['url-list']; if (!ul) return null; if (Buffer.isBuffer(ul)) return ul.toString('utf8'); if (Array.isArray(ul) && ul.length) return ul[0].toString('utf8'); return null; } catch { return null; } } // Compute the real torrent info hash (SHA1 of the bencoded info dict). // Sonarr tracks downloads by this hash — using any other hash causes mismatch. function extractInfoHash(torrentBuf) { try { const crypto = require('crypto'); const d = bdecode(torrentBuf); if (!d.info) return null; return crypto.createHash('sha1').update(bencode(d.info)).digest('hex'); } catch { return null; } } module.exports = { makeTorrent, extractWebseed, extractInfoHash };