#!/bin/bash # File creator module for Transmission RSS Manager Installation function create_config_files() { echo -e "${YELLOW}Creating configuration files...${NC}" # Create initial config.json echo "Creating config.json..." mkdir -p "$CONFIG_DIR" cat > $CONFIG_DIR/config.json << EOF { "version": "2.0.3", "installPath": "$INSTALL_DIR", "transmissionConfig": { "host": "$TRANSMISSION_HOST", "port": $TRANSMISSION_PORT, "username": "$TRANSMISSION_USER", "password": "$TRANSMISSION_PASS", "path": "$TRANSMISSION_RPC_PATH" }, "remoteConfig": { "isRemote": $TRANSMISSION_REMOTE, "directoryMapping": $TRANSMISSION_DIR_MAPPING }, "destinationPaths": { "movies": "$MEDIA_DIR/movies", "tvShows": "$MEDIA_DIR/tvshows", "music": "$MEDIA_DIR/music", "books": "$MEDIA_DIR/books", "magazines": "$MEDIA_DIR/magazines", "software": "$MEDIA_DIR/software" }, "seedingRequirements": { "minRatio": 1.0, "minTimeMinutes": 60, "checkIntervalSeconds": 300 }, "processingOptions": { "enableBookSorting": $ENABLE_BOOK_SORTING, "extractArchives": true, "deleteArchives": true, "createCategoryFolders": true, "ignoreSample": true, "ignoreExtras": true, "renameFiles": true, "autoReplaceUpgrades": true, "removeDuplicates": true, "keepOnlyBestVersion": true }, "rssFeeds": [], "rssUpdateIntervalMinutes": 60, "autoProcessing": false, "securitySettings": { "authEnabled": false, "httpsEnabled": false, "sslCertPath": "", "sslKeyPath": "", "users": [] }, "port": $PORT, "logLevel": "info" } EOF # Create package.json echo "Creating package.json..." cat > $INSTALL_DIR/package.json << EOF { "name": "transmission-rss-manager", "version": "2.0.0", "description": "Enhanced Transmission RSS Manager with post-processing capabilities", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "express": "^4.18.2", "body-parser": "^1.20.2", "transmission-promise": "^1.1.5", "adm-zip": "^0.5.10", "node-fetch": "^2.6.9", "xml2js": "^0.5.0", "cors": "^2.8.5", "bcrypt": "^5.1.0", "jsonwebtoken": "^9.0.0", "morgan": "^1.10.0" } } EOF # Create server.js echo "Creating server.js..." cp "${SCRIPT_DIR}/server.js" "$INSTALL_DIR/server.js" || { # If the file doesn't exist in the script directory, create it from scratch cat > $INSTALL_DIR/server.js << 'EOF' // server.js - Main application server file const express = require('express'); const bodyParser = require('body-parser'); const path = require('path'); const fs = require('fs').promises; const cors = require('cors'); const Transmission = require('transmission'); // Import custom modules const PostProcessor = require('./modules/postProcessor'); const RssFeedManager = require('./modules/rssFeedManager'); // Initialize Express app const app = express(); const PORT = process.env.PORT || 3000; // Load configuration let config = { transmissionConfig: { host: 'localhost', port: 9091, username: '', password: '', path: '/transmission/rpc' }, remoteConfig: { isRemote: false, directoryMapping: {} }, destinationPaths: { movies: '/mnt/media/movies', tvShows: '/mnt/media/tvshows', music: '/mnt/media/music', books: '/mnt/media/books', magazines: '/mnt/media/magazines', software: '/mnt/media/software' }, seedingRequirements: { minRatio: 1.0, minTimeMinutes: 60, checkIntervalSeconds: 300 }, processingOptions: { enableBookSorting: true, extractArchives: true, deleteArchives: true, createCategoryFolders: true, ignoreSample: true, ignoreExtras: true, renameFiles: true, autoReplaceUpgrades: true, removeDuplicates: true, keepOnlyBestVersion: true }, rssFeeds: [], rssUpdateIntervalMinutes: 60, autoProcessing: false }; // Service instances let transmissionClient = null; let postProcessor = null; let rssFeedManager = null; // Save config function async function saveConfig() { try { await fs.writeFile( path.join(__dirname, 'config.json'), JSON.stringify(config, null, 2), 'utf8' ); console.log('Configuration saved'); return true; } catch (err) { console.error('Error saving config:', err.message); return false; } } // Load config function async function loadConfig() { try { const data = await fs.readFile(path.join(__dirname, 'config.json'), 'utf8'); const loadedConfig = JSON.parse(data); config = { ...config, ...loadedConfig }; console.log('Configuration loaded'); return true; } catch (err) { console.error('Error loading config, using defaults:', err.message); // Save default config await saveConfig(); return false; } } // Initialize Transmission client function initTransmission() { transmissionClient = new Transmission({ host: config.transmissionConfig.host, port: config.transmissionConfig.port, username: config.transmissionConfig.username, password: config.transmissionConfig.password, url: config.transmissionConfig.path }); console.log(`Transmission client initialized for ${config.transmissionConfig.host}:${config.transmissionConfig.port}`); return transmissionClient; } // Initialize post processor function initPostProcessor() { if (postProcessor) { postProcessor.stop(); } postProcessor = new PostProcessor({ transmissionConfig: config.transmissionConfig, remoteConfig: config.remoteConfig, destinationPaths: config.destinationPaths, seedingRequirements: config.seedingRequirements, processingOptions: config.processingOptions, downloadDir: config.downloadDir }); if (config.autoProcessing) { postProcessor.start(); console.log('Post-processor started automatically'); } else { console.log('Post-processor initialized (not auto-started)'); } return postProcessor; } // Initialize RSS feed manager function initRssFeedManager() { if (rssFeedManager) { rssFeedManager.stop(); } rssFeedManager = new RssFeedManager({ feeds: config.rssFeeds, updateIntervalMinutes: config.rssUpdateIntervalMinutes }); rssFeedManager.loadItems() .then(() => { if (config.rssFeeds && config.rssFeeds.length > 0) { rssFeedManager.start(); console.log('RSS feed manager started'); } else { console.log('RSS feed manager initialized (no feeds configured)'); } }) .catch(err => { console.error('Error initializing RSS feed manager:', err); }); return rssFeedManager; } // Enable CORS app.use(cors()); // Parse JSON bodies app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' })); // API routes //============================== // Server status API app.get('/api/status', (req, res) => { res.json({ status: 'running', version: '1.2.0', transmissionConnected: !!transmissionClient, postProcessorActive: postProcessor && postProcessor.processingIntervalId !== null, rssFeedManagerActive: rssFeedManager && rssFeedManager.updateIntervalId !== null, config: { autoProcessing: config.autoProcessing, rssEnabled: config.rssFeeds && config.rssFeeds.length > 0 } }); }); // Get configuration app.get('/api/config', (req, res) => { // Don't send password in response const safeConfig = { ...config }; if (safeConfig.transmissionConfig) { safeConfig.transmissionConfig = { ...safeConfig.transmissionConfig, password: '••••••••' }; } res.json(safeConfig); }); // Update configuration app.post('/api/config', async (req, res) => { try { // Create a deep copy of the old config const oldConfig = JSON.parse(JSON.stringify(config)); // Update config object, preserving password if not provided config = { ...config, ...req.body, transmissionConfig: { ...config.transmissionConfig, ...req.body.transmissionConfig, password: req.body.transmissionConfig?.password || config.transmissionConfig.password }, remoteConfig: { ...config.remoteConfig, ...req.body.remoteConfig }, destinationPaths: { ...config.destinationPaths, ...req.body.destinationPaths }, seedingRequirements: { ...config.seedingRequirements, ...req.body.seedingRequirements }, processingOptions: { ...config.processingOptions, ...req.body.processingOptions } }; // Save the updated config await saveConfig(); // Check if key services need reinitialization let changes = { transmissionChanged: JSON.stringify(oldConfig.transmissionConfig) !== JSON.stringify(config.transmissionConfig), postProcessorChanged: JSON.stringify(oldConfig.remoteConfig) !== JSON.stringify(config.remoteConfig) || JSON.stringify(oldConfig.destinationPaths) !== JSON.stringify(config.destinationPaths) || JSON.stringify(oldConfig.seedingRequirements) !== JSON.stringify(config.seedingRequirements) || JSON.stringify(oldConfig.processingOptions) !== JSON.stringify(config.processingOptions), rssFeedsChanged: JSON.stringify(oldConfig.rssFeeds) !== JSON.stringify(config.rssFeeds) || oldConfig.rssUpdateIntervalMinutes !== config.rssUpdateIntervalMinutes, autoProcessingChanged: oldConfig.autoProcessing !== config.autoProcessing }; // Reinitialize services as needed if (changes.transmissionChanged) { initTransmission(); } if (changes.postProcessorChanged || changes.transmissionChanged) { initPostProcessor(); } if (changes.rssFeedsChanged) { initRssFeedManager(); } if (changes.autoProcessingChanged) { if (config.autoProcessing && postProcessor) { postProcessor.start(); } else if (!config.autoProcessing && postProcessor) { postProcessor.stop(); } } res.json({ success: true, message: 'Configuration updated successfully', changes }); } catch (error) { console.error('Error updating configuration:', error); res.status(500).json({ success: false, message: `Failed to update configuration: ${error.message}` }); } }); // Transmission API routes //============================== // Test Transmission connection app.post('/api/transmission/test', (req, res) => { const { host, port, username, password } = req.body; // Create a test client with provided credentials const testClient = new Transmission({ host: host || config.transmissionConfig.host, port: port || config.transmissionConfig.port, username: username || config.transmissionConfig.username, password: password || config.transmissionConfig.password, url: config.transmissionConfig.path }); // Test connection testClient.sessionStats((err, result) => { if (err) { return res.json({ success: false, message: `Connection failed: ${err.message}` }); } // Connection successful res.json({ success: true, message: "Connected to Transmission successfully!", data: { version: result.version || "Unknown", rpcVersion: result.rpcVersion || "Unknown" } }); }); }); // Get torrents from Transmission app.get('/api/transmission/torrents', (req, res) => { if (!transmissionClient) { return res.status(400).json({ success: false, message: 'Transmission client not initialized' }); } transmissionClient.get((err, result) => { if (err) { return res.status(500).json({ success: false, message: `Error getting torrents: ${err.message}` }); } res.json({ success: true, data: result.torrents || [] }); }); }); // Add torrent to Transmission app.post('/api/transmission/add', (req, res) => { if (!transmissionClient) { return res.status(400).json({ success: false, message: 'Transmission client not initialized' }); } const { url } = req.body; if (!url) { return res.status(400).json({ success: false, message: 'URL is required' }); } transmissionClient.addUrl(url, (err, result) => { if (err) { return res.status(500).json({ success: false, message: `Error adding torrent: ${err.message}` }); } res.json({ success: true, data: result }); }); }); // Remove torrent from Transmission app.post('/api/transmission/remove', (req, res) => { if (!transmissionClient) { return res.status(400).json({ success: false, message: 'Transmission client not initialized' }); } const { ids, deleteLocalData } = req.body; if (!ids || !Array.isArray(ids) && typeof ids !== 'number') { return res.status(400).json({ success: false, message: 'Valid torrent ID(s) required' }); } transmissionClient.remove(ids, !!deleteLocalData, (err, result) => { if (err) { return res.status(500).json({ success: false, message: `Error removing torrent: ${err.message}` }); } res.json({ success: true, data: result }); }); }); // Start torrents app.post('/api/transmission/start', (req, res) => { if (!transmissionClient) { return res.status(400).json({ success: false, message: 'Transmission client not initialized' }); } const { ids } = req.body; if (!ids) { return res.status(400).json({ success: false, message: 'Torrent ID(s) required' }); } transmissionClient.start(ids, (err, result) => { if (err) { return res.status(500).json({ success: false, message: `Error starting torrent: ${err.message}` }); } res.json({ success: true, data: result }); }); }); // Stop torrents app.post('/api/transmission/stop', (req, res) => { if (!transmissionClient) { return res.status(400).json({ success: false, message: 'Transmission client not initialized' }); } const { ids } = req.body; if (!ids) { return res.status(400).json({ success: false, message: 'Torrent ID(s) required' }); } transmissionClient.stop(ids, (err, result) => { if (err) { return res.status(500).json({ success: false, message: `Error stopping torrent: ${err.message}` }); } res.json({ success: true, data: result }); }); }); // RSS Feed Manager API routes //============================== // Get all feeds app.get('/api/rss/feeds', (req, res) => { if (!rssFeedManager) { return res.status(400).json({ success: false, message: 'RSS feed manager not initialized' }); } res.json({ success: true, data: rssFeedManager.getAllFeeds() }); }); // Add a new feed app.post('/api/rss/feeds', async (req, res) => { if (!rssFeedManager) { return res.status(400).json({ success: false, message: 'RSS feed manager not initialized' }); } const feed = req.body; if (!feed || !feed.url) { return res.status(400).json({ success: false, message: 'Feed URL is required' }); } try { const newFeed = rssFeedManager.addFeed(feed); // Update the config with the new feed config.rssFeeds = rssFeedManager.getAllFeeds(); await saveConfig(); res.json({ success: true, data: newFeed }); } catch (error) { res.status(500).json({ success: false, message: `Error adding feed: ${error.message}` }); } }); // Update a feed app.put('/api/rss/feeds/:id', async (req, res) => { if (!rssFeedManager) { return res.status(400).json({ success: false, message: 'RSS feed manager not initialized' }); } const { id } = req.params; const updates = req.body; if (!id || !updates) { return res.status(400).json({ success: false, message: 'Feed ID and updates are required' }); } try { const success = rssFeedManager.updateFeedConfig(id, updates); if (!success) { return res.status(404).json({ success: false, message: `Feed with ID ${id} not found` }); } // Update the config with the updated feeds config.rssFeeds = rssFeedManager.getAllFeeds(); await saveConfig(); res.json({ success: true, message: 'Feed updated successfully' }); } catch (error) { res.status(500).json({ success: false, message: `Error updating feed: ${error.message}` }); } }); // Delete a feed app.delete('/api/rss/feeds/:id', async (req, res) => { if (!rssFeedManager) { return res.status(400).json({ success: false, message: 'RSS feed manager not initialized' }); } const { id } = req.params; if (!id) { return res.status(400).json({ success: false, message: 'Feed ID is required' }); } try { const success = rssFeedManager.removeFeed(id); if (!success) { return res.status(404).json({ success: false, message: `Feed with ID ${id} not found` }); } // Update the config with the remaining feeds config.rssFeeds = rssFeedManager.getAllFeeds(); await saveConfig(); res.json({ success: true, message: 'Feed removed successfully' }); } catch (error) { res.status(500).json({ success: false, message: `Error removing feed: ${error.message}` }); } }); // Get feed items app.get('/api/rss/items', (req, res) => { if (!rssFeedManager) { return res.status(400).json({ success: false, message: 'RSS feed manager not initialized' }); } const { filter } = req.query; let items; if (filter === 'undownloaded') { items = rssFeedManager.getUndownloadedItems(); } else { items = rssFeedManager.getAllItems(); } res.json({ success: true, data: items }); }); // Filter feed items app.post('/api/rss/filter', (req, res) => { if (!rssFeedManager) { return res.status(400).json({ success: false, message: 'RSS feed manager not initialized' }); } const filters = req.body; const filteredItems = rssFeedManager.filterItems(filters); res.json({ success: true, data: filteredItems }); }); // Fetch and update RSS feed app.post('/api/rss/update', async (req, res) => { if (!rssFeedManager) { return res.status(400).json({ success: false, message: 'RSS feed manager not initialized' }); } try { const result = await rssFeedManager.updateAllFeeds(); res.json({ success: true, data: result }); } catch (error) { res.status(500).json({ success: false, message: `Error updating feeds: ${error.message}` }); } }); // Download RSS item app.post('/api/rss/download', async (req, res) => { if (!rssFeedManager || !transmissionClient) { return res.status(400).json({ success: false, message: 'RSS feed manager or Transmission client not initialized' }); } const { itemId } = req.body; if (!itemId) { return res.status(400).json({ success: false, message: 'Item ID is required' }); } try { const items = rssFeedManager.getAllItems(); const item = items.find(i => i.id === itemId); if (!item) { return res.status(404).json({ success: false, message: `Item with ID ${itemId} not found` }); } const result = await rssFeedManager.downloadItem(item, transmissionClient); res.json({ success: result.success, message: result.success ? 'Item added to Transmission' : result.message, data: result.result }); } catch (error) { res.status(500).json({ success: false, message: `Error downloading item: ${error.message}` }); } }); // Post-Processor API routes //============================== // Start post-processor app.post('/api/post-processor/start', (req, res) => { if (!postProcessor) { return res.status(400).json({ success: false, message: 'Post-processor not initialized' }); } try { postProcessor.start(); // Update config config.autoProcessing = true; saveConfig(); res.json({ success: true, message: 'Post-processor started' }); } catch (error) { res.status(500).json({ success: false, message: `Error starting post-processor: ${error.message}` }); } }); // Stop post-processor app.post('/api/post-processor/stop', (req, res) => { if (!postProcessor) { return res.status(400).json({ success: false, message: 'Post-processor not initialized' }); } try { postProcessor.stop(); // Update config config.autoProcessing = false; saveConfig(); res.json({ success: true, message: 'Post-processor stopped' }); } catch (error) { res.status(500).json({ success: false, message: `Error stopping post-processor: ${error.message}` }); } }); // Get media library app.get('/api/media/library', (req, res) => { if (!postProcessor) { return res.status(400).json({ success: false, message: 'Post-processor not initialized' }); } const { query } = req.query; let library; if (query) { library = postProcessor.searchLibrary(query); } else { library = postProcessor.getLibrary(); } res.json({ success: true, data: library }); }); // Get library statistics app.get('/api/media/stats', (req, res) => { if (!postProcessor) { return res.status(400).json({ success: false, message: 'Post-processor not initialized' }); } const stats = postProcessor.getLibraryStats(); res.json({ success: true, data: stats }); }); // Serve static files app.use(express.static(path.join(__dirname, 'public'))); // Catch-all route for SPA app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // Initialize application async function init() { console.log('Initializing application...'); // Load configuration await loadConfig(); // Initialize services initTransmission(); initPostProcessor(); initRssFeedManager(); // Start the server app.listen(PORT, () => { console.log(`Transmission RSS Manager running on port ${PORT}`); }); } // Start the application init().catch(err => { console.error('Failed to initialize application:', err); }); // Handle graceful shutdown process.on('SIGINT', async () => { console.log('Shutting down...'); if (postProcessor) { postProcessor.stop(); } if (rssFeedManager) { rssFeedManager.stop(); await rssFeedManager.saveItems(); await rssFeedManager.saveConfig(); } await saveConfig(); console.log('Shutdown complete'); process.exit(0); }); EOF } # Create enhanced UI JavaScript echo "Creating enhanced-ui.js..." cp "${SCRIPT_DIR}/public/js/enhanced-ui.js" "$INSTALL_DIR/public/js/enhanced-ui.js" || { cat > $INSTALL_DIR/public/js/enhanced-ui.js << 'EOF' // RSS Feed Management Functions function addFeed() { // Create a modal dialog for adding a feed const modal = document.createElement('div'); modal.style.position = 'fixed'; modal.style.top = '0'; modal.style.left = '0'; modal.style.width = '100%'; modal.style.height = '100%'; modal.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; modal.style.display = 'flex'; modal.style.justifyContent = 'center'; modal.style.alignItems = 'center'; modal.style.zIndex = '1000'; // Create the modal content const modalContent = document.createElement('div'); modalContent.style.backgroundColor = 'white'; modalContent.style.padding = '20px'; modalContent.style.borderRadius = '8px'; modalContent.style.width = '500px'; modalContent.style.maxWidth = '90%'; modalContent.innerHTML = `