Torznab + fake qBittorrent proxy for Webshare.cz, with Compose/Docker packaging, MIT license, and tests.
98 lines
3.2 KiB
JavaScript
98 lines
3.2 KiB
JavaScript
// 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 };
|