// core/torrent-handler/index.js import manifest from './manifest'; export default class TorrentHandlerCore { constructor(framework) { this.id = manifest.coreID; this.name = manifest.name; this.version = manifest.version; this.framework = framework; this.config = {}; this.sessionId = null; // Define hooks for plugin extension this.hooks = {}; manifest.hooks.forEach(hook => { this.hooks[hook.id] = []; }); // Active torrents cache this.torrents = []; } // Initialize the core system async initialize(config) { this.config = { ...this.getDefaultConfig(), ...config }; // Initialize connection to Transmission try { await this.connectToTransmission(); console.log(`[${this.name}] Initialized and connected to Transmission successfully`); // Start periodic refresh of torrent list this.startTorrentRefresh(); return true; } catch (error) { console.error(`[${this.name}] Initialization failed:`, error); return false; } } // Get default configuration from manifest getDefaultConfig() { const defaults = {}; manifest.configuration.forEach(configItem => { defaults[configItem.field] = configItem.default; }); return defaults; } // Connect to Transmission RPC API async connectToTransmission() { try { const response = await this.transmissionRequest('session-stats', {}); console.log(`[${this.name}] Connected to Transmission ${response.version}`); return true; } catch (error) { console.error(`[${this.name}] Connection error:`, error); throw new Error('Failed to connect to Transmission RPC'); } } // Make request to Transmission RPC async transmissionRequest(method, arguments) { const url = this.config.transmission_url; const headers = { 'Content-Type': 'application/json', 'X-Transmission-Session-Id': this.sessionId || '' }; // Add authentication if provided let auth = null; if (this.config.transmission_username && this.config.transmission_password) { auth = btoa(`${this.config.transmission_username}:${this.config.transmission_password}`); headers['Authorization'] = `Basic ${auth}`; } const requestBody = { method: method, arguments: arguments }; try { const response = await fetch(url, { method: 'POST', headers: headers, body: JSON.stringify(requestBody) }); // Handle session ID requirement if (response.status === 409) { this.sessionId = response.headers.get('X-Transmission-Session-Id'); return this.transmissionRequest(method, arguments); } if (!response.ok) { throw new Error(`HTTP Error: ${response.status}`); } const data = await response.json(); if (data.result !== 'success') { throw new Error(`Transmission Error: ${data.result}`); } return data.arguments; } catch (error) { console.error(`[${this.name}] RPC request failed:`, error); throw error; } } // Start periodic refresh of torrent list startTorrentRefresh() { // Refresh every 3 seconds this.refreshInterval = setInterval(async () => { try { await this.refreshTorrents(); } catch (error) { console.error(`[${this.name}] Refresh error:`, error); } }, 3000); } // Refresh list of torrents async refreshTorrents() { const fields = [ 'id', 'name', 'status', 'percentDone', 'totalSize', 'uploadRatio', 'rateDownload', 'rateUpload', 'eta', 'errorString', 'downloadDir', 'addedDate', 'seedRatioLimit', 'seedRatioMode', 'seedIdleLimit', 'seedIdleMode', 'files', 'priorities' ]; const response = await this.transmissionRequest('torrent-get', { fields: fields }); const newTorrents = response.torrents; // Check for completed torrents newTorrents.forEach(torrent => { const existingTorrent = this.torrents.find(t => t.id === torrent.id); // If torrent just completed, trigger hook if (existingTorrent && existingTorrent.percentDone < 1 && torrent.percentDone === 1) { this.executeHook('torrent_completed', { torrentId: torrent.id, name: torrent.name, downloadDirectory: torrent.downloadDir, files: torrent.files }); } }); // Update torrents cache this.torrents = newTorrents; return this.torrents; } // Get all torrents async getTorrents() { try { return await this.refreshTorrents(); } catch (error) { console.error(`[${this.name}] Error getting torrents:`, error); throw error; } } // Add a new torrent async addTorrent(torrentFile, options = {}) { // Convert file to base64 const reader = new FileReader(); return new Promise((resolve, reject) => { reader.onload = async () => { try { const base64Data = reader.result.split(',')[1]; const args = { metainfo: base64Data, 'download-dir': options.downloadDirectory || this.config.default_download_directory, paused: !(options.autoStart !== undefined ? options.autoStart : this.config.auto_start_torrents) }; // Set seed ratio if provided if (options.seedRatio !== undefined) { args.seedRatioLimit = options.seedRatio; args.seedRatioMode = 1; // Use torrent-specific seed ratio limit } // Set seed time if provided if (options.seedTime !== undefined) { args.seedIdleLimit = options.seedTime; args.seedIdleMode = 1; // Use torrent-specific seed idle limit } const response = await this.transmissionRequest('torrent-add', args); const torrentAdded = response['torrent-added'] || {}; this.executeHook('torrent_added', { torrentId: torrentAdded.id, name: torrentAdded.name, size: torrentAdded.totalSize, addedTimestamp: Math.floor(Date.now() / 1000) }); resolve(torrentAdded); } catch (error) { reject(error); } }; reader.onerror = () => { reject(new Error('Failed to read torrent file')); }; reader.readAsDataURL(torrentFile); }); } // Start a torrent async startTorrent(torrentId) { // Execute before action hook const hookData = { torrentId: torrentId, name: this.getTorrentName(torrentId), action: 'start', cancel: false }; await this.executeHook('before_torrent_action', hookData); if (hookData.cancel) { console.log(`[${this.name}] Torrent start cancelled by hook`); return false; } await this.transmissionRequest('torrent-start', { ids: [torrentId] }); return true; } // Stop a torrent async stopTorrent(torrentId) { // Execute before action hook const hookData = { torrentId: torrentId, name: this.getTorrentName(torrentId), action: 'stop', cancel: false }; await this.executeHook('before_torrent_action', hookData); if (hookData.cancel) { console.log(`[${this.name}] Torrent stop cancelled by hook`); return false; } await this.transmissionRequest('torrent-stop', { ids: [torrentId] }); return true; } // Remove a torrent async removeTorrent(torrentId, deleteData = false) { // Execute before action hook const hookData = { torrentId: torrentId, name: this.getTorrentName(torrentId), action: 'remove', cancel: false }; await this.executeHook('before_torrent_action', hookData); if (hookData.cancel) { console.log(`[${this.name}] Torrent removal cancelled by hook`); return false; } await this.transmissionRequest('torrent-remove', { ids: [torrentId], 'delete-local-data': deleteData }); this.executeHook('torrent_removed', { torrentId: torrentId, name: this.getTorrentName(torrentId), reason: deleteData ? 'removed_with_data' : 'removed' }); return true; } // Set torrent properties async setTorrentProperties(torrentId, properties) { const args = { ids: [torrentId], ...properties }; await this.transmissionRequest('torrent-set', args); return true; } // Helper to get torrent name by id getTorrentName(torrentId) { const torrent = this.torrents.find(t => t.id === torrentId); return torrent ? torrent.name : `Unknown (${torrentId})`; } // Register a hook handler (used by plugins) registerHook(hookName, callback) { if (this.hooks[hookName]) { this.hooks[hookName].push(callback); return true; } return false; } // Execute hook handlers async executeHook(hookName, data) { if (!this.hooks[hookName]) return data; let result = { ...data }; for (const handler of this.hooks[hookName]) { try { result = await handler(result) || result; } catch (error) { console.error(`[${this.name}] Hook execution error (${hookName}):`, error); } } return result; } // Cleanup on shutdown async shutdown() { if (this.refreshInterval) { clearInterval(this.refreshInterval); } console.log(`[${this.name}] Core system shutting down`); return true; } }