Initial commit: webshare-api

Torznab + fake qBittorrent proxy for Webshare.cz, with Compose/Docker
packaging, MIT license, and tests.
This commit is contained in:
2026-08-07 08:37:10 +02:00
commit 3fcadfe973
19 changed files with 2047 additions and 0 deletions

97
src/torrent.js Normal file
View File

@@ -0,0 +1,97 @@
// 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 };