some program redesign

This commit is contained in:
masterdraco 2025-02-28 10:07:04 +01:00
parent 6c164193b3
commit c924f096e7
9 changed files with 1361 additions and 514 deletions

341
README.md
View File

@ -1,136 +1,243 @@
<h1>Torrent Mover v8.0</h1> # Torrent Mover v8.0
<div class="section"> ## Description
<h2>Description</h2>
<p>
<strong>Torrent Mover</strong> is a Bash script designed to automate the processing of completed torrents in Transmission.
It moves or copies downloaded files from a Transmissionreported 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.
</p>
</div>
<div class="section"> **Torrent Mover** is a Bash script designed to automate the processing of completed torrents in Transmission.
<h2>Features</h2> It moves or copies downloaded files from a Transmissionreported download location to designated destination directories on your system.
<ul> This enhanced version includes a modular architecture, dedicated security user, robust locking, advanced error handling with retry capabilities,
<li><strong>Automatic Torrent Processing:</strong> Monitors Transmission for completed torrents and processes them based on configurable seeding criteria.</li> parallel processing, configurable path mapping, improved archive extraction, and optional file integrity verification.
<li><strong>Configurable Path Mapping:</strong> Uses Transmissions reported download path (e.g. <code>/downloads</code>) and maps it to your local file system (e.g. <code>/mnt/dsnas2</code>) via configurable settings.</li>
<li><strong>Robust Locking:</strong> Employs <code>flock</code> to ensure that only one instance of the script runs at a time.</li>
<li><strong>Advanced Error Handling & Logging:</strong> Global error handler and detailed logging (with DEBUG mode support). Optionally, logs to syslog.</li>
<li><strong>Parallel File Operations:</strong> Utilizes GNU Parallel for moving, copying, and generating checksums, enabling efficient multi-threaded processing.</li>
<li><strong>Archive Extraction:</strong> 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.</li>
<li><strong>Directory Deduplication:</strong> Prevents reprocessing the same source directory if multiple torrents reference it.</li>
<li><strong>Optional Integrity Verification:</strong> Verifies file integrity by comparing MD5 checksums after transfer.</li>
</ul>
</div>
<div class="section"> 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.
<h2>Requirements</h2>
<ul>
<li>Bash</li>
<li>transmission-remote</li>
<li>GNU Parallel</li>
<li>unrar, unzip, 7z</li>
<li>bc</li>
</ul>
</div>
<div class="section"> ## Features
<h2>Installation</h2>
<ol>
<li><strong>Download the Script:</strong> Save the script (e.g., <code>torrent-mover.sh</code>) to your desired location (e.g., <code>/usr/local/bin/</code>).</li>
<li><strong>Make It Executable:</strong>
<pre>chmod +x /usr/local/bin/torrent-mover.sh</pre>
</li>
<li><strong>Create/Edit the Configuration File:</strong> The script expects a configuration file at <code>/etc/torrent/mover.conf</code>. See the configuration section below.</li>
</ol>
</div>
<div class="section"> ### Core Features
<h2>Configuration</h2> - **Automatic Torrent Processing:** Monitors Transmission for completed torrents and processes them based on configurable seeding criteria.
<p>Edit or create <code>/etc/torrent/mover.conf</code> with the following content:</p> - **Configurable Path Mapping:** Uses Transmission's reported download path and maps it to your local file system via configurable settings.
<pre> - **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.
# Transmission settings - **Directory Deduplication:** Prevents reprocessing the same source directory if multiple torrents reference it.
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)
# Path mapping settings ### Advanced Content Organization
TRANSMISSION_PATH_PREFIX="/downloads" - **Smart Content Categorization:** Uses both pattern matching and directory name detection to properly categorize content.
LOCAL_PATH_PREFIX="/mnt/dsnas2" - **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.
# Destination directories ### Enhanced Security & Reliability
DIR_GAMES_DST="/mnt/dsnas1/Games" - **Dedicated Non-Root User:** Uses a dedicated service user with minimal permissions for enhanced security.
DIR_APPS_DST="/mnt/dsnas1/Apps" - **Error Recovery:** Includes retry mechanisms with configurable attempts and delay for network operations.
DIR_MOVIES_DST="/mnt/dsnas1/Movies" - **Data Integrity Protection:** Optionally verifies file integrity by comparing MD5 checksums after transfer.
DIR_BOOKS_DST="/mnt/dsnas1/Books" - **Robust Locking:** Employs `flock` to ensure that only one instance of the script runs at a time.
DEFAULT_DST="/mnt/dsnas1/Other"
# Additional storage directories (comma-separated list) ### Performance & Engineering
STORAGE_DIRS="/mnt/dsnas/Movies" - **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.
# Performance settings ## Requirements
PARALLEL_THREADS="32"
PARALLEL_PROCESSING=1
# Operation mode: "move" or "copy" - Bash
COPY_MODE="copy" - 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 # File tracking & integrity
PROCESSED_LOG="/var/log/torrent_processed.log" PROCESSED_LOG="/var/log/torrent_processed.log" # Tracks processed torrents
CHECKSUM_DB="/var/lib/torrent/checksums.db" CHECKSUM_DB="/var/lib/torrent/checksums.db" # Stores file checksums
# Logging settings # Logging configuration
LOG_FILE="/var/log/torrent_mover.log" LOG_FILE="/var/log/torrent_mover.log" # Main log file location
LOG_LEVEL="INFO" # Set to "DEBUG" for more verbose logging LOG_LEVEL="INFO" # Logging level: "INFO" or "DEBUG"
USE_SYSLOG="false" # Set to "true" to log messages to syslog USE_SYSLOG="false" # Also log to system syslog: "true" or "false"
# Optional integrity verification after transfer ("true" to enable) # Data integrity protection
CHECK_TRANSFER_INTEGRITY="true" CHECK_TRANSFER_INTEGRITY="true" # Verify file integrity after transfers
</pre> ```
</div>
<div class="section"> ## Usage
<h2>Usage</h2>
<p>Run the script using the following options:</p>
<ul>
<li><strong>Dry-run mode (simulate operations):</strong>
<pre>/usr/local/bin/torrent-mover.sh --dry-run</pre>
</li>
<li><strong>Interactive mode (prompt for confirmation):</strong>
<pre>/usr/local/bin/torrent-mover.sh --interactive</pre>
</li>
<li><strong>Cache warmup mode (pre-calculate checksums):</strong>
<pre>/usr/local/bin/torrent-mover.sh --cache-warmup</pre>
</li>
<li><strong>Debug mode (verbose logging):</strong>
<pre>/usr/local/bin/torrent-mover.sh --debug</pre>
</li>
</ul>
<p>You can combine options as needed. For example:</p>
<pre>/usr/local/bin/torrent-mover.sh --dry-run --debug</pre>
</div>
<div class="section"> ### Main Torrent Mover Script
<h2>How It Works</h2>
<ol>
<li><strong>Locking:</strong> Uses <code>flock</code> to ensure only one instance runs at a time.</li>
<li><strong>Path Translation:</strong> The script translates the Transmission-reported path (e.g., <code>/downloads</code>) to the local file system path (e.g., <code>/mnt/dsnas2</code>) using the configured mapping.</li>
<li><strong>Torrent Processing:</strong> Retrieves torrent info via <code>transmission-remote</code> and processes torrents that are 100% complete. It skips torrents already processed or those with duplicate source directories.</li>
<li><strong>File Verification & Deduplication:</strong> Compares file checksums between source and destination, and avoids reprocessing if a match is found.</li>
<li><strong>Archive Extraction:</strong> 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.</li>
<li><strong>Seeding Criteria:</strong> Checks seeding ratio and time. When thresholds are met, the torrent is removed from Transmission.</li>
<li><strong>Integrity Check (Optional):</strong> Optionally verifies file integrity by comparing MD5 checksums post-transfer.</li>
</ol>
</div>
Run the main script using the following options:
- **Dry-run mode (simulate operations):**
```
/usr/local/bin/torrent-mover --dry-run
```
<div class="section"> - **Interactive mode (prompt for confirmation):**
<h2>License</h2> ```
<p> /usr/local/bin/torrent-mover --interactive
This script is provided as-is without any warranty. Use it at your own risk. Contributions and improvements are welcome. ```
</p>
</div> - **Cache warmup mode (pre-calculate checksums):**
</body> ```
/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.

