@lucasmod/modulo-torrent
Version:
Módulo para scraping de torrents, desenvolvido por @lucas_mod_domina.
241 lines (225 loc) • 7.05 kB
JavaScript
//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
}