Torrent handler core
This commit is contained in:
parent
130e9275b8
commit
18d726d609
75
torrent-handler/README.md
Normal file
75
torrent-handler/README.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
Core System Components
|
||||||
|
|
||||||
|
Manifest File
|
||||||
|
|
||||||
|
Contains all core system metadata, configuration schema, navigation structure, features, and hooks
|
||||||
|
CoreID: 1000
|
||||||
|
Name: Torrent Handler (Transmission)
|
||||||
|
Version: 1.0.0
|
||||||
|
Author: MasterDraco
|
||||||
|
|
||||||
|
|
||||||
|
Core Implementation
|
||||||
|
|
||||||
|
Complete JavaScript implementation with Transmission RPC API integration
|
||||||
|
Methods for connecting to Transmission, managing torrents, and handling hooks
|
||||||
|
Support for all requested features: start/stop/delete torrents, add new torrents, set seed ratio and time
|
||||||
|
|
||||||
|
|
||||||
|
UI Components
|
||||||
|
|
||||||
|
Torrent Dashboard: Shows all active torrents with stats and control buttons
|
||||||
|
Add Torrent Form: Upload torrent files with configurable settings
|
||||||
|
Torrent Settings: Configure connection and default torrent settings
|
||||||
|
|
||||||
|
|
||||||
|
Routes and Registration
|
||||||
|
|
||||||
|
Route handling for all pages
|
||||||
|
Navigation registration
|
||||||
|
CSS style registration
|
||||||
|
Core system registration with the framework
|
||||||
|
|
||||||
|
|
||||||
|
CSS Styles
|
||||||
|
|
||||||
|
Comprehensive styling for all components
|
||||||
|
Responsive design for different screen sizes
|
||||||
|
Status indicators, progress bars, and other UI elements
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Features Implemented
|
||||||
|
|
||||||
|
✅ Connection to Transmission RPC API
|
||||||
|
✅ Start/stop/delete torrents
|
||||||
|
✅ Add new torrents by uploading torrent files
|
||||||
|
✅ Configure seed ratio for torrents
|
||||||
|
✅ Set time ratio (minutes) for seeding
|
||||||
|
✅ Auto-start toggle for new torrents
|
||||||
|
✅ Progress monitoring and status display
|
||||||
|
✅ Hook system for plugin extensions
|
||||||
|
|
||||||
|
How to Use the Core System
|
||||||
|
|
||||||
|
The system will connect to Transmission at the configured URL
|
||||||
|
The dashboard shows all torrents with their current status and progress
|
||||||
|
Use the Add Torrent button to upload new .torrent files
|
||||||
|
Configure default settings through the Settings page
|
||||||
|
For each torrent, you can:
|
||||||
|
|
||||||
|
Start or stop torrents
|
||||||
|
Remove torrents (keeping files)
|
||||||
|
Delete torrents (removing files)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Extension Points (Hooks)
|
||||||
|
I've included several hooks that plugins can use:
|
||||||
|
|
||||||
|
torrent_added: Triggers when a new torrent is added
|
||||||
|
torrent_completed: Triggers when a torrent finishes downloading
|
||||||
|
torrent_removed: Triggers when a torrent is removed
|
||||||
|
before_torrent_action: Allows intercepting actions before they happen
|
||||||
|
|
||||||
|
This system is ready to integrate with any framework that follows the specified pattern. Let me know if you need any clarification or have any questions about implementing specific aspects of the core system!
|
351
torrent-handler/core-system-implementation.js
Normal file
351
torrent-handler/core-system-implementation.js
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
167
torrent-handler/core-system-manifest.js
Normal file
167
torrent-handler/core-system-manifest.js
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
// core/torrent-handler/manifest.js
|
||||||
|
export default {
|
||||||
|
coreID: "1000",
|
||||||
|
name: "Torrent Handler (Transmission)",
|
||||||
|
description: "System for handling torrents",
|
||||||
|
version: "1.0.0",
|
||||||
|
author: "MasterDraco",
|
||||||
|
|
||||||
|
// Configuration schema
|
||||||
|
configuration: [
|
||||||
|
{
|
||||||
|
field: "transmission_url",
|
||||||
|
type: "string",
|
||||||
|
label: "Transmission URL",
|
||||||
|
description: "URL to the Transmission RPC interface (e.g. http://localhost:9091/transmission/rpc)",
|
||||||
|
required: true,
|
||||||
|
default: "http://localhost:9091/transmission/rpc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "transmission_username",
|
||||||
|
type: "string",
|
||||||
|
label: "Username",
|
||||||
|
description: "Username for Transmission authentication",
|
||||||
|
required: false,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "transmission_password",
|
||||||
|
type: "string",
|
||||||
|
label: "Password",
|
||||||
|
description: "Password for Transmission authentication",
|
||||||
|
required: false,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "default_seed_ratio",
|
||||||
|
type: "number",
|
||||||
|
label: "Default Seed Ratio",
|
||||||
|
description: "Default ratio to seed torrents (e.g. 2.0 means seed until shared 2x the download size)",
|
||||||
|
required: false,
|
||||||
|
default: 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "default_seed_time",
|
||||||
|
type: "number",
|
||||||
|
label: "Default Seed Time (minutes)",
|
||||||
|
description: "Default time to seed torrents in minutes",
|
||||||
|
required: false,
|
||||||
|
default: 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "auto_start_torrents",
|
||||||
|
type: "boolean",
|
||||||
|
label: "Auto-start Torrents",
|
||||||
|
description: "Automatically start torrents when added",
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Navigation structure
|
||||||
|
navigation: [
|
||||||
|
{
|
||||||
|
id: "torrent_dashboard",
|
||||||
|
name: "Torrent Dashboard",
|
||||||
|
icon: "download",
|
||||||
|
route: "/torrents"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "torrent_add",
|
||||||
|
name: "Add Torrent",
|
||||||
|
icon: "plus-circle",
|
||||||
|
route: "/torrents/add"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "torrent_settings",
|
||||||
|
name: "Torrent Settings",
|
||||||
|
icon: "settings",
|
||||||
|
route: "/torrents/settings"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Main features
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
feature: "transmission_connection",
|
||||||
|
description: "Connect to Transmission RPC API",
|
||||||
|
dataModel: {
|
||||||
|
url: "string",
|
||||||
|
username: "string",
|
||||||
|
password: "string",
|
||||||
|
sessionId: "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: "torrent_management",
|
||||||
|
description: "Start, stop, and delete torrents",
|
||||||
|
dataModel: {
|
||||||
|
torrentId: "number",
|
||||||
|
action: "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: "add_torrent",
|
||||||
|
description: "Add new torrents by uploading torrent file",
|
||||||
|
dataModel: {
|
||||||
|
torrentFile: "file",
|
||||||
|
seedRatio: "number",
|
||||||
|
seedTime: "number",
|
||||||
|
autoStart: "boolean",
|
||||||
|
downloadDirectory: "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: "torrent_settings",
|
||||||
|
description: "Configure default torrent settings",
|
||||||
|
dataModel: {
|
||||||
|
defaultSeedRatio: "number",
|
||||||
|
defaultSeedTime: "number",
|
||||||
|
defaultDownloadDirectory: "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Extension points (hooks)
|
||||||
|
hooks: [
|
||||||
|
{
|
||||||
|
id: "torrent_added",
|
||||||
|
purpose: "Called when a new torrent is added to the system",
|
||||||
|
dataFormat: {
|
||||||
|
torrentId: "number",
|
||||||
|
name: "string",
|
||||||
|
size: "number",
|
||||||
|
addedTimestamp: "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "torrent_completed",
|
||||||
|
purpose: "Called when a torrent completes downloading",
|
||||||
|
dataFormat: {
|
||||||
|
torrentId: "number",
|
||||||
|
name: "string",
|
||||||
|
downloadDirectory: "string",
|
||||||
|
files: "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "torrent_removed",
|
||||||
|
purpose: "Called when a torrent is removed from the system",
|
||||||
|
dataFormat: {
|
||||||
|
torrentId: "number",
|
||||||
|
name: "string",
|
||||||
|
reason: "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "before_torrent_action",
|
||||||
|
purpose: "Called before performing an action on a torrent (start, stop, delete)",
|
||||||
|
dataFormat: {
|
||||||
|
torrentId: "number",
|
||||||
|
name: "string",
|
||||||
|
action: "string",
|
||||||
|
cancel: "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
278
torrent-handler/css-styles.css
Normal file
278
torrent-handler/css-styles.css
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
/* core/torrent-handler/styles.css */
|
||||||
|
|
||||||
|
/* Dashboard layout */
|
||||||
|
.torrent-dashboard {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Torrent table styles */
|
||||||
|
.torrents-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrents-table th,
|
||||||
|
.torrents-table td {
|
||||||
|
padding: 12px 15px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrents-table th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.8em;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrents-table tr:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrents-table .actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress bar */
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
|
height: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar span {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 20px;
|
||||||
|
width: 100%;
|
||||||
|
color: #000;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 0 0 2px rgba(255,255,255,0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status indicators */
|
||||||
|
.status-indicator {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-downloading {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
color: #0d47a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-seeding {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-stopped {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #616161;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-checking {
|
||||||
|
background-color: #fff3e0;
|
||||||
|
color: #e65100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-queued {
|
||||||
|
background-color: #f3e5f5;
|
||||||
|
color: #6a1b9a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message boxes */
|
||||||
|
.error-message,
|
||||||
|
.success-message {
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background-color: #fdecea;
|
||||||
|
color: #c62828;
|
||||||
|
border-left: 4px solid #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
border-left: 4px solid #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message button,
|
||||||
|
.success-message button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1.5em;
|
||||||
|
line-height: 1;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message button:hover,
|
||||||
|
.success-message button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
.add-torrent-form,
|
||||||
|
.torrent-settings {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label[for="auto-start"],
|
||||||
|
.form-group label[for="auto_start_torrents"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label[for="auto-start"] input,
|
||||||
|
.form-group label[for="auto_start_torrents"] input {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"],
|
||||||
|
.form-group input[type="password"],
|
||||||
|
.form-group input[type="number"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="file"] {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group .help-text {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 30px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: #cccccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[type="button"] {
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[type="button"]:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.torrents-table {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
184
torrent-handler/routes-registration.js
Normal file
184
torrent-handler/routes-registration.js
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
// core/torrent-handler/routes.js
|
||||||
|
import React from 'react';
|
||||||
|
import { TorrentDashboard, AddTorrentForm, TorrentSettings } from './components';
|
||||||
|
|
||||||
|
export default function registerRoutes(framework, core) {
|
||||||
|
// Register routes based on navigation items in manifest
|
||||||
|
framework.registerRoute('/torrents', () => <TorrentDashboard core={core} />);
|
||||||
|
framework.registerRoute('/torrents/add', ({navigate}) => <AddTorrentForm core={core} navigate={navigate} />);
|
||||||
|
framework.registerRoute('/torrents/settings', ({navigate}) => <TorrentSettings core={core} navigate={navigate} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
// core/torrent-handler/register.js
|
||||||
|
import TorrentHandlerCore from './index';
|
||||||
|
import registerRoutes from './routes';
|
||||||
|
import manifest from './manifest';
|
||||||
|
|
||||||
|
// Register the core system with the framework
|
||||||
|
export default function register(framework) {
|
||||||
|
// Create instance of the core system
|
||||||
|
const core = new TorrentHandlerCore(framework);
|
||||||
|
|
||||||
|
// Register core with the framework
|
||||||
|
framework.registerCore(core.id, core);
|
||||||
|
|
||||||
|
// Register navigation items
|
||||||
|
manifest.navigation.forEach(item => {
|
||||||
|
framework.registerNavigationItem(item.id, {
|
||||||
|
name: item.name,
|
||||||
|
icon: item.icon,
|
||||||
|
route: item.route,
|
||||||
|
coreId: core.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register routes
|
||||||
|
registerRoutes(framework, core);
|
||||||
|
|
||||||
|
// Register CSS/Styles
|
||||||
|
framework.registerStyles(`
|
||||||
|
/* Torrent Dashboard Styles */
|
||||||
|
.torrent-dashboard .progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-dashboard .progress-fill {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-dashboard .progress-bar span {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 20px;
|
||||||
|
width: 100%;
|
||||||
|
color: #000;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-dashboard .torrents-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-dashboard .torrents-table th,
|
||||||
|
.torrent-dashboard .torrents-table td {
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-dashboard .torrents-table th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-dashboard .torrents-table .actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message, .success-message {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Styles */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"],
|
||||||
|
.form-group input[type="password"],
|
||||||
|
.form-group input[type="number"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group .help-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: #cccccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[type="button"] {
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[type="button"]:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Return success
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `${manifest.name} core system registered successfully`
|
||||||
|
};
|
||||||
|
}
|
489
torrent-handler/ui-components.js
Normal file
489
torrent-handler/ui-components.js
Normal file
@ -0,0 +1,489 @@
|
|||||||
|
// core/torrent-handler/components/TorrentDashboard.js
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export const TorrentDashboard = ({ core }) => {
|
||||||
|
const [torrents, setTorrents] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const loadTorrents = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const torrentList = await core.getTorrents();
|
||||||
|
setTorrents(torrentList);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to load torrents: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load torrents on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
loadTorrents();
|
||||||
|
|
||||||
|
// Set up refresh interval
|
||||||
|
const interval = setInterval(loadTorrents, 3000);
|
||||||
|
|
||||||
|
// Clean up interval on unmount
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleStart = async (torrentId) => {
|
||||||
|
try {
|
||||||
|
await core.startTorrent(torrentId);
|
||||||
|
loadTorrents();
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to start torrent: ' + err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStop = async (torrentId) => {
|
||||||
|
try {
|
||||||
|
await core.stopTorrent(torrentId);
|
||||||
|
loadTorrents();
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to stop torrent: ' + err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = async (torrentId, deleteData) => {
|
||||||
|
if (confirm(`Are you sure you want to remove this torrent${deleteData ? ' and its data' : ''}?`)) {
|
||||||
|
try {
|
||||||
|
await core.removeTorrent(torrentId, deleteData);
|
||||||
|
loadTorrents();
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to remove torrent: ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to format size
|
||||||
|
const formatSize = (bytes) => {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to format status
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
0: 'Stopped',
|
||||||
|
1: 'Queued to check',
|
||||||
|
2: 'Checking',
|
||||||
|
3: 'Queued to download',
|
||||||
|
4: 'Downloading',
|
||||||
|
5: 'Queued to seed',
|
||||||
|
6: 'Seeding'
|
||||||
|
};
|
||||||
|
return statusMap[status] || 'Unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="torrent-dashboard">
|
||||||
|
<h1>Torrent Dashboard</h1>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="form-actions">
|
||||||
|
<button type="button" onClick={() => navigate('/torrents')}>Cancel</button>
|
||||||
|
<button type="submit" disabled={loading}>
|
||||||
|
{loading ? 'Saving...' : 'Save Settings'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// core/torrent-handler/components/index.js
|
||||||
|
// Export all components for easier imports
|
||||||
|
export { TorrentDashboard } from './TorrentDashboard';
|
||||||
|
export { AddTorrentForm } from './AddTorrentForm';
|
||||||
|
export { TorrentSettings } from './TorrentSettings';
|
||||||
|
error-message">
|
||||||
|
{error}
|
||||||
|
<button onClick={() => setError(null)}>Dismiss</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="dashboard-actions">
|
||||||
|
<button onClick={loadTorrents} disabled={loading}>
|
||||||
|
{loading ? 'Refreshing...' : 'Refresh'}
|
||||||
|
</button>
|
||||||
|
<a href="#/torrents/add" className="button">Add Torrent</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && torrents.length === 0 ? (
|
||||||
|
<div className="loading">Loading torrents...</div>
|
||||||
|
) : torrents.length === 0 ? (
|
||||||
|
<div className="empty-state">
|
||||||
|
<p>No torrents found.</p>
|
||||||
|
<a href="#/torrents/add" className="button">Add a torrent</a>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<table className="torrents-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Progress</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Down</th>
|
||||||
|
<th>Up</th>
|
||||||
|
<th>Ratio</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{torrents.map(torrent => (
|
||||||
|
<tr key={torrent.id}>
|
||||||
|
<td>{torrent.name}</td>
|
||||||
|
<td>{getStatusText(torrent.status)}</td>
|
||||||
|
<td>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="default_seed_ratio">Default Seed Ratio:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="default_seed_ratio"
|
||||||
|
name="default_seed_ratio"
|
||||||
|
value={settings.default_seed_ratio}
|
||||||
|
onChange={handleChange}
|
||||||
|
min="0"
|
||||||
|
step="0.1"
|
||||||
|
/>
|
||||||
|
<p className="help-text">Set to 0 for unlimited</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="default_seed_time">Default Seed Time (minutes):</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="default_seed_time"
|
||||||
|
name="default_seed_time"
|
||||||
|
value={settings.default_seed_time}
|
||||||
|
onChange={handleChange}
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
<p className="help-text">Set to 0 for unlimited</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="auto_start_torrents">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="auto_start_torrents"
|
||||||
|
name="auto_start_torrents"
|
||||||
|
checked={settings.auto_start_torrents}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
Start torrents automatically when added
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="transmission_username">Username:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="transmission_username"
|
||||||
|
name="transmission_username"
|
||||||
|
value={settings.transmission_username}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<p className="help-text">Leave empty if authentication is not required</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="transmission_password">Password:</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="transmission_password"
|
||||||
|
name="transmission_password"
|
||||||
|
value={settings.transmission_password}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-actions">
|
||||||
|
<button type="button" onClick={handleTestConnection} disabled={loading}>
|
||||||
|
{loading ? 'Testing...' : 'Test Connection'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Default Torrent Settings</legend>
|
||||||
|
|
||||||
|
<div className="progress-bar">
|
||||||
|
<div
|
||||||
|
className="progress-fill"
|
||||||
|
style={{ width: `${Math.round(torrent.percentDone * 100)}%` }}
|
||||||
|
></div>
|
||||||
|
<span>{Math.round(torrent.percentDone * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{formatSize(torrent.totalSize)}</td>
|
||||||
|
<td>{formatSize(torrent.rateDownload)}/s</td>
|
||||||
|
<td>{formatSize(torrent.rateUpload)}/s</td>
|
||||||
|
<td>{torrent.uploadRatio.toFixed(2)}</td>
|
||||||
|
<td className="actions">
|
||||||
|
{[0, 3, 5].includes(torrent.status) ? (
|
||||||
|
<button onClick={() => handleStart(torrent.id)}>Start</button>
|
||||||
|
) : (
|
||||||
|
<button onClick={() => handleStop(torrent.id)}>Stop</button>
|
||||||
|
)}
|
||||||
|
<button onClick={() => handleRemove(torrent.id, false)}>Remove</button>
|
||||||
|
<button onClick={() => handleRemove(torrent.id, true)}>Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// core/torrent-handler/components/AddTorrentForm.js
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
export const AddTorrentForm = ({ core, navigate }) => {
|
||||||
|
const [torrentFile, setTorrentFile] = useState(null);
|
||||||
|
const [seedRatio, setSeedRatio] = useState(core.config.default_seed_ratio);
|
||||||
|
const [seedTime, setSeedTime] = useState(core.config.default_seed_time);
|
||||||
|
const [autoStart, setAutoStart] = useState(core.config.auto_start_torrents);
|
||||||
|
const [downloadDir, setDownloadDir] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file && (file.name.endsWith('.torrent') || file.type === 'application/x-bittorrent')) {
|
||||||
|
setTorrentFile(file);
|
||||||
|
setError(null);
|
||||||
|
} else {
|
||||||
|
setTorrentFile(null);
|
||||||
|
setError('Please select a valid .torrent file');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!torrentFile) {
|
||||||
|
setError('Please select a torrent file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await core.addTorrent(torrentFile, {
|
||||||
|
seedRatio: parseFloat(seedRatio),
|
||||||
|
seedTime: parseInt(seedTime, 10),
|
||||||
|
autoStart: autoStart,
|
||||||
|
downloadDirectory: downloadDir || undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redirect to dashboard
|
||||||
|
navigate('/torrents');
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to add torrent: ' + err.message);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="add-torrent-form">
|
||||||
|
<h1>Add New Torrent</h1>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="error-message">
|
||||||
|
{error}
|
||||||
|
<button onClick={() => setError(null)}>Dismiss</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="torrent-file">Torrent File:</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="torrent-file"
|
||||||
|
accept=".torrent,application/x-bittorrent"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="seed-ratio">Seed Ratio:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="seed-ratio"
|
||||||
|
value={seedRatio}
|
||||||
|
onChange={e => setSeedRatio(e.target.value)}
|
||||||
|
min="0"
|
||||||
|
step="0.1"
|
||||||
|
/>
|
||||||
|
<p className="help-text">Set to 0 for unlimited</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="seed-time">Seed Time (minutes):</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="seed-time"
|
||||||
|
value={seedTime}
|
||||||
|
onChange={e => setSeedTime(e.target.value)}
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
<p className="help-text">Set to 0 for unlimited</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="auto-start">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="auto-start"
|
||||||
|
checked={autoStart}
|
||||||
|
onChange={e => setAutoStart(e.target.checked)}
|
||||||
|
/>
|
||||||
|
Start torrent automatically
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="download-dir">Download Directory (optional):</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="download-dir"
|
||||||
|
value={downloadDir}
|
||||||
|
onChange={e => setDownloadDir(e.target.value)}
|
||||||
|
placeholder="Leave empty for default"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-actions">
|
||||||
|
<button type="button" onClick={() => navigate('/torrents')}>Cancel</button>
|
||||||
|
<button type="submit" disabled={loading || !torrentFile}>
|
||||||
|
{loading ? 'Adding...' : 'Add Torrent'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// core/torrent-handler/components/TorrentSettings.js
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
export const TorrentSettings = ({ core, navigate }) => {
|
||||||
|
const [settings, setSettings] = useState({
|
||||||
|
default_seed_ratio: core.config.default_seed_ratio,
|
||||||
|
default_seed_time: core.config.default_seed_time,
|
||||||
|
auto_start_torrents: core.config.auto_start_torrents,
|
||||||
|
transmission_url: core.config.transmission_url,
|
||||||
|
transmission_username: core.config.transmission_username,
|
||||||
|
transmission_password: core.config.transmission_password
|
||||||
|
});
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value, type, checked } = e.target;
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
[name]: type === 'checkbox' ? checked : value
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setSuccess(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update core configuration
|
||||||
|
core.config = { ...core.config, ...settings };
|
||||||
|
|
||||||
|
// Save configuration to framework persistence
|
||||||
|
await core.framework.saveConfiguration(core.id, core.config);
|
||||||
|
|
||||||
|
setSuccess(true);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to save settings: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestConnection = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setSuccess(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Temporarily update connection settings
|
||||||
|
const originalConfig = { ...core.config };
|
||||||
|
core.config = {
|
||||||
|
...core.config,
|
||||||
|
transmission_url: settings.transmission_url,
|
||||||
|
transmission_username: settings.transmission_username,
|
||||||
|
transmission_password: settings.transmission_password
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test connection
|
||||||
|
await core.connectToTransmission();
|
||||||
|
|
||||||
|
setSuccess('Connection successful!');
|
||||||
|
} catch (err) {
|
||||||
|
setError('Connection test failed: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="torrent-settings">
|
||||||
|
<h1>Torrent Settings</h1>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="error-message">
|
||||||
|
{error}
|
||||||
|
<button onClick={() => setError(null)}>Dismiss</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className="success-message">
|
||||||
|
{typeof success === 'string' ? success : 'Settings saved successfully!'}
|
||||||
|
<button onClick={() => setSuccess(false)}>Dismiss</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Connection Settings</legend>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="transmission_url">Transmission URL:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="transmission_url"
|
||||||
|
name="transmission_url"
|
||||||
|
value={settings.transmission_url}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p className="help-text">URL to Transmission RPC interface (e.g. http://localhost:9091/transmission/rpc)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="
|
Loading…
x
Reference in New Issue
Block a user