diff --git a/README.md b/README.md index 8335be9..733c00f 100644 --- a/README.md +++ b/README.md @@ -1,136 +1,243 @@ -

Torrent Mover v8.0

- -
-

Description

-

- Torrent Mover is a Bash script designed to automate the processing of completed torrents in Transmission. - It moves or copies downloaded files from a Transmission‑reported download location to designated destination directories on your system. - This enhanced version includes robust locking, advanced error handling, parallel processing, configurable path mapping, improved archive extraction, - and optional file integrity verification. -

-
- -
-

Features

- -
- -
-

Requirements

- -
- -
-

Installation

-
    -
  1. Download the Script: Save the script (e.g., torrent-mover.sh) to your desired location (e.g., /usr/local/bin/).
  2. -
  3. Make It Executable: -
    chmod +x /usr/local/bin/torrent-mover.sh
    -
  4. -
  5. Create/Edit the Configuration File: The script expects a configuration file at /etc/torrent/mover.conf. See the configuration section below.
  6. -
-
- -
-

Configuration

-

Edit or create /etc/torrent/mover.conf with the following content:

-
-# Transmission settings
-TRANSMISSION_IP="192.168.1.100"         # Replace with your Transmission server's IP
-TRANSMISSION_PORT="9091"                # Replace with your Transmission server's port
-TRANSMISSION_USER="your_username"       # Transmission username (if set)
-TRANSMISSION_PASSWORD="your_password"   # Transmission password (if set)
+# Torrent Mover v8.0
 
-# Path mapping settings
-TRANSMISSION_PATH_PREFIX="/downloads"
-LOCAL_PATH_PREFIX="/mnt/dsnas2"
+## Description
 
-# Destination directories
-DIR_GAMES_DST="/mnt/dsnas1/Games"
-DIR_APPS_DST="/mnt/dsnas1/Apps"
-DIR_MOVIES_DST="/mnt/dsnas1/Movies"
-DIR_BOOKS_DST="/mnt/dsnas1/Books"
-DEFAULT_DST="/mnt/dsnas1/Other"
+**Torrent Mover** is a Bash script designed to automate the processing of completed torrents in Transmission.
+It moves or copies downloaded files from a Transmission‑reported download location to designated destination directories on your system.
+This enhanced version includes a modular architecture, dedicated security user, robust locking, advanced error handling with retry capabilities, 
+parallel processing, configurable path mapping, improved archive extraction, and optional file integrity verification.
 
-# Additional storage directories (comma-separated list)
-STORAGE_DIRS="/mnt/dsnas/Movies"
+The system seamlessly organizes content into appropriate directories using smart pattern matching and customizable category detection, helping you maintain a well-structured media library with minimal manual intervention.
 
-# Performance settings
-PARALLEL_THREADS="32"
-PARALLEL_PROCESSING=1
+## Features
 
-# Operation mode: "move" or "copy"
-COPY_MODE="copy"
+### Core Features
+- **Automatic Torrent Processing:** Monitors Transmission for completed torrents and processes them based on configurable seeding criteria.
+- **Configurable Path Mapping:** Uses Transmission's reported download path and maps it to your local file system via configurable settings.
+- **Archive Extraction:** Extracts archives (RAR, ZIP, 7z) into subdirectories at the destination—preserving internal structure—while retaining the archive in the source until seeding criteria are met.
+- **Directory Deduplication:** Prevents re‑processing the same source directory if multiple torrents reference it.
 
+### Advanced Content Organization
+- **Smart Content Categorization:** Uses both pattern matching and directory name detection to properly categorize content.
+- **Regex Pattern Matching:** Define custom regex patterns to precisely organize content into subcategories (documentaries, anime, etc.).
+- **Multi-Library Support:** Manage content across multiple storage locations with different organization schemes.
+
+### Enhanced Security & Reliability
+- **Dedicated Non-Root User:** Uses a dedicated service user with minimal permissions for enhanced security.
+- **Error Recovery:** Includes retry mechanisms with configurable attempts and delay for network operations.
+- **Data Integrity Protection:** Optionally verifies file integrity by comparing MD5 checksums after transfer.
+- **Robust Locking:** Employs `flock` to ensure that only one instance of the script runs at a time.
+
+### Performance & Engineering
+- **Modular Architecture:** Code is organized into separate modules for better maintainability and extensibility.
+- **Parallel File Operations:** Utilizes GNU Parallel for moving, copying, and generating checksums, enabling efficient multi-threaded processing.
+- **Advanced Error Handling & Logging:** Global error handler and detailed logging (with DEBUG mode support). Optionally, logs to syslog.
+
+## Requirements
+
+- Bash
+- transmission-remote
+- GNU Parallel
+- unrar, unzip, 7z
+- bc
+
+## Installation
+
+1. Run the installation script as root:
+   ```
+   sudo ./install.sh
+   ```
+
+2. The script will:
+   - Install all necessary dependencies
+   - Create a dedicated non-root user for security
+   - Set up the configuration file in `/etc/torrent/mover.conf`
+   - Install systemd service and timer
+   - Configure file permissions and log rotation
+
+3. Enable the service to run every 15 minutes:
+   ```
+   sudo systemctl enable --now torrent-mover.timer
+   ```
+
+## Configuration
+
+Edit the configuration file at `/etc/torrent/mover.conf` to customize the behavior of Torrent Mover:
+
+### Connection Configuration
+```bash
+# Transmission connection settings
+TRANSMISSION_IP="192.168.1.100"       # IP address of your Transmission server
+TRANSMISSION_PORT="9091"              # RPC port for Transmission
+TRANSMISSION_USER="your_username"     # Username for authentication (if enabled)
+TRANSMISSION_PASSWORD="your_password" # Password for authentication (if enabled)
+
+# Path mapping configuration
+TRANSMISSION_PATH_PREFIX="/downloads" # Path prefix reported by Transmission
+LOCAL_PATH_PREFIX="/mnt/dsnas2"       # Corresponding local path prefix
+```
+
+### Content Organization
+```bash
+# Primary content destination directories
+DIR_GAMES_DST="/mnt/dsnas1/Games"     # Games destination
+DIR_APPS_DST="/mnt/dsnas1/Apps"       # Applications destination
+DIR_MOVIES_DST="/mnt/dsnas1/Movies"   # Movies destination
+DIR_BOOKS_DST="/mnt/dsnas1/Books"     # Books/eBooks destination
+DIR_TV_DST="/mnt/dsnas1/TV"           # TV series destination
+DIR_MUSIC_DST="/mnt/dsnas1/Music"     # Music destination
+DEFAULT_DST="/mnt/dsnas1/Other"       # Default for unrecognized content
+
+# Additional storage libraries (comma-separated)
+STORAGE_DIRS="/mnt/dsnas/Movies,/mnt/external/Movies" # Additional movie libraries
+STORAGE_TV_DIRS="/mnt/dsnas/TV,/mnt/external/TV"      # Additional TV libraries
+
+# Custom pattern matching for advanced categorization
+# Format: "regex_pattern=destination_path;another_pattern=another_path"
+CUSTOM_PATTERNS=".*documentary.*=${DIR_MOVIES_DST}/Documentary;
+                 .*anime.*=${DIR_TV_DST}/Anime;
+                 .*linux.*=${DIR_APPS_DST}/Linux;
+                 .*tutorial.*=${DIR_BOOKS_DST}/Tutorials"
+```
+
+### Security & Performance
+```bash
+# Security settings - dedicated non-root user
+TORRENT_USER="torrent-mover"          # Dedicated service user
+TORRENT_GROUP="torrent-mover"         # User's primary group
+
+# Error recovery configuration
+MAX_RETRY_ATTEMPTS="3"                # Maximum retry attempts for failed operations
+RETRY_WAIT_TIME="15"                  # Seconds to wait between retry attempts
+
+# Performance tuning
+PARALLEL_THREADS="32"                 # Number of parallel threads (match CPU cores)
+PARALLEL_PROCESSING=1                 # Enable (1) or disable (0) parallel processing
+
+# Operation mode
+COPY_MODE="copy"                      # "copy" to preserve or "move" to relocate files
+```
+
+### Logging & Integrity
+```bash
 # File tracking & integrity
-PROCESSED_LOG="/var/log/torrent_processed.log"
-CHECKSUM_DB="/var/lib/torrent/checksums.db"
+PROCESSED_LOG="/var/log/torrent_processed.log" # Tracks processed torrents
+CHECKSUM_DB="/var/lib/torrent/checksums.db"    # Stores file checksums
 
-# Logging settings
-LOG_FILE="/var/log/torrent_mover.log"
-LOG_LEVEL="INFO"          # Set to "DEBUG" for more verbose logging
-USE_SYSLOG="false"        # Set to "true" to log messages to syslog
+# Logging configuration
+LOG_FILE="/var/log/torrent_mover.log" # Main log file location
+LOG_LEVEL="INFO"                      # Logging level: "INFO" or "DEBUG"
+USE_SYSLOG="false"                    # Also log to system syslog: "true" or "false"
 
-# Optional integrity verification after transfer ("true" to enable)
-CHECK_TRANSFER_INTEGRITY="true"
-    
-
- -
-

Usage

-

Run the script using the following options:

- -

You can combine options as needed. For example:

-
/usr/local/bin/torrent-mover.sh --dry-run --debug
-
- -
-

How It Works

-
    -
  1. Locking: Uses flock to ensure only one instance runs at a time.
  2. -
  3. Path Translation: The script translates the Transmission-reported path (e.g., /downloads) to the local file system path (e.g., /mnt/dsnas2) using the configured mapping.
  4. -
  5. Torrent Processing: Retrieves torrent info via transmission-remote and processes torrents that are 100% complete. It skips torrents already processed or those with duplicate source directories.
  6. -
  7. File Verification & Deduplication: Compares file checksums between source and destination, and avoids re‑processing if a match is found.
  8. -
  9. Archive Extraction: Extracts archives (RAR, ZIP, 7z) into subdirectories within the destination while preserving directory structure. The original archive is retained in the source until seeding criteria are met.
  10. -
  11. Seeding Criteria: Checks seeding ratio and time. When thresholds are met, the torrent is removed from Transmission.
  12. -
  13. Integrity Check (Optional): Optionally verifies file integrity by comparing MD5 checksums post-transfer.
  14. -
-
- +# Data integrity protection +CHECK_TRANSFER_INTEGRITY="true" # Verify file integrity after transfers +``` - -
-