View File

@ -13,15 +13,32 @@ DIR_GAMES_DST="/mnt/dsnas1/Games"
DIR_APPS_DST="/mnt/dsnas1/Apps" DIR_APPS_DST="/mnt/dsnas1/Apps"
DIR_MOVIES_DST="/mnt/dsnas1/Movies" DIR_MOVIES_DST="/mnt/dsnas1/Movies"
DIR_BOOKS_DST="/mnt/dsnas1/Books" DIR_BOOKS_DST="/mnt/dsnas1/Books"
DIR_TV_DST="/mnt/dsnas1/TV"
DIR_MUSIC_DST="/mnt/dsnas1/Music"
DEFAULT_DST="/mnt/dsnas1/Other" DEFAULT_DST="/mnt/dsnas1/Other"
# Storage directories (comma-separated) # Storage directories (comma-separated)
STORAGE_DIRS="/mnt/dsnas/Movies" STORAGE_DIRS="/mnt/dsnas/Movies"
STORAGE_TV_DIRS="/mnt/dsnas/TV"
# Path mapping # Path mapping
TRANSMISSION_PATH_PREFIX="/downloads" TRANSMISSION_PATH_PREFIX="/downloads"
LOCAL_PATH_PREFIX="/mnt/dsnas2" 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 # Performance settings
PARALLEL_THREADS="32" # Match CPU core count PARALLEL_THREADS="32" # Match CPU core count
PARALLEL_PROCESSING=1 PARALLEL_PROCESSING=1
@ -46,4 +63,5 @@ USE_SYSLOG="false"
# Auto-create directories # Auto-create directories
mkdir -p "${DIR_GAMES_DST}" "${DIR_APPS_DST}" \ mkdir -p "${DIR_GAMES_DST}" "${DIR_APPS_DST}" \
"${DIR_MOVIES_DST}" "${DIR_BOOKS_DST}" \ "${DIR_MOVIES_DST}" "${DIR_BOOKS_DST}" \
"${DIR_TV_DST}" "${DIR_MUSIC_DST}" \
"${DEFAULT_DST}" 2>/dev/null || true "${DEFAULT_DST}" 2>/dev/null || true

