diff --git a/torrent-handler/README.md b/torrent-handler/README.md
new file mode 100644
index 0000000..b49050d
--- /dev/null
+++ b/torrent-handler/README.md
@@ -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!
diff --git a/torrent-handler/core-system-implementation.js b/torrent-handler/core-system-implementation.js
new file mode 100644
index 0000000..ae4174c
--- /dev/null
+++ b/torrent-handler/core-system-implementation.js
@@ -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;
+ }
+}
diff --git a/torrent-handler/core-system-manifest.js b/torrent-handler/core-system-manifest.js
new file mode 100644
index 0000000..1266e66
--- /dev/null
+++ b/torrent-handler/core-system-manifest.js
@@ -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"
+ }
+ }
+ ]
+};
diff --git a/torrent-handler/css-styles.css b/torrent-handler/css-styles.css
new file mode 100644
index 0000000..ae3765e
--- /dev/null
+++ b/torrent-handler/css-styles.css
@@ -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%;
+ }
+}
diff --git a/torrent-handler/routes-registration.js b/torrent-handler/routes-registration.js
new file mode 100644
index 0000000..9037ae3
--- /dev/null
+++ b/torrent-handler/routes-registration.js
@@ -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', () =>
No torrents found.
+ Add a torrent +Name | +Status | +Progress | +Size | +Down | +Up | +Ratio | +Actions | +
---|---|---|---|---|---|---|---|
{torrent.name} | +{getStatusText(torrent.status)} | +
+
+
+
+
+
+ Set to 0 for unlimited +
+
+
+
+
+ Set to 0 for unlimited +
+
+
+
+
+
+
+
+
+
+ Leave empty if authentication is not required +
+
+
+
+
+
+
+
+
+
+ |
+ {formatSize(torrent.totalSize)} | +{formatSize(torrent.rateDownload)}/s | +{formatSize(torrent.rateUpload)}/s | +{torrent.uploadRatio.toFixed(2)} | ++ {[0, 3, 5].includes(torrent.status) ? ( + + ) : ( + + )} + + + | +