License

-

- This script is provided as-is without any warranty. Use it at your own risk. Contributions and improvements are welcome. -

-
- +## Usage + +### Main Torrent Mover Script + +Run the main script using the following options: + +- **Dry-run mode (simulate operations):** + ``` + /usr/local/bin/torrent-mover --dry-run + ``` + +- **Interactive mode (prompt for confirmation):** + ``` + /usr/local/bin/torrent-mover --interactive + ``` + +- **Cache warmup mode (pre-calculate checksums):** + ``` + /usr/local/bin/torrent-mover --cache-warmup + ``` + +- **Debug mode (verbose logging):** + ``` + /usr/local/bin/torrent-mover --debug + ``` + +You can combine options as needed. For example: +``` +/usr/local/bin/torrent-mover --dry-run --debug +``` + +### Configuration Management Tool + +The system includes a dedicated configuration management tool that helps you safely update and manage your torrent-mover settings: + +``` +sudo torrent-config [OPTION] +``` + +Available options: + +- **show** - Display the current configuration with color-coding +- **edit** - Edit the configuration in your preferred text editor (automatically creates a backup) +- **backup** - Create a timestamped backup of the current configuration +- **restore** - List and restore from available backups +- **validate** - Check the configuration for errors +- **set KEY VALUE** - Update a specific configuration value +- **get KEY** - Retrieve the current value of a configuration setting +- **default** - Show the default configuration values as a reference + +Examples: +```bash +# View current configuration +sudo torrent-config show + +# Change the copy mode to 'move' +sudo torrent-config set COPY_MODE move + +# Add a new pattern for documentaries +sudo torrent-config set CUSTOM_PATTERNS ".*documentary.*=${DIR_MOVIES_DST}/Documentary" + +# Edit the configuration file in your preferred editor +sudo torrent-config edit + +# View the value of a specific setting +sudo torrent-config get TRANSMISSION_IP +``` + +## Architecture & Module Organization + +The system uses a modular architecture for improved maintainability: + +- **Main Script (`/usr/local/bin/torrent-mover`)**: Orchestrates the overall process and loads modules +- **Common Module**: Contains shared utilities, logging functions and error handling +- **File Operations Module**: Handles file transfers, checksums, and integrity verification +- **Archive Handler Module**: Specializes in extracting and managing various archive formats +- **Transmission Handler Module**: Manages all communication with the Transmission client + +## How It Works + +### Initialization & Configuration +1. **Module Loading:** The main script dynamically loads all modules from the `/usr/local/lib/torrent-mover` directory +2. **Configuration Processing:** Loads and validates the configuration from `/etc/torrent/mover.conf` +3. **Locking:** Uses `flock` to prevent multiple instances from running simultaneously + +### Torrent Processing Workflow +1. **Torrent Discovery:** Retrieves the list of torrents from Transmission using retry-enabled API calls +2. **Smart Path Translation:** Converts Transmission-reported paths to local filesystem paths using configurable mappings +3. **Content Categorization:** + - First applies custom regex patterns from the configuration + - Falls back to keyword-based directory name detection if no patterns match + - Determines the appropriate destination directory for each content type +4. **Deduplication & Verification:** + - Tracks processed source directories to avoid redundant operations + - Generates and compares checksums between source and potential destinations + - Skips transfers if identical content is already present in any destination library +5. **File Processing:** + - Extracts archives with preservation of directory structure + - Transfers files using parallel operations when enabled + - Verifies integrity after transfer if configured +6. **Cleanup & Monitoring:** + - Checks seeding ratio and time against configured thresholds + - Removes torrents from Transmission when criteria are met + - Monitors disk usage across all configured storage directories + +## License + +This script is provided as-is without any warranty. Use it at your own risk. Contributions and improvements are welcome. \ No newline at end of file diff --git a/etc/torrent/mover.conf b/etc/torrent/mover.conf index 7e54c57..ae38256 100644 --- a/etc/torrent/mover.conf +++ b/etc/torrent/mover.conf @@ -13,15 +13,32 @@ DIR_GAMES_DST="/mnt/dsnas1/Games" DIR_APPS_DST="/mnt/dsnas1/Apps" DIR_MOVIES_DST="/mnt/dsnas1/Movies" DIR_BOOKS_DST="/mnt/dsnas1/Books" +DIR_TV_DST="/mnt/dsnas1/TV" +DIR_MUSIC_DST="/mnt/dsnas1/Music" DEFAULT_DST="/mnt/dsnas1/Other" # Storage directories (comma-separated) STORAGE_DIRS="/mnt/dsnas/Movies" +STORAGE_TV_DIRS="/mnt/dsnas/TV" # Path mapping TRANSMISSION_PATH_PREFIX="/downloads" LOCAL_PATH_PREFIX="/mnt/dsnas2" +# Security settings +# Default user/group for torrent operations (usually debian-transmission) +TORRENT_USER="debian-transmission" +TORRENT_GROUP="debian-transmission" + +# Custom pattern matching for content categorization +# Format: "pattern1=destination1;pattern2=destination2" +# Example: ".*\.linux.*=${DIR_LINUX_DST};.*documentary.*=${DIR_DOCUMENTARY_DST}" +CUSTOM_PATTERNS=".*documentary.*=${DIR_MOVIES_DST}/Documentary;.*anime.*=${DIR_TV_DST}/Anime" + +# Error recovery settings +MAX_RETRY_ATTEMPTS="3" +RETRY_WAIT_TIME="15" + # Performance settings PARALLEL_THREADS="32" # Match CPU core count PARALLEL_PROCESSING=1 @@ -46,4 +63,5 @@ USE_SYSLOG="false" # Auto-create directories mkdir -p "${DIR_GAMES_DST}" "${DIR_APPS_DST}" \ "${DIR_MOVIES_DST}" "${DIR_BOOKS_DST}" \ - "${DEFAULT_DST}" 2>/dev/null || true + "${DIR_TV_DST}" "${DIR_MUSIC_DST}" \ + "${DEFAULT_DST}" 2>/dev/null || true \ No newline at end of file diff --git a/install.sh b/install.sh index f5e6531..d48c7c1 100644 --- a/install.sh +++ b/install.sh @@ -3,6 +3,7 @@ set -e # Git repository configuration GIT_REPO="http://192.168.0.236:3000/masterdraco/torrent" +INSTALL_DIR="/tmp/torrent-install" # Check root privileges if [ "$EUID" -ne 0 ]; then @@ -20,6 +21,7 @@ declare -A PKGS=( [parallel]="parallel" [bc]="bc" [git]="git" + [logrotate]="logrotate" ) for pkg in "${!PKGS[@]}"; do @@ -31,22 +33,130 @@ for pkg in "${!PKGS[@]}"; do done # Get files from Repo -git pull http://192.168.0.236:3000/masterdraco/torrent.git - +echo "Getting latest files from repository..." +if [ -d "$INSTALL_DIR" ]; then + cd "$INSTALL_DIR" + git fetch + git reset --hard origin/main +else + mkdir -p "$INSTALL_DIR" + git clone "$GIT_REPO" "$INSTALL_DIR" + cd "$INSTALL_DIR" +fi # Create directory structure echo "Creating directory structure..." mkdir -p /etc/torrent mkdir -p /usr/local/bin +mkdir -p /usr/local/lib/torrent-mover +mkdir -p /var/lib/torrent +mkdir -p /var/log/torrent +mkdir -p /etc/systemd/system + +# Create dedicated user for security +TORRENT_USER="torrent-mover" +TORRENT_GROUP="torrent-mover" + +# Check if user exists and create if not +if ! id "$TORRENT_USER" &>/dev/null; then + echo "Creating dedicated $TORRENT_USER user for security..." + useradd -r -s /bin/false "$TORRENT_USER" +fi # Install files echo "Installing files..." -cp -v etc/torrent/mover.conf /etc/torrent/ -cp -v usr/local/bin/torrent-mover /usr/local/bin/ -chmod +x /usr/local/bin/torrent-mover +install -Dm644 etc/torrent/mover.conf /etc/torrent/mover.conf.new +install -Dm755 usr/local/bin/torrent-mover /usr/local/bin/torrent-mover +install -Dm755 usr/local/bin/torrent-config /usr/local/bin/torrent-config + +# Install library modules +for module in usr/local/lib/torrent-mover/*.sh; do + if [ -f "$module" ]; then + install -Dm755 "$module" "/usr/local/lib/torrent-mover/$(basename "$module")" + fi +done + +# Create backup directory for configuration files +mkdir -p /etc/torrent/backups +chown $TORRENT_USER:$TORRENT_GROUP /etc/torrent/backups + +# If this is a first-time install, copy the default config +if [ ! -f "/etc/torrent/mover.conf" ]; then + mv /etc/torrent/mover.conf.new /etc/torrent/mover.conf +else + echo "Existing configuration found at /etc/torrent/mover.conf" + echo "New configuration is at /etc/torrent/mover.conf.new" +fi + +# Create log rotation configuration +cat > /etc/logrotate.d/torrent-mover << EOF +/var/log/torrent_mover.log /var/log/torrent_processed.log { + weekly + rotate 4 + compress + delaycompress + missingok + notifempty + create 0640 $TORRENT_USER $TORRENT_GROUP +} +EOF + +# Create systemd service +cat > /etc/systemd/system/torrent-mover.service << EOF +[Unit] +Description=Torrent Mover Service +After=network.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/torrent-mover +Restart=on-failure +RestartSec=60 +User=$TORRENT_USER +Group=$TORRENT_GROUP + +[Install] +WantedBy=multi-user.target +EOF + +# Create systemd timer for periodic execution +cat > /etc/systemd/system/torrent-mover.timer << EOF +[Unit] +Description=Run Torrent Mover every 15 minutes + +[Timer] +OnBootSec=5min +OnUnitActiveSec=15min +AccuracySec=1min + +[Install] +WantedBy=timers.target +EOF # Set permissions echo "Setting permissions..." -chmod 600 /etc/torrent/mover.conf -chown root:root /etc/torrent/mover.conf +chmod 600 /etc/torrent/mover.conf* +chown root:root /etc/torrent/mover.conf* +chmod 644 /etc/systemd/system/torrent-mover.service +chmod 644 /etc/systemd/system/torrent-mover.timer +# Set permissions for data directories +chown $TORRENT_USER:$TORRENT_GROUP /var/lib/torrent +chmod 755 /var/lib/torrent +touch /var/log/torrent_mover.log /var/log/torrent_processed.log +chown $TORRENT_USER:$TORRENT_GROUP /var/log/torrent_mover.log /var/log/torrent_processed.log +chmod 640 /var/log/torrent_mover.log /var/log/torrent_processed.log + +# Ensure torrent-mover user can access required directories +echo "Setting up group memberships..." +if getent group debian-transmission >/dev/null; then + usermod -a -G debian-transmission $TORRENT_USER + echo "Added $TORRENT_USER to debian-transmission group" +fi + +# Reload systemd and enable timer +systemctl daemon-reload +echo "To enable automatic execution every 15 minutes, run:" +echo " systemctl enable --now torrent-mover.timer" +echo +echo "Installation complete!" \ No newline at end of file diff --git a/usr/local/bin/torrent-config b/usr/local/bin/torrent-config new file mode 100755 index 0000000..2534dc7 --- /dev/null +++ b/usr/local/bin/torrent-config @@ -0,0 +1,481 @@ +#!/bin/bash +# +# Torrent Mover Configuration Utility +# A helper tool to safely update and manage your torrent-mover configuration + +set -e + +CONFIG_PATH="/etc/torrent/mover.conf" +BACKUP_DIR="/etc/torrent/backups" +DEFAULT_EDITOR="${EDITOR:-nano}" + +# Colors for terminal output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +print_header() { + echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}" + echo -e "${BLUE}║ Torrent Mover Config Utility ║${NC}" + echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}" + echo "" +} + +usage() { + print_header + echo -e "Usage: ${GREEN}$(basename "$0")${NC} [OPTION]" + echo "" + echo "Options:" + echo -e " ${YELLOW}edit${NC} Edit the configuration file with your default editor" + echo -e " ${YELLOW}backup${NC} Create a backup of the current configuration" + echo -e " ${YELLOW}restore${NC} [file] Restore a previous backup (lists available backups if no file specified)" + echo -e " ${YELLOW}validate${NC} Check the configuration for errors" + echo -e " ${YELLOW}default${NC} Show the default configuration values" + echo -e " ${YELLOW}show${NC} Display the current configuration" + echo -e " ${YELLOW}set${NC} key value Update a specific configuration value" + echo -e " ${YELLOW}get${NC} key Get the value of a specific configuration key" + echo -e " ${YELLOW}help${NC} Display this help message" + echo "" + echo "Examples:" + echo " $(basename "$0") edit # Edit the configuration file" + echo " $(basename "$0") backup # Create a timestamped backup" + echo " $(basename "$0") set COPY_MODE move # Change the copy mode to 'move'" + echo " $(basename "$0") get TRANSMISSION_IP # Show the Transmission server IP" + echo "" +} + +# Check if user is root or using sudo +check_permissions() { + if [ "$EUID" -ne 0 ]; then + echo -e "${RED}Error: This command requires root privileges.${NC}" + echo "Please run with sudo:" + echo -e " ${YELLOW}sudo $(basename "$0") $*${NC}" + exit 1 + fi +} + +# Create a backup of the current configuration +backup_config() { + check_permissions "$@" + + if [ ! -f "$CONFIG_PATH" ]; then + echo -e "${RED}Error: Configuration file not found at $CONFIG_PATH${NC}" + exit 1 + fi + + mkdir -p "$BACKUP_DIR" + local timestamp=$(date +"%Y%m%d_%H%M%S") + local backup_file="$BACKUP_DIR/mover.conf.$timestamp" + + cp "$CONFIG_PATH" "$backup_file" + echo -e "${GREEN}Configuration backed up to:${NC} $backup_file" +} + +# Restore a configuration from backup +restore_config() { + check_permissions "$@" + + if [ ! -d "$BACKUP_DIR" ]; then + echo -e "${RED}Error: Backup directory not found at $BACKUP_DIR${NC}" + exit 1 + fi + + if [ -z "$1" ]; then + # List available backups + echo -e "${BLUE}Available backups:${NC}" + local count=0 + for file in "$BACKUP_DIR"/mover.conf.*; do + if [ -f "$file" ]; then + count=$((count+1)) + local date_part=$(basename "$file" | cut -d. -f3) + echo -e "${YELLOW}$count)${NC} $(basename "$file") ($(date -d "${date_part:0:8} ${date_part:9:2}:${date_part:11:2}:${date_part:13:2}" "+%Y-%m-%d %H:%M:%S"))" + fi + done + + if [ "$count" -eq 0 ]; then + echo -e "${YELLOW}No backups found.${NC}" + exit 0 + fi + + echo "" + read -p "Enter the number of the backup to restore: " selection + + if ! [[ "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ] || [ "$selection" -gt "$count" ]; then + echo -e "${RED}Error: Invalid selection.${NC}" + exit 1 + fi + + # Get the filename of the selected backup + local selected_file=$(ls -1 "$BACKUP_DIR"/mover.conf.* | sed -n "${selection}p") + else + # Use the specified backup file + local selected_file="$BACKUP_DIR/$1" + + if [ ! -f "$selected_file" ]; then + echo -e "${RED}Error: Backup file not found at $selected_file${NC}" + exit 1 + fi + fi + + # Create a backup of the current config before restoring + backup_config + + # Restore the selected backup + cp "$selected_file" "$CONFIG_PATH" + echo -e "${GREEN}Configuration restored from:${NC} $selected_file" +} + +# Edit the configuration file +edit_config() { + check_permissions "$@" + + if [ ! -f "$CONFIG_PATH" ]; then + echo -e "${RED}Error: Configuration file not found at $CONFIG_PATH${NC}" + exit 1 + } + + # Create a backup before editing + backup_config + + # Open in the user's preferred editor + $DEFAULT_EDITOR "$CONFIG_PATH" + + # Validate after editing + validate_config +} + +# Validate the configuration for errors +validate_config() { + check_permissions "$@" + + if [ ! -f "$CONFIG_PATH" ]; then + echo -e "${RED}Error: Configuration file not found at $CONFIG_PATH${NC}" + exit 1 + fi + + echo -e "${BLUE}Validating configuration file...${NC}" + + # Source the config file in a subshell to check for syntax errors + if ! (bash -n "$CONFIG_PATH"); then + echo -e "${RED}Error: The configuration file contains syntax errors.${NC}" + exit 1 + fi + + # Load the configuration + source "$CONFIG_PATH" + + # Check mandatory settings + local required_vars=( + "TRANSMISSION_IP" + "TRANSMISSION_PORT" + "TRANSMISSION_PATH_PREFIX" + "LOCAL_PATH_PREFIX" + "DIR_MOVIES_DST" + "DIR_APPS_DST" + "DIR_GAMES_DST" + "DIR_BOOKS_DST" + "DEFAULT_DST" + "COPY_MODE" + ) + + local error_count=0 + for var in "${required_vars[@]}"; do + if [ -z "${!var}" ]; then + echo -e "${RED}Error: Required setting '$var' is not defined.${NC}" + error_count=$((error_count+1)) + fi + done + + # Validate COPY_MODE + if [ -n "$COPY_MODE" ] && [ "$COPY_MODE" != "copy" ] && [ "$COPY_MODE" != "move" ]; then + echo -e "${RED}Error: COPY_MODE must be 'copy' or 'move', not '$COPY_MODE'.${NC}" + error_count=$((error_count+1)) + fi + + # Validate directory paths + local dir_vars=( + "DIR_GAMES_DST" + "DIR_APPS_DST" + "DIR_MOVIES_DST" + "DIR_BOOKS_DST" + "DIR_TV_DST" + "DIR_MUSIC_DST" + "DEFAULT_DST" + ) + + for var in "${dir_vars[@]}"; do + if [ -n "${!var}" ]; then + if [[ ! "${!var}" == /* ]]; then + echo -e "${RED}Error: Directory path for '$var' must be absolute (start with /).${NC}" + error_count=$((error_count+1)) + fi + fi + done + + # Check if any pattern in CUSTOM_PATTERNS references undefined variables + if [ -n "$CUSTOM_PATTERNS" ]; then + IFS=';' read -ra PATTERN_ARRAY <<< "$CUSTOM_PATTERNS" + for pattern in "${PATTERN_ARRAY[@]}"; do + IFS='=' read -ra PARTS <<< "$pattern" + if [ "${#PARTS[@]}" -eq 2 ]; then + local dest="${PARTS[1]}" + if [[ "$dest" == *'${'*'}'* ]]; then + local var_name=$(echo "$dest" | sed -n 's/.*\${//;s/}.*//p') + if [ -z "${!var_name}" ]; then + echo -e "${RED}Error: Custom pattern uses undefined variable: \${$var_name}${NC}" + error_count=$((error_count+1)) + fi + fi + fi + done + fi + + if [ "$error_count" -eq 0 ]; then + echo -e "${GREEN}Configuration validation passed. No errors found.${NC}" + else + echo -e "${RED}Configuration validation failed with $error_count error(s).${NC}" + exit 1 + fi +} + +# Show the current configuration +show_config() { + if [ ! -f "$CONFIG_PATH" ]; then + echo -e "${RED}Error: Configuration file not found at $CONFIG_PATH${NC}" + exit 1 + fi + + print_header + echo -e "${BLUE}Current Configuration:${NC}" + echo "" + + # Load config and display it categorized + source "$CONFIG_PATH" + + echo -e "${YELLOW}=== Connection Settings ===${NC}" + echo -e "TRANSMISSION_IP=${GREEN}${TRANSMISSION_IP:-}${NC}" + echo -e "TRANSMISSION_PORT=${GREEN}${TRANSMISSION_PORT:-}${NC}" + if [ -n "$TRANSMISSION_USER" ]; then + echo -e "TRANSMISSION_USER=${GREEN}${TRANSMISSION_USER}${NC}" + echo -e "TRANSMISSION_PASSWORD=${GREEN}********${NC}" + else + echo -e "TRANSMISSION_USER=${YELLOW}${NC}" + echo -e "TRANSMISSION_PASSWORD=${YELLOW}${NC}" + fi + echo -e "TRANSMISSION_PATH_PREFIX=${GREEN}${TRANSMISSION_PATH_PREFIX:-}${NC}" + echo -e "LOCAL_PATH_PREFIX=${GREEN}${LOCAL_PATH_PREFIX:-}${NC}" + echo "" + + echo -e "${YELLOW}=== Destination Directories ===${NC}" + echo -e "DIR_GAMES_DST=${GREEN}${DIR_GAMES_DST:-}${NC}" + echo -e "DIR_APPS_DST=${GREEN}${DIR_APPS_DST:-}${NC}" + echo -e "DIR_MOVIES_DST=${GREEN}${DIR_MOVIES_DST:-}${NC}" + echo -e "DIR_BOOKS_DST=${GREEN}${DIR_BOOKS_DST:-}${NC}" + echo -e "DIR_TV_DST=${GREEN}${DIR_TV_DST:-}${NC}" + echo -e "DIR_MUSIC_DST=${GREEN}${DIR_MUSIC_DST:-}${NC}" + echo -e "DEFAULT_DST=${GREEN}${DEFAULT_DST:-}${NC}" + echo "" + + echo -e "${YELLOW}=== Additional Storage Libraries ===${NC}" + echo -e "STORAGE_DIRS=${GREEN}${STORAGE_DIRS:-}${NC}" + echo -e "STORAGE_TV_DIRS=${GREEN}${STORAGE_TV_DIRS:-}${NC}" + echo "" + + echo -e "${YELLOW}=== Security Settings ===${NC}" + echo -e "TORRENT_USER=${GREEN}${TORRENT_USER:-debian-transmission}${NC}" + echo -e "TORRENT_GROUP=${GREEN}${TORRENT_GROUP:-debian-transmission}${NC}" + echo "" + + echo -e "${YELLOW}=== Performance Settings ===${NC}" + echo -e "PARALLEL_THREADS=${GREEN}${PARALLEL_THREADS:-$(nproc)}${NC}" + echo -e "PARALLEL_PROCESSING=${GREEN}${PARALLEL_PROCESSING:-1}${NC}" + echo -e "COPY_MODE=${GREEN}${COPY_MODE:-}${NC}" + echo "" + + echo -e "${YELLOW}=== Error Recovery ===${NC}" + echo -e "MAX_RETRY_ATTEMPTS=${GREEN}${MAX_RETRY_ATTEMPTS:-3}${NC}" + echo -e "RETRY_WAIT_TIME=${GREEN}${RETRY_WAIT_TIME:-15}${NC}" + echo "" + + echo -e "${YELLOW}=== Logging & Integrity ===${NC}" + echo -e "LOG_FILE=${GREEN}${LOG_FILE:-/var/log/torrent_mover.log}${NC}" + echo -e "LOG_LEVEL=${GREEN}${LOG_LEVEL:-INFO}${NC}" + echo -e "USE_SYSLOG=${GREEN}${USE_SYSLOG:-false}${NC}" + echo -e "PROCESSED_LOG=${GREEN}${PROCESSED_LOG:-/var/log/torrent_processed.log}${NC}" + echo -e "CHECKSUM_DB=${GREEN}${CHECKSUM_DB:-/var/lib/torrent/checksums.db}${NC}" + echo -e "CHECK_TRANSFER_INTEGRITY=${GREEN}${CHECK_TRANSFER_INTEGRITY:-true}${NC}" + echo "" + + if [ -n "$CUSTOM_PATTERNS" ]; then + echo -e "${YELLOW}=== Custom Content Patterns ===${NC}" + IFS=';' read -ra PATTERN_ARRAY <<< "$CUSTOM_PATTERNS" + for pattern in "${PATTERN_ARRAY[@]}"; do + if [ -n "$pattern" ]; then + IFS='=' read -ra PARTS <<< "$pattern" + if [ "${#PARTS[@]}" -eq 2 ]; then + local regex="${PARTS[0]}" + local dest="${PARTS[1]}" + echo -e "Pattern: ${GREEN}${regex}${NC} → ${BLUE}${dest}${NC}" + fi + fi + done + echo "" + fi +} + +# Update a specific configuration value +set_config_value() { + check_permissions "$@" + + if [ -z "$1" ] || [ -z "$2" ]; then + echo -e "${RED}Error: Both key and value must be provided.${NC}" + echo "Usage: $(basename "$0") set KEY VALUE" + exit 1 + fi + + local key="$1" + local value="$2" + + if [ ! -f "$CONFIG_PATH" ]; then + echo -e "${RED}Error: Configuration file not found at $CONFIG_PATH${NC}" + exit 1 + fi + + # Create a backup before modifying + backup_config + + # Check if the key already exists in the config + if grep -q "^$key=" "$CONFIG_PATH"; then + # Update the existing key + sed -i "s|^$key=.*|$key=\"$value\"|" "$CONFIG_PATH" + echo -e "${GREEN}Updated configuration:${NC} $key = \"$value\"" + else + # Add the new key + echo "$key=\"$value\"" >> "$CONFIG_PATH" + echo -e "${GREEN}Added new configuration:${NC} $key = \"$value\"" + fi + + # Validate after updating + validate_config +} + +# Get a specific configuration value +get_config_value() { + if [ -z "$1" ]; then + echo -e "${RED}Error: Key must be provided.${NC}" + echo "Usage: $(basename "$0") get KEY" + exit 1 + fi + + local key="$1" + + if [ ! -f "$CONFIG_PATH" ]; then + echo -e "${RED}Error: Configuration file not found at $CONFIG_PATH${NC}" + exit 1 + fi + + # Source the config file to get the value + source "$CONFIG_PATH" + + if [ -n "${!key+x}" ]; then + echo -e "${key}=${GREEN}${!key}${NC}" + else + echo -e "${RED}Error: Configuration key '$key' is not defined.${NC}" + exit 1 + fi +} + +# Show default configuration options +show_default_config() { + print_header + echo -e "${BLUE}Default Configuration Values:${NC}" + echo "" + cat << EOF +# Transmission Settings +TRANSMISSION_IP="192.168.1.100" +TRANSMISSION_PORT="9091" +TRANSMISSION_USER="" +TRANSMISSION_PASSWORD="" + +# Path Mapping +TRANSMISSION_PATH_PREFIX="/downloads" +LOCAL_PATH_PREFIX="/mnt/data" + +# Destination Directories +DIR_GAMES_DST="/mnt/media/Games" +DIR_APPS_DST="/mnt/media/Apps" +DIR_MOVIES_DST="/mnt/media/Movies" +DIR_BOOKS_DST="/mnt/media/Books" +DIR_TV_DST="/mnt/media/TV" +DIR_MUSIC_DST="/mnt/media/Music" +DEFAULT_DST="/mnt/media/Other" + +# Additional Storage +STORAGE_DIRS="" +STORAGE_TV_DIRS="" + +# Security +TORRENT_USER="torrent-mover" +TORRENT_GROUP="torrent-mover" + +# Error Recovery +MAX_RETRY_ATTEMPTS="3" +RETRY_WAIT_TIME="15" + +# Performance +PARALLEL_THREADS="$(nproc)" +PARALLEL_PROCESSING="1" +COPY_MODE="copy" + +# Logging & Integrity +LOG_FILE="/var/log/torrent_mover.log" +LOG_LEVEL="INFO" +USE_SYSLOG="false" +PROCESSED_LOG="/var/log/torrent_processed.log" +CHECKSUM_DB="/var/lib/torrent/checksums.db" +CHECK_TRANSFER_INTEGRITY="true" + +# Custom Content Patterns +CUSTOM_PATTERNS=".*documentary.*=\${DIR_MOVIES_DST}/Documentary;.*anime.*=\${DIR_TV_DST}/Anime" +EOF + echo "" +} + +# Main command processing +case "$1" in + edit) + edit_config "${@:2}" + ;; + backup) + backup_config "${@:2}" + ;; + restore) + restore_config "${@:2}" + ;; + validate) + validate_config "${@:2}" + ;; + show) + show_config + ;; + set) + set_config_value "${@:2}" + ;; + get) + get_config_value "${@:2}" + ;; + default) + show_default_config + ;; + help|--help|-h) + usage + ;; + *) + usage + exit 1 + ;; +esac + +exit 0 \ No newline at end of file diff --git a/usr/local/bin/torrent-mover b/usr/local/bin/torrent-mover index 7850125..bb1e0a2 100755 --- a/usr/local/bin/torrent-mover +++ b/usr/local/bin/torrent-mover @@ -1,14 +1,16 @@ #!/bin/bash -# Torrent Mover v8.0 - Enhanced & Robust Version with Directory Deduplication, -# Improved Archive Handling (keeping archives until ratio limits are reached) +# Torrent Mover v8.0 - Enhanced & Robust Version with modular architecture, +# improved error handling, security, and content categorization # # This script processes completed torrents reported by Transmission, # moving or copying files to designated destination directories. # It includes robust locking, advanced error handling & notifications, # improved logging, optional post-transfer integrity checks, configurable path mapping, # and improved archive extraction that preserves directory structure. -# -# Future improvements might include using Transmission’s RPC API. + +# Set script location for importing modules +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +LIB_DIR="/usr/local/lib/torrent-mover" ############################## # Robust Locking with flock # @@ -17,52 +19,6 @@ LOCK_FILE="/var/lock/torrent-mover.lock" exec 200>"${LOCK_FILE}" || { echo "Cannot open lock file" >&2; exit 1; } flock -n 200 || { echo "Another instance is running." >&2; exit 1; } -############################## -# Global Runtime Variables # -############################## -DRY_RUN=0 -INTERACTIVE=0 -CACHE_WARMUP=0 -DEBUG=0 # Set to 1 if LOG_LEVEL is DEBUG or --debug is passed - -# To avoid reprocessing the same source directory (across different torrents) -declare -A processed_source_dirs - -#################### -# Logging Functions# -#################### -# All log messages go to stderr. -log_debug() { - if [[ "${DEBUG}" -eq 1 ]]; then - echo -e "[DEBUG] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2 - [[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[DEBUG] $*" - fi -} -log_info() { - echo -e "[INFO] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2 - [[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[INFO] $*" -} -log_warn() { - echo -e "[WARN] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2 - [[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[WARN] $*" -} -log_error() { - echo -e "[ERROR] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2 - [[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[ERROR] $*" -} - -################################# -# Error Handling & Notifications# -################################# -error_handler() { - local lineno="$1" - local msg="$2" - log_error "Error on line ${lineno}: ${msg}" - # Optionally send a notification (e.g., email) - exit 1 -} -trap 'error_handler ${LINENO} "$BASH_COMMAND"' ERR - ############################## # Configuration & Validation # ############################## @@ -79,6 +35,20 @@ if [[ -z "${TRANSMISSION_PATH_PREFIX:-}" || -z "${LOCAL_PATH_PREFIX:-}" ]]; then exit 1 fi +# Load modules +for module in "${LIB_DIR}"/*.sh; do + if [[ -f "$module" ]]; then + source "$module" + fi +done + +# Set defaults for new configuration options +TORRENT_USER="${TORRENT_USER:-debian-transmission}" +TORRENT_GROUP="${TORRENT_GROUP:-debian-transmission}" +MAX_RETRY_ATTEMPTS="${MAX_RETRY_ATTEMPTS:-3}" +RETRY_WAIT_TIME="${RETRY_WAIT_TIME:-15}" + +# Enable DEBUG mode if set in config if [[ "${LOG_LEVEL}" == "DEBUG" ]]; then DEBUG=1 fi @@ -89,313 +59,10 @@ if [[ -n "${STORAGE_DIRS}" ]]; then IFS=',' read -ra STORAGE_DIRS_ARRAY <<< "${STORAGE_DIRS}" fi -############################## -# Helper & Utility Functions # -############################## - -# translate_source: Converts the Transmission‑reported path into the local path. -translate_source() { - local src="$1" - echo "${src/#${TRANSMISSION_PATH_PREFIX}/${LOCAL_PATH_PREFIX}}" -} - -# parse_args: Processes command‑line options. -parse_args() { - while [[ $# -gt 0 ]]; do - case "$1" in - --dry-run) DRY_RUN=1; shift ;; - --interactive) INTERACTIVE=1; shift ;; - --cache-warmup) CACHE_WARMUP=1; shift ;; - --debug) DEBUG=1; shift ;; - --help) - echo "Usage: $0 [--dry-run] [--interactive] [--cache-warmup] [--debug]" >&2 - exit 0 - ;; - *) echo "Invalid option: $1" >&2; exit 1 ;; - esac - done -} - -# check_dependencies: Ensures required commands are available. -check_dependencies() { - local deps=("transmission-remote" "unrar" "unzip" "7z" "parallel" "bc") - for dep in "${deps[@]}"; do - command -v "${dep}" >/dev/null 2>&1 || { log_error "Missing dependency: ${dep}"; exit 1; } - done -} - -# check_disk_usage: Warn if disk usage is over 90%. -declare -A CHECKED_MOUNTS=() -check_disk_usage() { - local dir="$1" - [[ -z "${dir}" ]] && return - if ! df -P "${dir}" &>/dev/null; then - log_warn "Directory not found: ${dir}" - return - fi - local mount_point - mount_point=$(df -P "${dir}" | awk 'NR==2 {print $6}') - [[ -z "${mount_point}" ]] && return - if [[ -z "${CHECKED_MOUNTS["${mount_point}"]+x}" ]]; then - local usage - usage=$(df -P "${dir}" | awk 'NR==2 {sub(/%/, "", $5); print $5}') - if (( usage >= 90 )); then - log_warn "Storage warning: ${mount_point} at ${usage}% capacity" - fi - CHECKED_MOUNTS["${mount_point}"]=1 - fi -} - -# init_checksum_db: Initializes the checksum database. -init_checksum_db() { - mkdir -p "$(dirname "${CHECKSUM_DB}")" - touch "${CHECKSUM_DB}" || { log_error "Could not create ${CHECKSUM_DB}"; exit 1; } - chmod 600 "${CHECKSUM_DB}" -} - -# record_checksums: Generates checksums for files in given directories. -record_checksums() { - log_info "Generating checksums with ${PARALLEL_THREADS:-$(nproc)} threads" - find "$@" -type f ! \( -iname "*.nfo" -o -iname "*.sfv" \) -print0 | \ - parallel -0 -j ${PARALLEL_THREADS:-$(nproc)} md5sum | sort > "${CHECKSUM_DB}.tmp" - mv "${CHECKSUM_DB}.tmp" "${CHECKSUM_DB}" -} - -# file_metadata: Returns an md5 hash for file metadata. -file_metadata() { - find "$1" -type f ! \( -iname "*.nfo" -o -iname "*.sfv" \) -exec md5sum {} \; | sort | awk '{print $1}' -} - -# files_need_processing: Checks if the source files need processing. -files_need_processing() { - local src="$1" - shift - local targets=("$@") - - if [[ ! -d "${src}" ]]; then - log_warn "Source directory missing: ${src}" - return 1 - fi - - log_info "=== FILE VERIFICATION DEBUG START ===" - log_info "Source directory: ${src}" - log_info "Verification targets: ${targets[*]}" - - local empty_target_found=0 - for target in "${targets[@]}"; do - if [[ ! -d "${target}" ]]; then - log_info "Target missing: ${target}" - empty_target_found=1 - continue - fi - - local file_count - file_count=$(find "${target}" -mindepth 1 -maxdepth 1 -print | wc -l) - log_debug "File count for target ${target}: ${file_count}" - if [[ "${file_count}" -eq 0 ]]; then - log_info "Empty target directory: ${target}" - empty_target_found=1 - else - log_info "Target contains ${file_count} items: ${target}" - log_info "First 5 items:" - find "${target}" -mindepth 1 -maxdepth 1 | head -n 5 | while read -r item; do - log_info " - ${item##*/}" - done - fi - done - - if [[ "${empty_target_found}" -eq 1 ]]; then - log_info "Empty target detected - processing needed" - log_info "=== FILE VERIFICATION DEBUG END ===" - return 0 - fi - - log_info "Generating source checksums..." - local src_checksums - src_checksums=$(find "${src}" -type f ! \( -iname "*.nfo" -o -iname "*.sfv" \) -exec md5sum {} \; | sort) - log_info "First 5 source checksums:" - echo "${src_checksums}" | head -n 5 | while read -r line; do - log_info " ${line}" - done - - local match_found=0 - for target in "${targets[@]}"; do - log_info "Checking against target: ${target}" - log_info "Generating target checksums..." - local target_checksums - target_checksums=$(find "${target}" -type f ! \( -iname "*.nfo" -o -iname "*.sfv" \) -exec md5sum {} \; | sort) - log_info "First 5 target checksums:" - echo "${target_checksums}" | head -n 5 | while read -r line; do - log_info " ${line}" - done - - if diff <(echo "${src_checksums}") <(echo "${target_checksums}") >/dev/null; then - log_info "Exact checksum match found in: ${target}" - match_found=1 - break - else - log_info "No match in: ${target}" - fi - done - - log_info "=== FILE VERIFICATION DEBUG END ===" - [[ "${match_found}" -eq 1 ]] && return 1 || return 0 -} - -# warm_cache: Pre-calculates checksums for storage directories. -warm_cache() { - log_info "Starting cache warmup for Movies..." - local targets=("${DIR_MOVIES_DST}" "${STORAGE_DIRS_ARRAY[@]}") - record_checksums "${targets[@]}" - log_info "Cache warmup completed. Checksums stored in ${CHECKSUM_DB}" -} - -# is_processed: Checks if the torrent (by hash) has already been processed. -is_processed() { - grep -q "^${1}$" "${PROCESSED_LOG}" 2>/dev/null -} - -# mark_processed: Records a processed torrent. -mark_processed() { - echo "${1}" >> "${PROCESSED_LOG}" -} - -# get_destination: Maps a source directory to a destination directory based on keywords. -declare -A PATH_CACHE -get_destination() { - local source_path="$1" - if [[ -n "${PATH_CACHE["${source_path}"]+x}" ]]; then - echo "${PATH_CACHE["${source_path}"]}" - return - fi - log_info "Analyzing path: ${source_path}" - local destination - case "${source_path,,}" in - *games*) destination="${DIR_GAMES_DST}";; - *apps*) destination="${DIR_APPS_DST}";; - *movies*) destination="${DIR_MOVIES_DST}";; - *books*) destination="${DIR_BOOKS_DST}";; - *) destination="${DEFAULT_DST}";; - esac - log_info "Mapped to: ${destination}" - PATH_CACHE["${source_path}"]="${destination}" - echo "${destination}" -} - -###################################### -# Improved Archive Extraction Handler # -###################################### -# For each archive found in the source directory, create a subdirectory in the destination -# named after the archive (without its extension) and extract into that subdirectory. -# IMPORTANT: The archive is now retained in the source, so it will remain until the ratio -# limits are reached and Transmission removes the torrent data. -handle_archives() { - local src="$1" dst="$2" - find "${src}" -type f \( -iname "*.rar" -o -iname "*.zip" -o -iname "*.7z" \) | while read -r arch; do - log_info "Extracting archive: ${arch}" - local base - base=$(basename "${arch}") - local subdir="${dst}/${base%.*}" - mkdir -p "${subdir}" || { log_error "Failed to create subdirectory ${subdir}"; continue; } - case "${arch##*.}" in - rar) - unrar x -o- "${arch}" "${subdir}" || { log_error "unrar failed for ${arch}"; continue; } - ;; - zip) - unzip -o "${arch}" -d "${subdir}" || { log_error "unzip failed for ${arch}"; continue; } - ;; - 7z) - 7z x "${arch}" -o"${subdir}" || { log_error "7z extraction failed for ${arch}"; continue; } - ;; - esac - log_info "Archive ${arch} retained in source until ratio limits are reached." - done -} - -# move_files: Moves files using parallel processing if enabled. -move_files() { - if (( PARALLEL_PROCESSING )); then - parallel -j ${PARALLEL_THREADS:-$(nproc)} mv {} "${1}" ::: "${2}"/* - else - mv "${2}"/* "${1}" - fi -} - -# copy_files: Copies files using parallel processing if enabled. -copy_files() { - if (( PARALLEL_PROCESSING )); then - parallel -j ${PARALLEL_THREADS:-$(nproc)} cp -r {} "${1}" ::: "${2}"/* - else - cp -r "${2}"/* "${1}" - fi -} - -# process_copy: Validates directories, then copies/moves files from source to destination. -# Optionally verifies integrity after transfer if CHECK_TRANSFER_INTEGRITY is "true". -process_copy() { - local id="$1" hash="$2" src="$3" dst="$4" - if [[ ! -d "${src}" ]]; then - log_error "Source directory missing: ${src}" - return 1 - fi - if [[ ! -d "${dst}" ]]; then - log_info "Creating destination directory: ${dst}" - mkdir -p "${dst}" || { log_error "Failed to create directory: ${dst}"; return 1; } - chmod 775 "${dst}" - chown debian-transmission:debian-transmission "${dst}" - fi - if [[ ! -w "${dst}" ]]; then - log_error "No write permissions for: ${dst}" - return 1 - fi - if (( DRY_RUN )); then - log_info "[DRY RUN] Would process torrent ${id}:" - log_info " - Copy files from ${src} to ${dst}" - log_info " - File count: $(find "${src}" -maxdepth 1 -type f | wc -l)" - return - fi - handle_archives "${src}" "${dst}" - case "${COPY_MODE}" in - move) - log_info "Moving files from ${src} to ${dst}" - move_files "${dst}" "${src}" - ;; - copy) - log_info "Copying files from ${src} to ${dst}" - copy_files "${dst}" "${src}" - ;; - esac - if [ $? -eq 0 ]; then - if [[ "${CHECK_TRANSFER_INTEGRITY}" == "true" ]]; then - log_info "Verifying integrity of transferred files..." - local src_checksum target_checksum - src_checksum=$(find "${src}" -type f ! \( -iname "*.nfo" -o -iname "*.sfv" \) -exec md5sum {} \; | sort) - target_checksum=$(find "${dst}" -type f ! \( -iname "*.nfo" -o -iname "*.sfv" \) -exec md5sum {} \; | sort) - if diff <(echo "${src_checksum}") <(echo "${target_checksum}") >/dev/null; then - log_info "Integrity check passed." - else - log_error "Integrity check FAILED for ${src}" - return 1 - fi - fi - log_info "Transfer completed successfully" - mark_processed "${hash}" - else - log_error "Transfer failed for ${src}" - fi -} - -# process_removal: Removes a torrent via Transmission. -process_removal() { - local id="$1" - if (( DRY_RUN )); then - log_info "[DRY RUN] Would remove torrent ${id}" - return - fi - transmission-remote "${TRANSMISSION_IP}:${TRANSMISSION_PORT}" \ - -n "${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}" \ - -t "${id}" --remove-and-delete -} +################################# +# Error Handling & Notifications# +################################# +trap 'error_handler ${LINENO} "$BASH_COMMAND"' ERR ################# # Main Function # @@ -411,16 +78,12 @@ main() { "${DIR_BOOKS_DST}" "${DEFAULT_DST}" ) - for dir in "${REQUIRED_DIRS[@]}"; do - if [[ ! -d "${dir}" ]]; then - log_error "Directory missing: ${dir}" - exit 1 - fi - if [[ ! -w "${dir}" ]]; then - log_error "Write permission denied: ${dir}" - exit 1 - fi - done + + # Add optional directories if defined + [[ -n "${DIR_TV_DST}" ]] && REQUIRED_DIRS+=("${DIR_TV_DST}") + [[ -n "${DIR_MUSIC_DST}" ]] && REQUIRED_DIRS+=("${DIR_MUSIC_DST}") + + validate_directories "${REQUIRED_DIRS[@]}" || exit 1 init_checksum_db @@ -429,14 +92,14 @@ main() { exit 0 fi - log_info "Starting processing" + log_info "Starting processing with user: ${TORRENT_USER}" declare -A warned_dirs=() - transmission-remote "${TRANSMISSION_IP}:${TRANSMISSION_PORT}" \ - -n "${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}" -l | awk 'NR>1 && $1 ~ /^[0-9]+$/ {print $1}' | while read -r id; do - + + # Get list of torrents from Transmission + get_torrents | while read -r id; do local info - info=$(transmission-remote "${TRANSMISSION_IP}:${TRANSMISSION_PORT}" \ - -n "${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}" -t "${id}" -i) + info=$(get_torrent_info "${id}") + local hash hash=$(grep "Hash:" <<< "${info}" | awk '{print $2}') local ratio @@ -470,8 +133,15 @@ main() { fi local targets=("${dst}") case "${dst}" in - "${DIR_MOVIES_DST}") targets+=("${STORAGE_DIRS_ARRAY[@]}");; + "${DIR_MOVIES_DST}") + targets+=("${STORAGE_DIRS_ARRAY[@]}") + ;; + "${DIR_TV_DST}") + # If there are TV storage dirs, include them + [[ -n "${STORAGE_TV_DIRS}" ]] && IFS=',' read -ra TV_DIRS <<< "${STORAGE_TV_DIRS}" && targets+=("${TV_DIRS[@]}") + ;; esac + if ! files_need_processing "${dir}" "${targets[@]}"; then log_info "Skipping copy - files already exist in:" for target in "${targets[@]}"; do @@ -489,11 +159,13 @@ main() { fi done - check_disk_usage "${DIR_GAMES_DST}" - check_disk_usage "${DIR_APPS_DST}" - check_disk_usage "${DIR_MOVIES_DST}" - check_disk_usage "${DIR_BOOKS_DST}" - check_disk_usage "${DEFAULT_DST}" + # Check disk usage for all directories + for dir in "${REQUIRED_DIRS[@]}"; do + check_disk_usage "${dir}" + done + for dir in "${STORAGE_DIRS_ARRAY[@]}"; do + check_disk_usage "${dir}" + done } ###################### @@ -506,4 +178,4 @@ if (( INTERACTIVE )); then [[ "${choice}" =~ ^[Yy]$ ]] || exit 0 fi -main +main \ No newline at end of file diff --git a/usr/local/lib/torrent-mover/archive_handler.sh b/usr/local/lib/torrent-mover/archive_handler.sh new file mode 100644 index 0000000..5134dc2 --- /dev/null +++ b/usr/local/lib/torrent-mover/archive_handler.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Archive extraction handler for torrent-mover + +# Improved Archive Extraction Handler +# For each archive found in the source directory, create a subdirectory in the destination +# named after the archive (without its extension) and extract into that subdirectory. +# The archive is retained in the source, so it will remain until the ratio +# limits are reached and Transmission removes the torrent data. +handle_archives() { + local src="$1" dst="$2" + find "${src}" -type f \( -iname "*.rar" -o -iname "*.zip" -o -iname "*.7z" \) | while read -r arch; do + log_info "Extracting archive: ${arch}" + local base + base=$(basename "${arch}") + local subdir="${dst}/${base%.*}" + mkdir -p "${subdir}" || { log_error "Failed to create subdirectory ${subdir}"; continue; } + + # Apply proper permissions to the extraction directory + chmod 775 "${subdir}" + chown ${TORRENT_USER:-debian-transmission}:${TORRENT_GROUP:-debian-transmission} "${subdir}" + + local extract_success=0 + case "${arch##*.}" in + rar) + retry_command "unrar x -o- \"${arch}\" \"${subdir}\"" 3 10 + extract_success=$? + ;; + zip) + retry_command "unzip -o \"${arch}\" -d \"${subdir}\"" 3 10 + extract_success=$? + ;; + 7z) + retry_command "7z x \"${arch}\" -o\"${subdir}\"" 3 10 + extract_success=$? + ;; + esac + + if [ $extract_success -eq 0 ]; then + log_info "Archive ${arch} extracted successfully to ${subdir}" + log_info "Archive ${arch} retained in source until ratio limits are reached." + else + log_error "Failed to extract archive ${arch}" + fi + done +} \ No newline at end of file diff --git a/usr/local/lib/torrent-mover/common.sh b/usr/local/lib/torrent-mover/common.sh new file mode 100644 index 0000000..b7354a8 --- /dev/null +++ b/usr/local/lib/torrent-mover/common.sh @@ -0,0 +1,139 @@ +#!/bin/bash +# Common utility functions and variables for torrent-mover + +# Global Runtime Variables +DRY_RUN=0 +INTERACTIVE=0 +CACHE_WARMUP=0 +DEBUG=0 + +# To avoid reprocessing the same source directory (across different torrents) +declare -A processed_source_dirs + +declare -A CHECKED_MOUNTS=() +declare -A PATH_CACHE + +# Logging Functions +# All log messages go to stderr. +log_debug() { + if [[ "${DEBUG}" -eq 1 ]]; then + echo -e "[DEBUG] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2 + [[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[DEBUG] $*" + fi +} + +log_info() { + echo -e "[INFO] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2 + [[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[INFO] $*" +} + +log_warn() { + echo -e "[WARN] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2 + [[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[WARN] $*" +} + +log_error() { + echo -e "[ERROR] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2 + [[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[ERROR] $*" +} + +# Error Handling & Notifications +error_handler() { + local lineno="$1" + local msg="$2" + log_error "Error on line ${lineno}: ${msg}" + # Optionally send a notification (e.g., email) + return 1 +} + +# translate_source: Converts the Transmission‑reported path into the local path. +translate_source() { + local src="$1" + echo "${src/#${TRANSMISSION_PATH_PREFIX}/${LOCAL_PATH_PREFIX}}" +} + +# parse_args: Processes command‑line options. +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=1; shift ;; + --interactive) INTERACTIVE=1; shift ;; + --cache-warmup) CACHE_WARMUP=1; shift ;; + --debug) DEBUG=1; shift ;; + --help) + echo "Usage: $0 [--dry-run] [--interactive] [--cache-warmup] [--debug]" >&2 + exit 0 + ;; + *) echo "Invalid option: $1" >&2; exit 1 ;; + esac + done +} + +# check_dependencies: Ensures required commands are available. +check_dependencies() { + local deps=("transmission-remote" "unrar" "unzip" "7z" "parallel" "bc") + for dep in "${deps[@]}"; do + command -v "${dep}" >/dev/null 2>&1 || { log_error "Missing dependency: ${dep}"; exit 1; } + done +} + +# check_disk_usage: Warn if disk usage is over 90%. +check_disk_usage() { + local dir="$1" + [[ -z "${dir}" ]] && return + if ! df -P "${dir}" &>/dev/null; then + log_warn "Directory not found: ${dir}" + return + fi + local mount_point + mount_point=$(df -P "${dir}" | awk 'NR==2 {print $6}') + [[ -z "${mount_point}" ]] && return + if [[ -z "${CHECKED_MOUNTS["${mount_point}"]+x}" ]]; then + local usage + usage=$(df -P "${dir}" | awk 'NR==2 {sub(/%/, "", $5); print $5}') + if (( usage >= 90 )); then + log_warn "Storage warning: ${mount_point} at ${usage}% capacity" + fi + CHECKED_MOUNTS["${mount_point}"]=1 + fi +} + +# retry_command: Execute a command with retries +retry_command() { + local cmd="$1" + local max_attempts="${2:-3}" # Default to 3 attempts + local wait_time="${3:-10}" # Default to 10 seconds wait between attempts + local attempt=1 + + while (( attempt <= max_attempts )); do + log_debug "Attempt $attempt of $max_attempts: $cmd" + if eval "$cmd"; then + return 0 + else + log_warn "Command failed (attempt $attempt): $cmd" + if (( attempt == max_attempts )); then + log_error "Maximum attempts reached for: $cmd" + return 1 + fi + sleep "$wait_time" + (( attempt++ )) + fi + done + return 1 +} + +# validate_directories: Ensure required directories exist and are writable +validate_directories() { + local directories=("$@") + for dir in "${directories[@]}"; do + if [[ ! -d "${dir}" ]]; then + log_error "Directory missing: ${dir}" + return 1 + fi + if [[ ! -w "${dir}" ]]; then + log_error "Write permission denied: ${dir}" + return 1 + fi + done + return 0 +} \ No newline at end of file diff --git a/usr/local/lib/torrent-mover/file_operations.sh b/usr/local/lib/torrent-mover/file_operations.sh new file mode 100644 index 0000000..5835ca3 --- /dev/null +++ b/usr/local/lib/torrent-mover/file_operations.sh @@ -0,0 +1,191 @@ +#!/bin/bash +# File operation functions for torrent-mover + +# init_checksum_db: Initializes the checksum database. +init_checksum_db() { + mkdir -p "$(dirname "${CHECKSUM_DB}")" + touch "${CHECKSUM_DB}" || { log_error "Could not create ${CHECKSUM_DB}"; exit 1; } + chmod 600 "${CHECKSUM_DB}" +} + +# record_checksums: Generates checksums for files in given directories. +record_checksums() { + log_info "Generating checksums with ${PARALLEL_THREADS:-$(nproc)} threads" + find "$@" -type f ! \( -iname "*.nfo" -o -iname "*.sfv" \) -print0 | \ + parallel -0 -j ${PARALLEL_THREADS:-$(nproc)} md5sum | sort > "${CHECKSUM_DB}.tmp" + mv "${CHECKSUM_DB}.tmp" "${CHECKSUM_DB}" +} + +# file_metadata: Returns an md5 hash for file metadata. +file_metadata() { + find "$1" -type f ! \( -iname "*.nfo" -o -iname "*.sfv" \) -exec md5sum {} \; | sort | awk '{print $1}' +} + +# files_need_processing: Checks if the source files need processing. +files_need_processing() { + local src="$1" + shift + local targets=("$@") + + if [[ ! -d "${src}" ]]; then + log_warn "Source directory missing: ${src}" + return 1 + fi + + log_info "=== FILE VERIFICATION DEBUG START ===" + log_info "Source directory: ${src}" + log_info "Verification targets: ${targets[*]}" + + local empty_target_found=0 + for target in "${targets[@]}"; do + if [[ ! -d "${target}" ]]; then + log_info "Target missing: ${target}" + empty_target_found=1 + continue + fi + + local file_count + file_count=$(find "${target}" -mindepth 1 -maxdepth 1 -print | wc -l) + log_debug "File count for target ${target}: ${file_count}" + if [[ "${file_count}" -eq 0 ]]; then + log_info "Empty target directory: ${target}" + empty_target_found=1 + else + log_info "Target contains ${file_count} items: ${target}" + log_info "First 5 items:" + find "${target}" -mindepth 1 -maxdepth 1 | head -n 5 | while read -r item; do + log_info " - ${item##*/}" + done + fi + done + + if [[ "${empty_target_found}" -eq 1 ]]; then + log_info "Empty target detected - processing needed" + log_info "=== FILE VERIFICATION DEBUG END ===" + return 0 + fi + + log_info "Generating source checksums..." + local src_checksums + src_checksums=$(find "${src}" -type f ! \( -iname "*.nfo" -o -iname "*.sfv" \) -exec md5sum {} \; | sort) + log_info "First 5 source checksums:" + echo "${src_checksums}" | head -n 5 | while read -r line; do + log_info " ${line}" + done + + local match_found=0 + for target in "${targets[@]}"; do + log_info "Checking against target: ${target}" + log_info "Generating target checksums..." + local target_checksums + target_checksums=$(find "${target}" -type f ! \( -iname "*.nfo" -o -iname "*.sfv" \) -exec md5sum {} \; | sort) + log_info "First 5 target checksums:" + echo "${target_checksums}" | head -n 5 | while read -r line; do + log_info " ${line}" + done + + if diff <(echo "${src_checksums}") <(echo "${target_checksums}") >/dev/null; then + log_info "Exact checksum match found in: ${target}" + match_found=1 + break + else + log_info "No match in: ${target}" + fi + done + + log_info "=== FILE VERIFICATION DEBUG END ===" + [[ "${match_found}" -eq 1 ]] && return 1 || return 0 +} + +# warm_cache: Pre-calculates checksums for storage directories. +warm_cache() { + log_info "Starting cache warmup for Movies..." + local targets=("${DIR_MOVIES_DST}" "${STORAGE_DIRS_ARRAY[@]}") + record_checksums "${targets[@]}" + log_info "Cache warmup completed. Checksums stored in ${CHECKSUM_DB}" +} + +# is_processed: Checks if the torrent (by hash) has already been processed. +is_processed() { + grep -q "^${1}$" "${PROCESSED_LOG}" 2>/dev/null +} + +# mark_processed: Records a processed torrent. +mark_processed() { + echo "${1}" >> "${PROCESSED_LOG}" +} + +# move_files: Moves files using parallel processing if enabled. +move_files() { + if (( PARALLEL_PROCESSING )); then + retry_command "parallel -j ${PARALLEL_THREADS:-$(nproc)} mv {} \"${1}\" ::: \"${2}\"/*" 3 15 + else + retry_command "mv \"${2}\"/* \"${1}\"" 3 15 + fi +} + +# copy_files: Copies files using parallel processing if enabled. +copy_files() { + if (( PARALLEL_PROCESSING )); then + retry_command "parallel -j ${PARALLEL_THREADS:-$(nproc)} cp -r {} \"${1}\" ::: \"${2}\"/*" 3 15 + else + retry_command "cp -r \"${2}\"/* \"${1}\"" 3 15 + fi +} + +# process_copy: Validates directories, then copies/moves files from source to destination. +# Optionally verifies integrity after transfer if CHECK_TRANSFER_INTEGRITY is "true". +process_copy() { + local id="$1" hash="$2" src="$3" dst="$4" + if [[ ! -d "${src}" ]]; then + log_error "Source directory missing: ${src}" + return 1 + fi + if [[ ! -d "${dst}" ]]; then + log_info "Creating destination directory: ${dst}" + mkdir -p "${dst}" || { log_error "Failed to create directory: ${dst}"; return 1; } + chmod 775 "${dst}" + chown ${TORRENT_USER:-debian-transmission}:${TORRENT_GROUP:-debian-transmission} "${dst}" + fi + if [[ ! -w "${dst}" ]]; then + log_error "No write permissions for: ${dst}" + return 1 + fi + if (( DRY_RUN )); then + log_info "[DRY RUN] Would process torrent ${id}:" + log_info " - Copy files from ${src} to ${dst}" + log_info " - File count: $(find "${src}" -maxdepth 1 -type f | wc -l)" + return + fi + handle_archives "${src}" "${dst}" + case "${COPY_MODE}" in + move) + log_info "Moving files from ${src} to ${dst}" + move_files "${dst}" "${src}" + ;; + copy) + log_info "Copying files from ${src} to ${dst}" + copy_files "${dst}" "${src}" + ;; + esac + if [ $? -eq 0 ]; then + if [[ "${CHECK_TRANSFER_INTEGRITY}" == "true" ]]; then + log_info "Verifying integrity of transferred files..." + local src_checksum target_checksum + src_checksum=$(find "${src}" -type f ! \( -iname "*.nfo" -o -iname "*.sfv" \) -exec md5sum {} \; | sort) + target_checksum=$(find "${dst}" -type f ! \( -iname "*.nfo" -o -iname "*.sfv" \) -exec md5sum {} \; | sort) + if diff <(echo "${src_checksum}") <(echo "${target_checksum}") >/dev/null; then + log_info "Integrity check passed." + else + log_error "Integrity check FAILED for ${src}" + return 1 + fi + fi + log_info "Transfer completed successfully" + mark_processed "${hash}" + else + log_error "Transfer failed for ${src}" + return 1 + fi + return 0 +} \ No newline at end of file diff --git a/usr/local/lib/torrent-mover/transmission_handler.sh b/usr/local/lib/torrent-mover/transmission_handler.sh new file mode 100644 index 0000000..a568cd2 --- /dev/null +++ b/usr/local/lib/torrent-mover/transmission_handler.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# Transmission-related functions for torrent-mover + +# get_destination: Maps a source directory to a destination directory based on keywords and patterns +get_destination() { + local source_path="$1" + if [[ -n "${PATH_CACHE["${source_path}"]+x}" ]]; then + echo "${PATH_CACHE["${source_path}"]}" + return + fi + + log_info "Analyzing path: ${source_path}" + local destination="${DEFAULT_DST}" + + # Match using custom patterns from config file if they exist + if [[ -n "${CUSTOM_PATTERNS}" ]]; then + log_debug "Using custom patterns from config..." + # Parse and apply each pattern + IFS=';' read -ra PATTERN_ARRAY <<< "${CUSTOM_PATTERNS}" + for pattern in "${PATTERN_ARRAY[@]}"; do + IFS='=' read -ra PARTS <<< "${pattern}" + if [[ "${#PARTS[@]}" -eq 2 ]]; then + local regex="${PARTS[0]}" + local dest="${PARTS[1]}" + if [[ "${source_path,,}" =~ ${regex,,} ]]; then + log_info "Custom pattern match: ${regex} -> ${dest}" + destination="${dest}" + break + fi + fi + done + fi + + # If no custom pattern matched, use default category mapping + if [[ "${destination}" == "${DEFAULT_DST}" ]]; then + case "${source_path,,}" in + *games*) destination="${DIR_GAMES_DST}";; + *apps*|*applications*|*programs*|*software*) destination="${DIR_APPS_DST}";; + *movies*|*film*|*video*) destination="${DIR_MOVIES_DST}";; + *books*|*ebook*|*pdf*|*epub*) destination="${DIR_BOOKS_DST}";; + *tv*|*series*|*episode*) + if [[ -n "${DIR_TV_DST}" ]]; then + destination="${DIR_TV_DST}" + else + destination="${DIR_MOVIES_DST}" + fi + ;; + *music*|*audio*|*mp3*|*flac*) + if [[ -n "${DIR_MUSIC_DST}" ]]; then + destination="${DIR_MUSIC_DST}" + else + destination="${DEFAULT_DST}" + fi + ;; + esac + fi + + log_info "Mapped to: ${destination}" + PATH_CACHE["${source_path}"]="${destination}" + echo "${destination}" +} + +# process_removal: Removes a torrent via Transmission. +process_removal() { + local id="$1" + if (( DRY_RUN )); then + log_info "[DRY RUN] Would remove torrent ${id}" + return + fi + + retry_command "transmission-remote \"${TRANSMISSION_IP}:${TRANSMISSION_PORT}\" -n \"${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}\" -t \"${id}\" --remove-and-delete" 3 15 +} + +# get_torrents: Retrieves a list of torrents from Transmission +get_torrents() { + retry_command "transmission-remote \"${TRANSMISSION_IP}:${TRANSMISSION_PORT}\" -n \"${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}\" -l" 3 20 | + awk 'NR>1 && $1 ~ /^[0-9]+$/ {print $1}' +} + +# get_torrent_info: Gets detailed info for a specific torrent +get_torrent_info() { + local id="$1" + retry_command "transmission-remote \"${TRANSMISSION_IP}:${TRANSMISSION_PORT}\" -n \"${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}\" -t \"${id}\" -i" 3 15 +} \ No newline at end of file