View File

@ -3,6 +3,7 @@ set -e
# Git repository configuration # Git repository configuration
GIT_REPO="http://192.168.0.236:3000/masterdraco/torrent" GIT_REPO="http://192.168.0.236:3000/masterdraco/torrent"
INSTALL_DIR="/tmp/torrent-install"
# Check root privileges # Check root privileges
if [ "$EUID" -ne 0 ]; then if [ "$EUID" -ne 0 ]; then
@ -20,6 +21,7 @@ declare -A PKGS=(
[parallel]="parallel" [parallel]="parallel"
[bc]="bc" [bc]="bc"
[git]="git" [git]="git"
[logrotate]="logrotate"
) )
for pkg in "${!PKGS[@]}"; do for pkg in "${!PKGS[@]}"; do
@ -31,22 +33,130 @@ for pkg in "${!PKGS[@]}"; do
done done
# Get files from Repo # 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 # Create directory structure
echo "Creating directory structure..." echo "Creating directory structure..."
mkdir -p /etc/torrent mkdir -p /etc/torrent
mkdir -p /usr/local/bin 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 # Install files
echo "Installing files..." echo "Installing files..."
cp -v etc/torrent/mover.conf /etc/torrent/ install -Dm644 etc/torrent/mover.conf /etc/torrent/mover.conf.new
cp -v usr/local/bin/torrent-mover /usr/local/bin/ install -Dm755 usr/local/bin/torrent-mover /usr/local/bin/torrent-mover
chmod +x /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 # Set permissions
echo "Setting permissions..." echo "Setting permissions..."
chmod 600 /etc/torrent/mover.conf chmod 600 /etc/torrent/mover.conf*
chown root:root /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!"

