torrent-man/install-script.sh
Claude 9e544456db Initial commit with UI fixes for dark mode
This repository contains Transmission RSS Manager with the following changes:
- Fixed dark mode navigation tab visibility issue
- Improved text contrast in dark mode throughout the app
- Created dedicated dark-mode.css for better organization
- Enhanced JavaScript for dynamic styling in dark mode
- Added complete installation scripts

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-13 17:16:41 +00:00

1359 lines
41 KiB
Bash
Executable File

#!/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 in multiple locations
IS_UPDATE=false
POSSIBLE_CONFIG_LOCATIONS=(
"${SCRIPT_DIR}/config.json"
"/opt/transmission-rss-manager/config.json"
"/etc/transmission-rss-manager/config.json"
)
# Also check for service file - secondary indicator
if [ -f "/etc/systemd/system/transmission-rss-manager.service" ]; then
# Extract install directory from service file if it exists
SERVICE_INSTALL_DIR=$(grep "WorkingDirectory=" "/etc/systemd/system/transmission-rss-manager.service" | cut -d'=' -f2)
if [ -n "$SERVICE_INSTALL_DIR" ]; then
echo -e "${YELLOW}Found existing service at: $SERVICE_INSTALL_DIR${NC}"
POSSIBLE_CONFIG_LOCATIONS+=("$SERVICE_INSTALL_DIR/config.json")
fi
fi
# Check all possible locations
for CONFIG_PATH in "${POSSIBLE_CONFIG_LOCATIONS[@]}"; do
if [ -f "$CONFIG_PATH" ]; then
IS_UPDATE=true
echo -e "${YELLOW}Existing installation detected at: $CONFIG_PATH${NC}"
echo -e "${YELLOW}Running in update mode.${NC}"
echo -e "${GREEN}Your existing configuration will be preserved.${NC}"
# If the config is not in the current directory, store its location
if [ "$CONFIG_PATH" != "${SCRIPT_DIR}/config.json" ]; then
export EXISTING_CONFIG_PATH="$CONFIG_PATH"
export EXISTING_INSTALL_DIR="$(dirname "$CONFIG_PATH")"
echo -e "${YELLOW}Will update installation at: $EXISTING_INSTALL_DIR${NC}"
fi
break
fi
done
if [ "$IS_UPDATE" = "false" ]; then
echo -e "${GREEN}No existing installation detected. 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 <<EOF
{
"$REMOTE_DOWNLOAD_DIR": "$LOCAL_DOWNLOAD_DIR"
}
EOF
)
# Create the local directory
mkdir -p "$LOCAL_DOWNLOAD_DIR"
chown -R $USER:$USER "$LOCAL_DOWNLOAD_DIR"
# Ask if want to add more mappings
while true; do
read -p "Add another directory mapping? (y/n) [n]: " add_another
if [[ ! $add_another =~ ^[Yy]$ ]]; then
break
fi
read -p "Remote directory path: " remote_dir
read -p "Corresponding local directory path: " local_dir
if [ -n "$remote_dir" ] && [ -n "$local_dir" ]; then
# Update mapping JSON (remove the last "}" and add the new mapping)
TRANSMISSION_DIR_MAPPING="${TRANSMISSION_DIR_MAPPING%\}}, \"$remote_dir\": \"$local_dir\" }"
# Create the local directory
mkdir -p "$local_dir"
chown -R $USER:$USER "$local_dir"
echo -e "${GREEN}Mapping added: $remote_dir → $local_dir${NC}"
fi
done
# Set Transmission download dir for configuration
TRANSMISSION_DOWNLOAD_DIR=$REMOTE_DOWNLOAD_DIR
else
read -p "Transmission download directory [/var/lib/transmission-daemon/downloads]: " input_trans_dir
TRANSMISSION_DOWNLOAD_DIR=${input_trans_dir:-$TRANSMISSION_DOWNLOAD_DIR}
fi
echo
echo -e "${BOLD}Media Destination Configuration:${NC}"
read -p "Media destination base directory [/mnt/media]: " input_media_dir
MEDIA_DIR=${input_media_dir:-$MEDIA_DIR}
# Ask about enabling book/magazine sorting
echo
echo -e "${BOLD}Content Type Configuration:${NC}"
read -p "Enable book and magazine sorting? (y/n) [y]: " input_book_sorting
ENABLE_BOOK_SORTING=true
if [[ $input_book_sorting =~ ^[Nn]$ ]]; then
ENABLE_BOOK_SORTING=false
fi
echo
echo -e "${GREEN}Configuration complete!${NC}"
echo
}
EOL
# Create utils module
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(/<item>[\s\S]*?<\/item>/g) || [];
for (const itemXml of matches) {
const titleMatch = itemXml.match(/<title>(.*?)<\/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}"
# Skip Transmission configuration if we're in update mode
if [ "$IS_UPDATE" = "true" ] && [ -n "$EXISTING_CONFIG_PATH" ]; then
echo -e "${GREEN}Existing configuration detected, skipping Transmission configuration...${NC}"
# Extract Transmission remote setting from existing config
if [ -f "$EXISTING_CONFIG_PATH" ]; then
# Try to extract remoteConfig.isRemote value from config.json
if command -v grep &> /dev/null && command -v sed &> /dev/null; then
IS_REMOTE=$(grep -o '"isRemote":[^,}]*' "$EXISTING_CONFIG_PATH" | sed 's/"isRemote"://; s/[[:space:]]//g')
if [ "$IS_REMOTE" = "true" ]; then
export TRANSMISSION_REMOTE=true
echo -e "${GREEN}Using existing remote Transmission configuration.${NC}"
else
export TRANSMISSION_REMOTE=false
echo -e "${GREEN}Using existing local Transmission configuration.${NC}"
fi
else
# Default to false if we can't extract it
export TRANSMISSION_REMOTE=false
echo -e "${YELLOW}Could not determine Transmission remote setting, using local configuration.${NC}"
fi
fi
else
# 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
# If stdin is not a terminal (pipe or redirect), read from stdin
if [ ! -t 0 ]; then
# Save all input to a temporary file
INPUT_FILE=$(mktemp)
cat > "$INPUT_FILE"
# Read the first line as the remote selection
input_remote=$(awk 'NR==1{print}' "$INPUT_FILE")
echo "DEBUG: Non-interactive mode detected, read input: '$input_remote'"
# Keep the rest of the input for later use
tail -n +2 "$INPUT_FILE" > "${INPUT_FILE}.rest"
mv "${INPUT_FILE}.rest" "$INPUT_FILE"
else
read -p "Is Transmission running on a remote server? (y/n) [n]: " input_remote
fi
echo "DEBUG: Input received for remote in install-script.sh: '$input_remote'"
# Explicitly check for "y" or "Y" response
if [ "$input_remote" = "y" ] || [ "$input_remote" = "Y" ]; 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
fi
# If remote mode is selected and not an update, collect remote details here and pass to main installer
if [ "$TRANSMISSION_REMOTE" = "true" ] && [ "$IS_UPDATE" != "true" ]; then
# Get remote transmission details
if [ ! -t 0 ]; then
# Non-interactive mode - we already have input saved to INPUT_FILE
# from the previous step
# Read each line from the input file
TRANSMISSION_HOST=$(awk 'NR==1{print}' "$INPUT_FILE")
TRANSMISSION_PORT=$(awk 'NR==2{print}' "$INPUT_FILE")
TRANSMISSION_USER=$(awk 'NR==3{print}' "$INPUT_FILE")
TRANSMISSION_PASS=$(awk 'NR==4{print}' "$INPUT_FILE")
TRANSMISSION_RPC_PATH=$(awk 'NR==5{print}' "$INPUT_FILE")
REMOTE_DOWNLOAD_DIR=$(awk 'NR==6{print}' "$INPUT_FILE")
LOCAL_DOWNLOAD_DIR=$(awk 'NR==7{print}' "$INPUT_FILE")
# Use defaults for empty values
TRANSMISSION_HOST=${TRANSMISSION_HOST:-"localhost"}
TRANSMISSION_PORT=${TRANSMISSION_PORT:-"9091"}
TRANSMISSION_USER=${TRANSMISSION_USER:-""}
TRANSMISSION_PASS=${TRANSMISSION_PASS:-""}
TRANSMISSION_RPC_PATH=${TRANSMISSION_RPC_PATH:-"/transmission/rpc"}
REMOTE_DOWNLOAD_DIR=${REMOTE_DOWNLOAD_DIR:-"/var/lib/transmission-daemon/downloads"}
LOCAL_DOWNLOAD_DIR=${LOCAL_DOWNLOAD_DIR:-"/mnt/transmission-downloads"}
# Clean up
rm -f "$INPUT_FILE"
echo "DEBUG: Non-interactive mode with remote details:"
echo "DEBUG: Host: $TRANSMISSION_HOST, Port: $TRANSMISSION_PORT"
echo "DEBUG: Remote dir: $REMOTE_DOWNLOAD_DIR, Local dir: $LOCAL_DOWNLOAD_DIR"
else
# Interactive mode - ask for details
read -p "Remote Transmission host [localhost]: " TRANSMISSION_HOST
TRANSMISSION_HOST=${TRANSMISSION_HOST:-"localhost"}
read -p "Remote Transmission port [9091]: " TRANSMISSION_PORT
TRANSMISSION_PORT=${TRANSMISSION_PORT:-"9091"}
read -p "Remote Transmission username []: " TRANSMISSION_USER
TRANSMISSION_USER=${TRANSMISSION_USER:-""}
read -s -p "Remote Transmission password []: " TRANSMISSION_PASS
echo # Add a newline after password input
TRANSMISSION_PASS=${TRANSMISSION_PASS:-""}
read -p "Remote Transmission RPC path [/transmission/rpc]: " TRANSMISSION_RPC_PATH
TRANSMISSION_RPC_PATH=${TRANSMISSION_RPC_PATH:-"/transmission/rpc"}
# 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
read -p "Remote Transmission download directory [/var/lib/transmission-daemon/downloads]: " REMOTE_DOWNLOAD_DIR
REMOTE_DOWNLOAD_DIR=${REMOTE_DOWNLOAD_DIR:-"/var/lib/transmission-daemon/downloads"}
read -p "Local directory that corresponds to the remote download directory [/mnt/transmission-downloads]: " LOCAL_DOWNLOAD_DIR
LOCAL_DOWNLOAD_DIR=${LOCAL_DOWNLOAD_DIR:-"/mnt/transmission-downloads"}
fi
# Create the environment file with all remote details
cat > "${SCRIPT_DIR}/.env.install" << EOF
export TRANSMISSION_REMOTE=$TRANSMISSION_REMOTE
export TRANSMISSION_HOST="$TRANSMISSION_HOST"
export TRANSMISSION_PORT="$TRANSMISSION_PORT"
export TRANSMISSION_USER="$TRANSMISSION_USER"
export TRANSMISSION_PASS="$TRANSMISSION_PASS"
export TRANSMISSION_RPC_PATH="$TRANSMISSION_RPC_PATH"
export REMOTE_DOWNLOAD_DIR="$REMOTE_DOWNLOAD_DIR"
export LOCAL_DOWNLOAD_DIR="$LOCAL_DOWNLOAD_DIR"
EOF
else
# Local mode - simpler environment file
echo "export TRANSMISSION_REMOTE=$TRANSMISSION_REMOTE" > "${SCRIPT_DIR}/.env.install"
fi
chmod +x "${SCRIPT_DIR}/.env.install"
# Ensure the environment file is world-readable to avoid permission issues
chmod 644 "${SCRIPT_DIR}/.env.install"
# If we're in update mode, add the existing installation path to the environment file
if [ "$IS_UPDATE" = "true" ] && [ -n "$EXISTING_CONFIG_PATH" ]; then
echo "export EXISTING_CONFIG_PATH=\"$EXISTING_CONFIG_PATH\"" >> "${SCRIPT_DIR}/.env.install"
echo "export EXISTING_INSTALL_DIR=\"$EXISTING_INSTALL_DIR\"" >> "${SCRIPT_DIR}/.env.install"
echo "export IS_UPDATE=true" >> "${SCRIPT_DIR}/.env.install"
fi
# 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"
export TRANSMISSION_REMOTE
"${SCRIPT_DIR}/main-installer.sh"