andrade-soulseek-downloader
Version:
Simple, safe Soulseek download library with built-in rate limiting to prevent bans
287 lines • 12.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SlskClient = void 0;
const events_1 = require("events");
const crypto = __importStar(require("crypto"));
const stream = __importStar(require("stream"));
const net = __importStar(require("net"));
const server_1 = require("./server");
const default_peer_1 = require("./peer/default-peer");
const distributed_peer_1 = require("./peer/distributed-peer");
const download_peer_file_1 = require("./peer/download-peer-file");
const listen_1 = require("./listen");
const shared_1 = require("./share/shared");
const stack_1 = __importDefault(require("./stack"));
const debug = (msg) => {
if (process.env.DEBUG?.includes('slsk')) {
console.log(`[slsk] ${msg}`);
}
};
let server;
let listen;
let peers = {};
let shared;
class SlskClient extends events_1.EventEmitter {
serverAddress;
sharedFolders;
constructor(serverAddress, sharedFolders) {
super();
this.serverAddress = serverAddress;
this.sharedFolders = sharedFolders;
}
init(cb) {
debug('Init client');
server = new server_1.Server(this.serverAddress, cb);
server.on('connect-to-peer', (peer) => {
this.connectToPeer(peer);
});
server.on('get-peer-address', (peer) => {
if (peers[peer.user]) {
peers[peer.user].setAddress(peer.host, peer.port);
}
else {
peers[peer.user] = new default_peer_1.DefaultPeer(net.createConnection({
host: peer.host,
port: peer.port
}), peer);
if (stack_1.default.peerSearchMatches[peer.user]) {
Object.keys(stack_1.default.peerSearchMatches[peer.user]).forEach(ticket => {
peers[peer.user].fileSearchResult(stack_1.default.peerSearchMatches[peer.user][ticket], ticket, stack_1.default.currentLogin);
});
stack_1.default.peerSearchMatches[peer.user] = {};
}
}
});
shared = new shared_1.Shared();
this.sharedFolders.forEach(folder => shared.scanFolder(folder));
}
connectToPeer(peer) {
debug(`connectToPeer ${peer.user} ${peer.host} ${peer.port} ${peer.token} ${peer.type}`);
switch (peer.type) {
case 'F': {
debug(`Handling file transfer for token ${peer.token} from user ${peer.user}`);
const downloader = (0, download_peer_file_1.downloadPeerFile)(peer.host, peer.port, peer.token, peer.user, false);
// Try to find progress emitter by token first, then by user+file
let mainEmitter = null;
if (stack_1.default.downloadTokens[peer.token] && stack_1.default.downloadTokens[peer.token].progressEmitter) {
debug(`Found progressEmitter by token ${peer.token}`);
mainEmitter = stack_1.default.downloadTokens[peer.token].progressEmitter;
}
else {
// Try to find by looking at active downloads for this user
const downloadKeys = Object.keys(stack_1.default.download).filter(key => key.startsWith(peer.user + '_'));
if (downloadKeys.length > 0) {
const download = stack_1.default.download[downloadKeys[0]];
if (download.progressEmitter) {
debug(`Found progressEmitter by user ${peer.user}`);
mainEmitter = download.progressEmitter;
// Also store it in the token for future use
if (!stack_1.default.downloadTokens[peer.token]) {
stack_1.default.downloadTokens[peer.token] = {};
}
stack_1.default.downloadTokens[peer.token].progressEmitter = mainEmitter;
}
}
}
if (mainEmitter) {
debug(`Linking progress emitters`);
downloader.on('start', (data) => {
debug(`Download started event received, forwarding`);
mainEmitter.emit('start', data);
});
downloader.on('progress', (data) => {
debug(`Progress event received: ${data.percentage}%`);
mainEmitter.emit('progress', data);
});
downloader.on('complete', (data) => {
debug(`Complete event received`);
mainEmitter.emit('complete', data);
});
downloader.on('error', (err) => {
debug(`Error event received`);
mainEmitter.emit('error', err);
});
}
else {
debug(`No progress emitter found for user ${peer.user}`);
}
break;
}
case 'D': {
peers[peer.user] = new distributed_peer_1.DistributedPeer(net.createConnection({
host: peer.host,
port: peer.port
}), peer);
peers[peer.user].on('search', (search) => {
const matched = shared.search(search.query);
if (matched.length > 0) {
if (!peers[search.user]) {
server.getPeerAddress(search.user);
if (!stack_1.default.peerSearchMatches[search.user]) {
stack_1.default.peerSearchMatches[search.user] = {
[search.ticket]: matched
};
}
else {
stack_1.default.peerSearchMatches[search.user][search.ticket] = matched;
}
}
else {
peers[search.user].fileSearchResult(matched, search.ticket, stack_1.default.currentLogin);
}
debug(`Search from peer ${search.user}, query: ${search.query}. Matched: ${matched.length} files`);
}
});
peers[peer.user].on('disconnect', () => {
delete peers[peer.user];
});
break;
}
default: {
peers[peer.user] = new default_peer_1.DefaultPeer(net.createConnection({
host: peer.host,
port: peer.port
}), peer);
peers[peer.user].on('disconnect', () => {
delete peers[peer.user];
});
}
}
}
login(credentials, cb) {
stack_1.default.currentLogin = credentials.user;
server.login(credentials);
const incomingPort = credentials.incomingPort || 2234;
listen = new listen_1.Listen(incomingPort);
listen.on('new-peer', (evt) => {
const peer = evt.peer;
if (peers[peer.user]) {
debug(`Already connected to ${peer.user}`);
}
else {
server.getPeerAddress(peer.user);
debug(`new Peer connected ${peer.user} token ${peer.token}`);
peers[peer.user] = new default_peer_1.DefaultPeer(evt.socket, peer);
peers[peer.user].on('disconnect', () => {
delete peers[peer.user];
});
}
});
server.setWaitPort(incomingPort);
stack_1.default.login = (err) => cb(err || undefined);
}
search(obj, cb) {
if (typeof cb !== 'function') {
throw new Error('2nd argument must be callback function');
}
const token = crypto.randomBytes(4).toString('hex');
const timeout = obj.timeout || 4000;
const results = [];
setTimeout(() => {
delete stack_1.default.search[token];
cb(null, results);
}, timeout);
stack_1.default.search[token] = {
cb: (res) => {
this.emit('found', res);
this.emit(`found:${obj.req}`, res);
results.push(res);
},
query: obj.req
};
server.fileSearch(obj.req, token);
}
download(obj, cb, streamParam) {
debug(`launch download ${obj.file.user} ${obj.file.file}`);
const progressEmitter = new events_1.EventEmitter();
if (typeof cb !== 'function' && !streamParam) {
throw new Error('2nd argument must be callback function');
}
if (typeof obj.file === 'undefined') {
throw new Error('You must specify file');
}
if (!peers[obj.file.user]) {
cb(new Error('User not exist'));
return progressEmitter;
}
const token = crypto.randomBytes(4).toString('hex');
stack_1.default.downloadTokens[token] = {
user: obj.file.user,
file: obj.file.file,
size: obj.file.size,
progressEmitter
};
stack_1.default.download[obj.file.user + '_' + obj.file.file] = {
cb,
path: obj.path,
stream: streamParam,
progressEmitter // Store emitter here too for access by user+file
};
// Store token reference for later progress tracking
progressEmitter.on('newEmitter', (newEmitter) => {
// Forward all events from the actual download emitter
newEmitter.on('start', (data) => progressEmitter.emit('start', data));
newEmitter.on('progress', (data) => progressEmitter.emit('progress', data));
newEmitter.on('complete', (data) => progressEmitter.emit('complete', data));
newEmitter.on('error', (err) => progressEmitter.emit('error', err));
});
// Store the token so we can link it when download starts
progressEmitter.token = token;
peers[obj.file.user].transferRequest(obj.file.file, token);
return progressEmitter;
}
downloadStream(obj, cb) {
const s = new stream.Readable();
s._read = () => { };
cb(null, s);
this.download(obj, null, s);
}
destroy() {
if (server)
server.destroy();
if (listen)
listen.destroy();
Object.keys(peers).forEach(peer => {
peers[peer].destroy();
});
delete stack_1.default.login;
}
}
exports.SlskClient = SlskClient;
//# sourceMappingURL=slsk-client.js.map