481
usr/local/bin/torrent-config Executable file
View File

@ -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:-<not set>}${NC}"
echo -e "TRANSMISSION_PORT=${GREEN}${TRANSMISSION_PORT:-<not set>}${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}<not set>${NC}"
echo -e "TRANSMISSION_PASSWORD=${YELLOW}<not set>${NC}"
fi
echo -e "TRANSMISSION_PATH_PREFIX=${GREEN}${TRANSMISSION_PATH_PREFIX:-<not set>}${NC}"
echo -e "LOCAL_PATH_PREFIX=${GREEN}${LOCAL_PATH_PREFIX:-<not set>}${NC}"
echo ""
echo -e "${YELLOW}=== Destination Directories ===${NC}"
echo -e "DIR_GAMES_DST=${GREEN}${DIR_GAMES_DST:-<not set>}${NC}"
echo -e "DIR_APPS_DST=${GREEN}${DIR_APPS_DST:-<not set>}${NC}"
echo -e "DIR_MOVIES_DST=${GREEN}${DIR_MOVIES_DST:-<not set>}${NC}"
echo -e "DIR_BOOKS_DST=${GREEN}${DIR_BOOKS_DST:-<not set>}${NC}"
echo -e "DIR_TV_DST=${GREEN}${DIR_TV_DST:-<not set>}${NC}"
echo -e "DIR_MUSIC_DST=${GREEN}${DIR_MUSIC_DST:-<not set>}${NC}"
echo -e "DEFAULT_DST=${GREEN}${DEFAULT_DST:-<not set>}${NC}"
echo ""
echo -e "${YELLOW}=== Additional Storage Libraries ===${NC}"
echo -e "STORAGE_DIRS=${GREEN}${STORAGE_DIRS:-<not set>}${NC}"
echo -e "STORAGE_TV_DIRS=${GREEN}${STORAGE_TV_DIRS:-<not set>}${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:-<not set>}${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

View File

