UNPKG

@lucasmod/modulo-torrent

Version:

Módulo para scraping de torrents, desenvolvido por @lucas_mod_domina.

241 lines (225 loc) 7.05 kB
//By: 𖧄 𝐋𝐔𝐂𝐀𝐒 𝐌𝐎𝐃 𝐃𝐎𝐌𝐈𝐍𝐀 𖧄 //Canal: https://whatsapp.com/channel/0029Va6riekH5JLwLUFI7P2B const mime = require('mime-types') const os = require('os') const process = require('process') const EventEmitter = require('events') const torrentStream = require('torrent-stream') const fs = require('fs') const path = require('path') /** * Gerenciador de streaming e download de torrents em Node.js. * * Este código implementa uma classe `TorrentStreamManager` que permite: * - Detectar o ambiente de execução (sistema operacional, arquitetura, etc.). * - Configurar opções para o motor de torrent. * - Iniciar o download de arquivos via magnet link. * - Realizar streaming de arquivos enquanto são baixados. * - Rastrear o progresso do download em tempo real. * - Gerenciar múltiplas instâncias de motores de torrent. * * Dependências principais: * - `torrent-stream`: Para manipulação de torrents. * - `mime-types`: Para identificar o tipo MIME dos arquivos. * - `fs` e `path`: Para manipulação de arquivos e diretórios. * - `os` e `process`: Para informações do sistema e controle do processo. * - `events`: Para emitir eventos de progresso. * * Uso: * - Use `getTorrentStream` para streaming de arquivos. * - Use `downloadTorrentLocally` para baixar arquivos localmente. * - Use `destroy` para encerrar todos os motores de torrent ativos. */ class TorrentStreamManager { constructor() { this.detectEnvironment() this.engines = new Map() } detectEnvironment() { const platform = os.platform() const termux = !!process.env.TERMUX_VERSION this.environment = { runtime: 'Node.js', version: process.versions.node, platform: platform, arch: os.arch(), termux: termux, pterodactyl: !!process.env.PTERODACTYL_SERVER_ID, isHeadless: !process.stdout.isTTY, device: this.detectDeviceType(platform, termux) } console.debug('Ambiente detectado:', this.environment) } detectDeviceType(platform, termux) { const p = platform.toLowerCase() if (p === 'android') return 'Mobile' if (p === 'darwin') return 'Desktop/Mobile' if (p === 'win32') return 'Desktop' if (p === 'linux') return termux ? 'Mobile (Termux)' : 'Desktop/Server' return 'Unknown' } configureOptions(downloadPath = null) { const options = { connections: 100, uploads: 10, verify: true, dht: true, tracker: true, tmp: downloadPath || os.tmpdir() } if (this.environment.termux) { options.tmp = `${os.tmpdir()}/torrent-stream` } if (this.environment.pterodactyl) { options.uploads = 5 options.connections = 50 } return options } /** * Função comum para iniciar o engine, selecionar o arquivo desejado e configurar o progresso. */ async setupTorrent(magnetURI, fileIndex = 0, downloadPath = null) { const engineId = Date.now() const engine = torrentStream(magnetURI, this.configureOptions(downloadPath)) this.engines.set(engineId, engine) return new Promise((resolve, reject) => { engine.on('ready', () => { try { const file = engine.files[fileIndex] if (!file) { this.destroyEngine(engineId) return reject(new Error(`Arquivo índice ${fileIndex} não encontrado`)) } // Prioriza o arquivo para download (útil para streaming) file.select() const progressEmitter = this.setupProgressTracking(engine, file.length) resolve({ engine, file, engineId, progressEmitter }) } catch (error) { this.destroyEngine(engineId) reject(error) } }) engine.on('error', error => { this.destroyEngine(engineId) reject(new Error(`Erro no torrent: ${error.message}`)) }) }) } /** * Para streaming, o arquivo é lido enquanto é baixado e o progresso é atualizado dinamicamente. */ async getTorrentStream(magnetURI, fileIndex = 0, range = '') { try { const { engine, file, engineId, progressEmitter } = await this.setupTorrent(magnetURI, fileIndex) const { start, end } = this.calculateRange(range, file.length) const stream = file.createReadStream({ start, end }) return { stream, progressEmitter, ...this.generateHeaders(file, start, end), cleanup: () => this.destroyEngine(engineId) } } catch (error) { throw new Error(`Erro no sistema: ${error.message}`) } } /** * Para download local, o arquivo é baixado para o diretório indicado e o progresso é informado dinamicamente. * A resolução ocorre quando o download estiver concluído (evento 'idle'). */ async downloadTorrentLocally(magnetURI, fileIndex = 0, downloadDir = os.tmpdir()) { await fs.promises.mkdir(downloadDir, { recursive: true }) try { const { engine, file, engineId, progressEmitter } = await this.setupTorrent(magnetURI, fileIndex, downloadDir) return await new Promise((resolve, reject) => { engine.on('idle', () => { const filePath = path.join(downloadDir, file.path) resolve({ filePath, size: file.length, progressEmitter, cleanup: () => this.destroyEngine(engineId) }) }) }) } catch (error) { throw new Error(`Erro no sistema: ${error.message}`) } } setupProgressTracking(engine, totalLength) { const progressEmitter = new EventEmitter() let downloaded = 0 engine.on('download', (pieceIndex) => { // Em cada evento 'download', soma o tamanho de peça (pode ser ajustado para usar dados reais de bytes) downloaded += engine.torrent.pieceLength let percent = ((downloaded / totalLength) * 100) if (percent > 100) percent = 100 const percentFixed = percent.toFixed(2) progressEmitter.emit('progress', percentFixed) console.log(`Progresso: ${percentFixed}%`) }) return progressEmitter } calculateRange(range, length) { let start = 0 let end = length - 1 if (range) { const parts = range.replace(/bytes=/, '').split('-') start = parseInt(parts[0], 10) end = parts[1] ? parseInt(parts[1], 10) : end } return { start, end } } generateHeaders(file, start, end) { return { contentType: mime.lookup(file.name) || 'application/octet-stream', headers: { 'Content-Type': mime.lookup(file.name) || 'application/octet-stream', 'Content-Length': end - start + 1, 'Accept-Ranges': 'bytes', 'Content-Range': `bytes ${start}-${end}/${file.length}`, 'Content-Disposition': `inline; filename="${encodeURIComponent(file.name)}"` } } } destroyEngine(engineId) { try { const engine = this.engines.get(engineId) if (engine) { engine.destroy() this.engines.delete(engineId) } } catch (error) { console.error('Erro ao remover engine:', error) } } async destroy() { for (const [id, engine] of this.engines) { try { engine.destroy() } catch (error) { console.error(`Erro ao destruir engine ${id}:`, error) } } this.engines.clear() } } if (parseInt(process.versions.node.split('.')[0]) < 14) { throw new Error('Node.js versão 14 ou superior é necessário') } const manager = new TorrentStreamManager(); ['SIGINT', 'SIGTERM', 'uncaughtException'].forEach(event => { process.on(event, async (err) => { console.log(`Desligando... (${event})`) if (err) console.error(err) await manager.destroy() process.exit(event === 'uncaughtException' ? 1 : 0) }) }) module.exports = { getTorrentStream: manager.getTorrentStream.bind(manager), downloadTorrentLocally: manager.downloadTorrentLocally.bind(manager), destroy: manager.destroy.bind(manager), environment: manager.environment }