Initial commit: webshare-api
Torznab + fake qBittorrent proxy for Webshare.cz, with Compose/Docker packaging, MIT license, and tests.
This commit is contained in:
97
src/torrent.js
Normal file
97
src/torrent.js
Normal 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 };
|
||||
Reference in New Issue
Block a user