core/torrent-handler/core-system-implementation.js
2025-02-27 19:18:54 +01:00

352 lines
9.5 KiB
JavaScript

// 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;
}
}