352 lines
9.5 KiB
JavaScript
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;
|
|
}
|
|
}
|