@ -1,14 +1,16 @@
#!/bin/bash #!/bin/bash
# Torrent Mover v8.0 - Enhanced & Robust Version with Directory Deduplication, # Torrent Mover v8.0 - Enhanced & Robust Version with modular architecture,
# Improved Archive Handling (keeping archives until ratio limits are reached) # improved error handling, security, and content categorization
# #
# This script processes completed torrents reported by Transmission, # This script processes completed torrents reported by Transmission,
# moving or copying files to designated destination directories. # moving or copying files to designated destination directories.
# It includes robust locking, advanced error handling & notifications, # It includes robust locking, advanced error handling & notifications,
# improved logging, optional post-transfer integrity checks, configurable path mapping, # improved logging, optional post-transfer integrity checks, configurable path mapping,
# and improved archive extraction that preserves directory structure. # and improved archive extraction that preserves directory structure.
#
# Future improvements might include using Transmissions 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 # # 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; } exec 200>"${LOCK_FILE}" || { echo "Cannot open lock file" >&2; exit 1; }
flock -n 200 || { echo "Another instance is running." >&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 # # Configuration & Validation #
############################## ##############################
@ -79,6 +35,20 @@ if [[ -z "${TRANSMISSION_PATH_PREFIX:-}" || -z "${LOCAL_PATH_PREFIX:-}" ]]; then
exit 1 exit 1
fi 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 if [[ "${LOG_LEVEL}" == "DEBUG" ]]; then
DEBUG=1 DEBUG=1
fi fi
@ -89,313 +59,10 @@ if [[ -n "${STORAGE_DIRS}" ]]; then
IFS=',' read -ra STORAGE_DIRS_ARRAY <<< "${STORAGE_DIRS}" IFS=',' read -ra STORAGE_DIRS_ARRAY <<< "${STORAGE_DIRS}"
fi fi
############################## #################################
# Helper & Utility Functions # # Error Handling & Notifications#
############################## #################################
trap 'error_handler ${LINENO} "$BASH_COMMAND"' ERR
# translate_source: Converts the Transmissionreported path into the local path.
translate_source() {
local src="$1"
echo "${src/#${TRANSMISSION_PATH_PREFIX}/${LOCAL_PATH_PREFIX}}"
}
# parse_args: Processes commandline 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
}
################# #################
# Main Function # # Main Function #
@ -411,16 +78,12 @@ main() {
"${DIR_BOOKS_DST}" "${DIR_BOOKS_DST}"
"${DEFAULT_DST}" "${DEFAULT_DST}"
) )
for dir in "${REQUIRED_DIRS[@]}"; do
if [[ ! -d "${dir}" ]]; then # Add optional directories if defined
log_error "Directory missing: ${dir}" [[ -n "${DIR_TV_DST}" ]] && REQUIRED_DIRS+=("${DIR_TV_DST}")
exit 1 [[ -n "${DIR_MUSIC_DST}" ]] && REQUIRED_DIRS+=("${DIR_MUSIC_DST}")
fi
if [[ ! -w "${dir}" ]]; then validate_directories "${REQUIRED_DIRS[@]}" || exit 1
log_error "Write permission denied: ${dir}"
exit 1
fi
done
init_checksum_db init_checksum_db
@ -429,14 +92,14 @@ main() {
exit 0 exit 0
fi fi
log_info "Starting processing" log_info "Starting processing with user: ${TORRENT_USER}"
declare -A warned_dirs=() 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 local info
info=$(transmission-remote "${TRANSMISSION_IP}:${TRANSMISSION_PORT}" \ info=$(get_torrent_info "${id}")
-n "${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}" -t "${id}" -i)
local hash local hash
hash=$(grep "Hash:" <<< "${info}" | awk '{print $2}') hash=$(grep "Hash:" <<< "${info}" | awk '{print $2}')
local ratio local ratio
@ -470,8 +133,15 @@ main() {
fi fi
local targets=("${dst}") local targets=("${dst}")
case "${dst}" in 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 esac
if ! files_need_processing "${dir}" "${targets[@]}"; then if ! files_need_processing "${dir}" "${targets[@]}"; then
log_info "Skipping copy - files already exist in:" log_info "Skipping copy - files already exist in:"
for target in "${targets[@]}"; do for target in "${targets[@]}"; do
@ -489,11 +159,13 @@ main() {
fi fi
done done
check_disk_usage "${DIR_GAMES_DST}" # Check disk usage for all directories
check_disk_usage "${DIR_APPS_DST}" for dir in "${REQUIRED_DIRS[@]}"; do
check_disk_usage "${DIR_MOVIES_DST}" check_disk_usage "${dir}"
check_disk_usage "${DIR_BOOKS_DST}" done
check_disk_usage "${DEFAULT_DST}" for dir in "${STORAGE_DIRS_ARRAY[@]}"; do
check_disk_usage "${dir}"
done
} }
###################### ######################

View File

@ -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
}

View File

@ -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 Transmissionreported path into the local path.
translate_source() {
local src="$1"
echo "${src/#${TRANSMISSION_PATH_PREFIX}/${LOCAL_PATH_PREFIX}}"
}
# parse_args: Processes commandline 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
}

View File

@ -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
}

View File

@ -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
}