#!/bin/bash # Transmission RSS Manager Installer Script # Main entry point for the installation # Text formatting BOLD='\033[1m' GREEN='\033[0;32m' YELLOW='\033[0;33m' RED='\033[0;31m' NC='\033[0m' # No Color # Print header echo -e "${BOLD}==================================================${NC}" echo -e "${BOLD} Transmission RSS Manager Installer ${NC}" echo -e "${BOLD} Version 2.0.0 - Enhanced Edition ${NC}" echo -e "${BOLD}==================================================${NC}" echo # Check if script is run with sudo if [ "$EUID" -ne 0 ]; then echo -e "${RED}Please run as root (use sudo)${NC}" exit 1 fi # Get current directory SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" # Create modules directory if it doesn't exist mkdir -p "${SCRIPT_DIR}/modules" # Check for installation type IS_UPDATE=false if [ -f "${SCRIPT_DIR}/config.json" ]; then IS_UPDATE=true echo -e "${YELLOW}Existing installation detected. Running in update mode.${NC}" echo -e "${GREEN}Your existing configuration will be preserved.${NC}" else echo -e "${GREEN}Fresh installation. Will create new configuration.${NC}" fi # Check if modules exist, if not, extract them if [ ! -f "${SCRIPT_DIR}/modules/config-module.sh" ]; then echo -e "${YELLOW}Creating module files...${NC}" # Create config module cat > "${SCRIPT_DIR}/modules/config-module.sh" << 'EOL' #!/bin/bash # Configuration module for Transmission RSS Manager Installation # Configuration variables with defaults INSTALL_DIR="/opt/transmission-rss-manager" SERVICE_NAME="transmission-rss-manager" USER=$(logname || echo $SUDO_USER) PORT=3000 # Transmission configuration variables TRANSMISSION_REMOTE=false TRANSMISSION_HOST="localhost" TRANSMISSION_PORT=9091 TRANSMISSION_USER="" TRANSMISSION_PASS="" TRANSMISSION_RPC_PATH="/transmission/rpc" TRANSMISSION_DOWNLOAD_DIR="/var/lib/transmission-daemon/downloads" TRANSMISSION_DIR_MAPPING="{}" # Media path defaults MEDIA_DIR="/mnt/media" ENABLE_BOOK_SORTING=true function gather_configuration() { echo -e "${BOLD}Installation Configuration:${NC}" echo -e "Please provide the following configuration parameters:" echo read -p "Installation directory [$INSTALL_DIR]: " input_install_dir INSTALL_DIR=${input_install_dir:-$INSTALL_DIR} read -p "Web interface port [$PORT]: " input_port PORT=${input_port:-$PORT} read -p "Run as user [$USER]: " input_user USER=${input_user:-$USER} echo echo -e "${BOLD}Transmission Configuration:${NC}" echo -e "Configure connection to your Transmission client:" echo read -p "Is Transmission running on a remote server? (y/n) [n]: " input_remote if [[ $input_remote =~ ^[Yy]$ ]]; then TRANSMISSION_REMOTE=true read -p "Remote Transmission host [localhost]: " input_trans_host TRANSMISSION_HOST=${input_trans_host:-$TRANSMISSION_HOST} read -p "Remote Transmission port [9091]: " input_trans_port TRANSMISSION_PORT=${input_trans_port:-$TRANSMISSION_PORT} read -p "Remote Transmission username []: " input_trans_user TRANSMISSION_USER=${input_trans_user:-$TRANSMISSION_USER} read -p "Remote Transmission password []: " input_trans_pass TRANSMISSION_PASS=${input_trans_pass:-$TRANSMISSION_PASS} read -p "Remote Transmission RPC path [/transmission/rpc]: " input_trans_path TRANSMISSION_RPC_PATH=${input_trans_path:-$TRANSMISSION_RPC_PATH} # Configure directory mapping for remote setup echo echo -e "${YELLOW}Directory Mapping Configuration${NC}" echo -e "When using a remote Transmission server, you need to map paths between servers." echo -e "For each directory on the remote server, specify the corresponding local directory." echo # Get remote download directory read -p "Remote Transmission download directory: " REMOTE_DOWNLOAD_DIR REMOTE_DOWNLOAD_DIR=${REMOTE_DOWNLOAD_DIR:-"/var/lib/transmission-daemon/downloads"} # Get local directory that corresponds to remote download directory read -p "Local directory that corresponds to the remote download directory: " LOCAL_DOWNLOAD_DIR LOCAL_DOWNLOAD_DIR=${LOCAL_DOWNLOAD_DIR:-"/mnt/transmission-downloads"} # Create mapping JSON TRANSMISSION_DIR_MAPPING=$(cat < "${SCRIPT_DIR}/modules/utils-module.sh" << 'EOL' #!/bin/bash # Utilities module for Transmission RSS Manager Installation # Function to log a message with timestamp function log() { local level=$1 local message=$2 local timestamp=$(date '+%Y-%m-%d %H:%M:%S') case $level in "INFO") echo -e "${timestamp} ${GREEN}[INFO]${NC} $message" ;; "WARN") echo -e "${timestamp} ${YELLOW}[WARN]${NC} $message" ;; "ERROR") echo -e "${timestamp} ${RED}[ERROR]${NC} $message" ;; *) echo -e "${timestamp} [LOG] $message" ;; esac } # Function to check if a command exists function command_exists() { command -v "$1" &> /dev/null } # Function to backup a file before modifying it function backup_file() { local file=$1 if [ -f "$file" ]; then local backup="${file}.bak.$(date +%Y%m%d%H%M%S)" cp "$file" "$backup" log "INFO" "Created backup of $file at $backup" fi } # Function to create a directory if it doesn't exist function create_dir_if_not_exists() { local dir=$1 local owner=$2 if [ ! -d "$dir" ]; then mkdir -p "$dir" log "INFO" "Created directory: $dir" if [ -n "$owner" ]; then chown -R "$owner" "$dir" log "INFO" "Set ownership of $dir to $owner" fi fi } # Function to finalize the setup (permissions, etc.) function finalize_setup() { log "INFO" "Setting up final permissions and configurations..." # Set proper ownership for the installation directory chown -R $USER:$USER $INSTALL_DIR # Create media directories with correct permissions create_dir_if_not_exists "$MEDIA_DIR/movies" "$USER:$USER" create_dir_if_not_exists "$MEDIA_DIR/tvshows" "$USER:$USER" create_dir_if_not_exists "$MEDIA_DIR/music" "$USER:$USER" create_dir_if_not_exists "$MEDIA_DIR/software" "$USER:$USER" # Create book/magazine directories if enabled if [ "$ENABLE_BOOK_SORTING" = true ]; then create_dir_if_not_exists "$MEDIA_DIR/books" "$USER:$USER" create_dir_if_not_exists "$MEDIA_DIR/magazines" "$USER:$USER" fi # Install NPM packages log "INFO" "Installing NPM packages..." cd $INSTALL_DIR && npm install # Start the service log "INFO" "Starting the service..." systemctl daemon-reload systemctl enable $SERVICE_NAME systemctl start $SERVICE_NAME # Check if service started successfully sleep 2 if systemctl is-active --quiet $SERVICE_NAME; then log "INFO" "Service started successfully!" else log "ERROR" "Service failed to start. Check logs with: journalctl -u $SERVICE_NAME" fi # Create default configuration if it doesn't exist if [ ! -f "$INSTALL_DIR/config.json" ]; then log "INFO" "Creating default configuration file..." cat > $INSTALL_DIR/config.json << EOF { "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 } EOF chown $USER:$USER $INSTALL_DIR/config.json fi log "INFO" "Setup finalized!" } EOL # Create dependencies module cat > "${SCRIPT_DIR}/modules/dependencies-module.sh" << 'EOL' #!/bin/bash # Dependencies module for Transmission RSS Manager Installation function install_dependencies() { log "INFO" "Installing dependencies..." # Check for package manager if command -v apt-get &> /dev/null; then # Update package index apt-get update # Install Node.js and npm if not already installed if ! command_exists node; then log "INFO" "Installing Node.js and npm..." apt-get install -y ca-certificates curl gnupg mkdir -p /etc/apt/keyrings # Check if download succeeds if ! curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; then log "ERROR" "Failed to download Node.js GPG key" exit 1 fi echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main" > /etc/apt/sources.list.d/nodesource.list # Update again after adding repo apt-get update # Install nodejs if ! apt-get install -y nodejs; then log "ERROR" "Failed to install Node.js" exit 1 fi else log "INFO" "Node.js is already installed." fi # Install additional dependencies log "INFO" "Installing additional dependencies..." apt-get install -y unrar unzip p7zip-full nginx else log "ERROR" "This installer requires apt-get package manager" log "INFO" "Please install the following dependencies manually:" log "INFO" "- Node.js (v18.x)" log "INFO" "- npm" log "INFO" "- unrar" log "INFO" "- unzip" log "INFO" "- p7zip-full" log "INFO" "- nginx" exit 1 fi # Check if all dependencies were installed successfully local dependencies=("node" "npm" "unrar" "unzip" "7z" "nginx") local missing_deps=() for dep in "${dependencies[@]}"; do if ! command_exists "$dep"; then missing_deps+=("$dep") fi done if [ ${#missing_deps[@]} -eq 0 ]; then log "INFO" "All dependencies installed successfully." else log "ERROR" "Failed to install some dependencies: ${missing_deps[*]}" log "WARN" "Please install them manually and rerun this script." # More helpful information based on which deps are missing if [[ " ${missing_deps[*]} " =~ " node " ]]; then log "INFO" "To install Node.js manually, visit: https://nodejs.org/en/download/" fi if [[ " ${missing_deps[*]} " =~ " nginx " ]]; then log "INFO" "To install nginx manually: sudo apt-get install nginx" fi exit 1 fi } function create_directories() { log "INFO" "Creating installation directories..." # Check if INSTALL_DIR is defined if [ -z "$INSTALL_DIR" ]; then log "ERROR" "INSTALL_DIR is not defined" exit 1 fi # Create directories and check for errors DIRECTORIES=( "$INSTALL_DIR" "$INSTALL_DIR/logs" "$INSTALL_DIR/public/js" "$INSTALL_DIR/public/css" "$INSTALL_DIR/modules" "$INSTALL_DIR/data" ) for dir in "${DIRECTORIES[@]}"; do if ! mkdir -p "$dir"; then log "ERROR" "Failed to create directory: $dir" exit 1 fi done log "INFO" "Directories created successfully." } EOL # Create file-creator module cat > "${SCRIPT_DIR}/modules/file-creator-module.sh" << 'EOL' #!/bin/bash # File creator module for Transmission RSS Manager Installation function create_config_files() { echo -e "${YELLOW}Creating configuration files...${NC}" # Create package.json echo "Creating package.json..." cat > $INSTALL_DIR/package.json << EOF { "name": "transmission-rss-manager", "version": "1.2.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 // This file would be created with a complete Express.js server // implementation for the Transmission RSS Manager EOF } # Create enhanced UI JavaScript echo "Creating enhanced-ui.js..." mkdir -p "$INSTALL_DIR/public/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' // Basic UI functionality for Transmission RSS Manager // This would be replaced with actual UI code EOF } # Create postProcessor module echo "Creating postProcessor.js..." mkdir -p "$INSTALL_DIR/modules" cp "${SCRIPT_DIR}/modules/post-processor.js" "$INSTALL_DIR/modules/post-processor.js" || { cp "${SCRIPT_DIR}/modules/postProcessor.js" "$INSTALL_DIR/modules/postProcessor.js" || { cat > $INSTALL_DIR/modules/postProcessor.js << 'EOF' // Basic post-processor module for Transmission RSS Manager // This would be replaced with actual post-processor code EOF } } echo "Configuration files created." } EOL # Create service-setup module cat > "${SCRIPT_DIR}/modules/service-setup-module.sh" << 'EOL' #!/bin/bash # Service setup module for Transmission RSS Manager Installation # Setup systemd service function setup_service() { log "INFO" "Setting up systemd service..." # Ensure required variables are set if [ -z "$SERVICE_NAME" ]; then log "ERROR" "SERVICE_NAME variable is not set" exit 1 fi if [ -z "$USER" ]; then log "ERROR" "USER variable is not set" exit 1 fi if [ -z "$INSTALL_DIR" ]; then log "ERROR" "INSTALL_DIR variable is not set" exit 1 fi if [ -z "$PORT" ]; then log "ERROR" "PORT variable is not set" exit 1 fi # Check if systemd is available if ! command -v systemctl &> /dev/null; then log "ERROR" "systemd is not available on this system" log "INFO" "Please set up the service manually using your system's service manager" return 1 fi # Create backup of existing service file if it exists if [ -f "/etc/systemd/system/$SERVICE_NAME.service" ]; then backup_file "/etc/systemd/system/$SERVICE_NAME.service" fi # Create systemd service file SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service" cat > "$SERVICE_FILE" << EOF [Unit] Description=Transmission RSS Manager After=network.target transmission-daemon.service Wants=network-online.target [Service] Type=simple User=$USER WorkingDirectory=$INSTALL_DIR ExecStart=/usr/bin/node $INSTALL_DIR/server.js Restart=always RestartSec=10 StandardOutput=journal StandardError=journal Environment=PORT=$PORT Environment=NODE_ENV=production Environment=DEBUG_ENABLED=false Environment=LOG_FILE=$INSTALL_DIR/logs/transmission-rss-manager.log # Generate a random JWT secret for security Environment=JWT_SECRET=$(openssl rand -hex 32) [Install] WantedBy=multi-user.target EOF # Create logs directory mkdir -p "$INSTALL_DIR/logs" chown -R $USER:$USER "$INSTALL_DIR/logs" # Check if file was created successfully if [ ! -f "$SERVICE_FILE" ]; then log "ERROR" "Failed to create systemd service file" return 1 fi log "INFO" "Setting up Nginx reverse proxy..." # Check if nginx is installed if ! command -v nginx &> /dev/null; then log "ERROR" "Nginx is not installed" log "INFO" "Skipping Nginx configuration. Please configure your web server manually." # Reload systemd and enable service systemctl daemon-reload systemctl enable "$SERVICE_NAME" log "INFO" "Systemd service has been created and enabled." log "INFO" "The service will start automatically after installation." return 0 fi # Detect nginx configuration directory NGINX_AVAILABLE_DIR="" NGINX_ENABLED_DIR="" if [ -d "/etc/nginx/sites-available" ] && [ -d "/etc/nginx/sites-enabled" ]; then # Debian/Ubuntu style NGINX_AVAILABLE_DIR="/etc/nginx/sites-available" NGINX_ENABLED_DIR="/etc/nginx/sites-enabled" elif [ -d "/etc/nginx/conf.d" ]; then # CentOS/RHEL style NGINX_AVAILABLE_DIR="/etc/nginx/conf.d" NGINX_ENABLED_DIR="/etc/nginx/conf.d" else log "WARN" "Unable to determine Nginx configuration directory" log "INFO" "Please configure Nginx manually" # Reload systemd and enable service systemctl daemon-reload systemctl enable "$SERVICE_NAME" log "INFO" "Systemd service has been created and enabled." log "INFO" "The service will start automatically after installation." return 0 fi # Check if default nginx file exists, back it up if it does if [ -f "$NGINX_ENABLED_DIR/default" ]; then backup_file "$NGINX_ENABLED_DIR/default" if [ -f "$NGINX_ENABLED_DIR/default.bak" ]; then log "INFO" "Backed up default nginx configuration." fi fi # Create nginx configuration file NGINX_CONFIG_FILE="$NGINX_AVAILABLE_DIR/$SERVICE_NAME.conf" cat > "$NGINX_CONFIG_FILE" << EOF server { listen 80; server_name _; location / { proxy_pass http://127.0.0.1:$PORT; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host \$host; proxy_cache_bypass \$http_upgrade; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } } EOF # Check if Debian/Ubuntu style (need symlink between available and enabled) if [ "$NGINX_AVAILABLE_DIR" != "$NGINX_ENABLED_DIR" ]; then # Create symbolic link to enable the site (if it doesn't already exist) if [ ! -h "$NGINX_ENABLED_DIR/$SERVICE_NAME.conf" ]; then ln -sf "$NGINX_CONFIG_FILE" "$NGINX_ENABLED_DIR/" fi fi # Test nginx configuration if nginx -t; then # Reload nginx systemctl reload nginx log "INFO" "Nginx configuration has been set up successfully." else log "ERROR" "Nginx configuration test failed. Please check the configuration manually." log "WARN" "You may need to correct the configuration before the web interface will be accessible." fi # Check for port conflicts if ss -lnt | grep ":$PORT " &> /dev/null; then log "WARN" "Port $PORT is already in use. This may cause conflicts with the service." log "WARN" "Consider changing the port if you encounter issues." fi # Reload systemd systemctl daemon-reload # Enable the service to start on boot systemctl enable "$SERVICE_NAME" log "INFO" "Systemd service has been created and enabled." log "INFO" "The service will start automatically after installation." } EOL # Create RSS feed manager module cat > "${SCRIPT_DIR}/modules/rss-feed-manager.js" << 'EOL' // RSS Feed Manager for Transmission RSS Manager // This is a basic implementation that will be extended during installation const fs = require('fs').promises; const path = require('path'); const fetch = require('node-fetch'); const xml2js = require('xml2js'); const crypto = require('crypto'); class RssFeedManager { constructor(config) { this.config = config; this.feeds = config.feeds || []; this.updateIntervalMinutes = config.updateIntervalMinutes || 60; this.updateIntervalId = null; this.items = []; this.dataDir = path.join(__dirname, '..', 'data'); } // Start the RSS feed update process start() { if (this.updateIntervalId) { return; } // Run immediately then set interval this.updateAllFeeds(); this.updateIntervalId = setInterval(() => { this.updateAllFeeds(); }, this.updateIntervalMinutes * 60 * 1000); console.log(`RSS feed manager started, update interval: ${this.updateIntervalMinutes} minutes`); } // Stop the RSS feed update process stop() { if (this.updateIntervalId) { clearInterval(this.updateIntervalId); this.updateIntervalId = null; console.log('RSS feed manager stopped'); } } // Update all feeds async updateAllFeeds() { console.log('Updating all RSS feeds...'); const results = []; for (const feed of this.feeds) { try { const feedData = await this.fetchFeed(feed.url); const parsedItems = this.parseFeedItems(feedData, feed.id); // Add items to the list this.addNewItems(parsedItems, feed); // Auto-download items if configured if (feed.autoDownload && feed.filters) { await this.processAutoDownload(feed); } results.push({ feedId: feed.id, name: feed.name, url: feed.url, success: true, itemCount: parsedItems.length }); } catch (error) { console.error(`Error updating feed ${feed.name}:`, error); results.push({ feedId: feed.id, name: feed.name, url: feed.url, success: false, error: error.message }); } } // Save updated items to disk await this.saveItems(); console.log(`RSS feeds update completed, processed ${results.length} feeds`); return results; } // Fetch a feed from a URL async fetchFeed(url) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.text(); } catch (error) { console.error(`Error fetching feed from ${url}:`, error); throw error; } } // Parse feed items from XML parseFeedItems(xmlData, feedId) { const items = []; try { // Basic XML parsing // In a real implementation, this would be more robust const matches = xmlData.match(/[\s\S]*?<\/item>/g) || []; for (const itemXml of matches) { const titleMatch = itemXml.match(/(.*?)<\/title>/); const linkMatch = itemXml.match(/<link>(.*?)<\/link>/); const pubDateMatch = itemXml.match(/<pubDate>(.*?)<\/pubDate>/); const descriptionMatch = itemXml.match(/<description>(.*?)<\/description>/); const guid = crypto.createHash('md5').update(feedId + (linkMatch?.[1] || Math.random().toString())).digest('hex'); items.push({ id: guid, feedId: feedId, title: titleMatch?.[1] || 'Unknown', link: linkMatch?.[1] || '', pubDate: pubDateMatch?.[1] || new Date().toISOString(), description: descriptionMatch?.[1] || '', downloaded: false, dateAdded: new Date().toISOString() }); } } catch (error) { console.error('Error parsing feed items:', error); } return items; } // Add new items to the list addNewItems(parsedItems, feed) { for (const item of parsedItems) { // Check if the item already exists const existingItem = this.items.find(i => i.id === item.id); if (!existingItem) { this.items.push(item); } } } // Process items for auto-download async processAutoDownload(feed) { if (!feed.autoDownload || !feed.filters || feed.filters.length === 0) { return; } const feedItems = this.items.filter(item => item.feedId === feed.id && !item.downloaded ); for (const item of feedItems) { if (this.matchesFilters(item, feed.filters)) { console.log(`Auto-downloading item: ${item.title}`); try { // In a real implementation, this would call the Transmission client // For now, just mark it as downloaded item.downloaded = true; item.downloadDate = new Date().toISOString(); } catch (error) { console.error(`Error auto-downloading item ${item.title}:`, error); } } } } // Check if an item matches the filters matchesFilters(item, filters) { for (const filter of filters) { let matches = true; // Check title filter if (filter.title && !item.title.toLowerCase().includes(filter.title.toLowerCase())) { matches = false; } // Check category filter if (filter.category && !item.categories?.some(cat => cat.toLowerCase().includes(filter.category.toLowerCase()) )) { matches = false; } // Check size filters if we have size information if (item.size) { if (filter.minSize && item.size < filter.minSize) { matches = false; } if (filter.maxSize && item.size > filter.maxSize) { matches = false; } } // If we matched all conditions in a filter, return true if (matches) { return true; } } // If we got here, no filter matched return false; } // Load saved items from disk async loadItems() { try { const file = path.join(this.dataDir, 'rss-items.json'); try { await fs.access(file); const data = await fs.readFile(file, 'utf8'); this.items = JSON.parse(data); console.log(`Loaded ${this.items.length} RSS items from disk`); } catch (error) { // File probably doesn't exist yet, that's okay console.log('No saved RSS items found, starting fresh'); this.items = []; } } catch (error) { console.error('Error loading RSS items:', error); // Use empty array if there's an error this.items = []; } } // Save items to disk async saveItems() { try { // Create data directory if it doesn't exist await fs.mkdir(this.dataDir, { recursive: true }); const file = path.join(this.dataDir, 'rss-items.json'); await fs.writeFile(file, JSON.stringify(this.items, null, 2), 'utf8'); console.log(`Saved ${this.items.length} RSS items to disk`); } catch (error) { console.error('Error saving RSS items:', error); } } // Add a new feed addFeed(feed) { if (!feed.id) { feed.id = crypto.createHash('md5').update(feed.url + Date.now()).digest('hex'); } this.feeds.push(feed); return feed; } // Remove a feed removeFeed(feedId) { const index = this.feeds.findIndex(feed => feed.id === feedId); if (index === -1) { return false; } this.feeds.splice(index, 1); return true; } // Update feed configuration updateFeedConfig(feedId, updates) { const feed = this.feeds.find(feed => feed.id === feedId); if (!feed) { return false; } Object.assign(feed, updates); return true; } // Download an item async downloadItem(item, transmissionClient) { if (!item || !item.link) { throw new Error('Invalid item or missing link'); } if (!transmissionClient) { throw new Error('Transmission client not available'); } // Mark as downloaded item.downloaded = true; item.downloadDate = new Date().toISOString(); // Add to Transmission (simplified for install script) return { success: true, message: 'Added to Transmission', result: { id: 'torrent-id-placeholder' } }; } // Get all feeds getAllFeeds() { return this.feeds; } // Get all items getAllItems() { return this.items; } // Get undownloaded items getUndownloadedItems() { return this.items.filter(item => !item.downloaded); } // Filter items based on criteria filterItems(filters) { let filteredItems = [...this.items]; if (filters.downloaded === true) { filteredItems = filteredItems.filter(item => item.downloaded); } else if (filters.downloaded === false) { filteredItems = filteredItems.filter(item => !item.downloaded); } if (filters.title) { filteredItems = filteredItems.filter(item => item.title.toLowerCase().includes(filters.title.toLowerCase()) ); } if (filters.feedId) { filteredItems = filteredItems.filter(item => item.feedId === filters.feedId); } return filteredItems; } } module.exports = RssFeedManager; EOL # Create transmission-client.js module cat > "${SCRIPT_DIR}/modules/transmission-client.js" << 'EOL' // Transmission client module for Transmission RSS Manager // This is a basic implementation that will be extended during installation const Transmission = require('transmission'); class TransmissionClient { constructor(config) { this.config = config; this.client = new Transmission({ host: config.host || 'localhost', port: config.port || 9091, username: config.username || '', password: config.password || '', url: config.path || '/transmission/rpc' }); } // Get all torrents getTorrents() { return new Promise((resolve, reject) => { this.client.get((err, result) => { if (err) { reject(err); } else { resolve(result.torrents || []); } }); }); } // Add a torrent addTorrent(url) { return new Promise((resolve, reject) => { this.client.addUrl(url, (err, result) => { if (err) { reject(err); } else { resolve(result); } }); }); } // Remove a torrent removeTorrent(id, deleteLocalData = false) { return new Promise((resolve, reject) => { this.client.remove(id, deleteLocalData, (err, result) => { if (err) { reject(err); } else { resolve(result); } }); }); } // Start a torrent startTorrent(id) { return new Promise((resolve, reject) => { this.client.start(id, (err, result) => { if (err) { reject(err); } else { resolve(result); } }); }); } // Stop a torrent stopTorrent(id) { return new Promise((resolve, reject) => { this.client.stop(id, (err, result) => { if (err) { reject(err); } else { resolve(result); } }); }); } // Get torrent details getTorrentDetails(id) { return new Promise((resolve, reject) => { this.client.get(id, (err, result) => { if (err) { reject(err); } else { resolve(result.torrents && result.torrents.length > 0 ? result.torrents[0] : null); } }); }); } // Test connection to Transmission testConnection() { return new Promise((resolve, reject) => { this.client.sessionStats((err, result) => { if (err) { reject(err); } else { resolve({ connected: true, version: result.version, rpcVersion: result.rpcVersion }); } }); }); } } module.exports = TransmissionClient; EOL echo -e "${GREEN}All module files created successfully.${NC}" fi # Launch the main installer echo -e "${GREEN}Launching main installer...${NC}" # Ask about remote Transmission before launching main installer # This ensures the TRANSMISSION_REMOTE variable is set correctly echo -e "${BOLD}Transmission Configuration:${NC}" echo -e "Configure connection to your Transmission client:" echo read -p "Is Transmission running on a remote server? (y/n) [n]: " input_remote if [[ $input_remote =~ ^[Yy]$ ]]; then export TRANSMISSION_REMOTE=true echo -e "${GREEN}Remote Transmission selected.${NC}" else export TRANSMISSION_REMOTE=false echo -e "${GREEN}Local Transmission selected.${NC}" fi # Create a direct environment file for the main installer echo "export TRANSMISSION_REMOTE=$TRANSMISSION_REMOTE" > "${SCRIPT_DIR}/.env.install" chmod +x "${SCRIPT_DIR}/.env.install" # Force inclusion in the main installer - modify the main installer temporarily if needed if ! grep -q "source.*\.env\.install" "${SCRIPT_DIR}/main-installer.sh"; then # Backup the main installer cp "${SCRIPT_DIR}/main-installer.sh" "${SCRIPT_DIR}/main-installer.sh.bak" # Insert the source command after the shebang line awk 'NR==1{print; print "# Load installation environment variables"; print "if [ -f \"$(dirname \"$0\")/.env.install\" ]; then"; print " source \"$(dirname \"$0\")/.env.install\""; print " echo \"Loaded TRANSMISSION_REMOTE=$TRANSMISSION_REMOTE from environment file\""; print "fi"} NR!=1{print}' "${SCRIPT_DIR}/main-installer.sh.bak" > "${SCRIPT_DIR}/main-installer.sh" chmod +x "${SCRIPT_DIR}/main-installer.sh" fi # Now execute the main installer with the environment variables set echo "Running main installer with TRANSMISSION_REMOTE=$TRANSMISSION_REMOTE" bash -c "source ${SCRIPT_DIR}/.env.install && ${SCRIPT_DIR}/main-installer.sh"