Compare commits
18 Commits
7464b41b18
...
main
Author | SHA1 | Date | |
---|---|---|---|
91106a244c | |||
1119f38fd6 | |||
e64e1115a7 | |||
bf41b9ad71 | |||
4f7cb91bc5 | |||
fb56817e76 | |||
f572a241ef | |||
![]() |
4c7ebaf5fe | ||
![]() |
d799a2e8bd | ||
![]() |
bb2ebaaa5d | ||
![]() |
c924f096e7 | ||
6c164193b3 | |||
5972dc2e1c | |||
21db2cea6f | |||
ecb39f4fb0 | |||
cbf1de8a91 | |||
![]() |
3287566fb7 | ||
![]() |
22abd7fbaf |
324
README.md
324
README.md
@@ -1,64 +1,286 @@
|
||||
# Torrent Mover
|
||||
# Torrent Mover v9.1
|
||||
|
||||
A automated torrent management system for handling completed downloads and seeding management.
|
||||
## 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 a modular architecture, dedicated security user, robust locking, advanced error handling with retry capabilities,
|
||||
parallel processing, configurable path mapping, improved archive extraction, shared directory handling, and optional file integrity verification.
|
||||
|
||||
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.
|
||||
|
||||
## Features
|
||||
- Automatic file organization by category (Movies, Games, Apps, Books)
|
||||
- Seeding management based on ratio/time
|
||||
- Archive extraction support (RAR, ZIP, 7z)
|
||||
- Parallel processing
|
||||
- Checksum verification
|
||||
- Storage capacity monitoring
|
||||
|
||||
### 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.
|
||||
- **Shared Directory Handling:** Intelligently processes torrents that share the same download directory by matching files to specific torrents.
|
||||
|
||||
### 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
|
||||
```bash
|
||||
|
||||
1. Run the installation script as root:
|
||||
```
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
Add a cron job to run it every 5 minutes:
|
||||
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
|
||||
|
||||
sudo crontab -e
|
||||
Add this line:
|
||||
3. Enable the service to run every 15 minutes:
|
||||
```
|
||||
sudo systemctl enable --now torrent-mover.timer
|
||||
```
|
||||
|
||||
*/5 * * * * /usr/local/bin/torrent-mover >/dev/null 2>&1
|
||||
## Configuration
|
||||
|
||||
torrent-mover [options]
|
||||
Options:
|
||||
--dry-run Simulate operations
|
||||
--interactive Confirm before processing
|
||||
--cache-warmup Pre-generate checksums
|
||||
Edit the configuration file at `/etc/torrent/mover.conf` to customize the behavior of Torrent Mover:
|
||||
|
||||
Before first run please check /etc/torrent/mover.conf
|
||||
and correct any needed values there.
|
||||
|
||||
|
||||
First run:
|
||||
torrent-mover --cache-warmup
|
||||
|
||||
takes a loooong time since script is generating hash values for all files
|
||||
|
||||
|
||||
|
||||
|
||||
Git Hosting
|
||||
Repository URL: http://192.168.0.236:3000/masterdraco/torrent
|
||||
|
||||
Security
|
||||
Configuration files stored in /etc/torrent
|
||||
|
||||
Processed logs in /var/log/torrent_*
|
||||
|
||||
Copy
|
||||
|
||||
3. **Repository Structure**:
|
||||
### Connection Configuration
|
||||
```bash
|
||||
.
|
||||
├── etc
|
||||
│ └── torrent
|
||||
│ └── mover.conf
|
||||
├── usr
|
||||
│ └── local
|
||||
│ └── bin
|
||||
│ └── torrent-mover
|
||||
├── install.sh
|
||||
└── README.md
|
||||
# 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" # Tracks processed torrents
|
||||
CHECKSUM_DB="/var/lib/torrent/checksums.db" # Stores file checksums
|
||||
|
||||
# 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"
|
||||
|
||||
# Data integrity protection
|
||||
CHECK_TRANSFER_INTEGRITY="true" # Verify file integrity after transfers
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
### Helper Scripts
|
||||
|
||||
The system includes additional helper scripts for more advanced usage:
|
||||
|
||||
- **Torrent Processor:**
|
||||
```
|
||||
/usr/local/bin/torrent-processor [OPTIONS]
|
||||
```
|
||||
|
||||
Available options:
|
||||
- `--reset` - Clear processed log to re-process all torrents
|
||||
- `--books` - Process only book torrents
|
||||
- `--movies` - Process only movie torrents
|
||||
- `--tv` - Process only TV show torrents
|
||||
- `--apps` - Process only application torrents
|
||||
- `--games` - Process only game torrents
|
||||
- `--id NUMBER` - Process a specific torrent ID
|
||||
|
||||
Examples:
|
||||
```bash
|
||||
# Process all book torrents (even if previously processed)
|
||||
/usr/local/bin/torrent-processor --reset --books
|
||||
|
||||
# Process only torrent with ID 123
|
||||
/usr/local/bin/torrent-processor --id 123
|
||||
```
|
||||
|
||||
- **Smart Processor:**
|
||||
```
|
||||
/usr/local/bin/smart-processor
|
||||
```
|
||||
|
||||
An alternative processor specifically designed to handle shared directories more intelligently by:
|
||||
- Detecting shared download directories
|
||||
- Matching files to specific torrents
|
||||
- Using content type detection for files
|
||||
- Processing multiple torrents efficiently
|
||||
|
||||
### 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. **Smart File Matching:**
|
||||
- Detects when multiple torrents share the same download directory
|
||||
- Uses intelligent pattern matching to identify specific files for each torrent
|
||||
- Handles shared directories by matching torrent names to specific files
|
||||
6. **File Processing:**
|
||||
- Extracts archives with preservation of directory structure
|
||||
- Transfers files using parallel operations when enabled
|
||||
- Verifies integrity after transfer if configured
|
||||
7. **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.
|
@@ -13,15 +13,36 @@ 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
|
||||
# This maps the transmission-reported download path to the local filesystem path
|
||||
# The script will use this prefix to translate paths between Transmission and local filesystem
|
||||
#
|
||||
# IMPORTANT: Transmission reports paths as /downloads/Books but they are actually in /mnt/dsnas2/Books
|
||||
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;.*games.*=${DIR_GAMES_DST};.*apps.*=${DIR_APPS_DST};.*books.*=${DIR_BOOKS_DST};.*tv.*=${DIR_TV_DST};.*series.*=${DIR_TV_DST};.*music.*=${DIR_MUSIC_DST}"
|
||||
|
||||
# Error recovery settings
|
||||
MAX_RETRY_ATTEMPTS="3"
|
||||
RETRY_WAIT_TIME="15"
|
||||
|
||||
# Performance settings
|
||||
PARALLEL_THREADS="32" # Match CPU core count
|
||||
PARALLEL_PROCESSING=1
|
||||
@@ -43,7 +64,9 @@ CHECK_TRANSFER_INTEGRITY="true"
|
||||
# Optionally, set USE_SYSLOG="true" to also log messages to syslog.
|
||||
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
|
||||
# Auto-create directories - commented out from config file
|
||||
# These should be created in a script, not in the config file
|
||||
# mkdir -p "${DIR_GAMES_DST}" "${DIR_APPS_DST}" \
|
||||
# "${DIR_MOVIES_DST}" "${DIR_BOOKS_DST}" \
|
||||
# "${DIR_TV_DST}" "${DIR_MUSIC_DST}" \
|
||||
# "${DEFAULT_DST}" 2>/dev/null || true
|
151
install.sh
Normal file → Executable file
151
install.sh
Normal file → Executable file
@@ -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
|
||||
@@ -14,12 +15,13 @@ fi
|
||||
echo "Checking dependencies..."
|
||||
declare -A PKGS=(
|
||||
[transmission-cli]="transmission-remote"
|
||||
[unrar]="unrar"
|
||||
[unrar-free]="unrar-free"
|
||||
[unzip]="unzip"
|
||||
[p7zip-full]="7z"
|
||||
[parallel]="parallel"
|
||||
[bc]="bc"
|
||||
[git]="git"
|
||||
[logrotate]="logrotate"
|
||||
)
|
||||
|
||||
for pkg in "${!PKGS[@]}"; do
|
||||
@@ -31,22 +33,155 @@ 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
|
||||
echo "Config file installed at /etc/torrent/mover.conf"
|
||||
echo "Please run 'torrent-config edit' to set up your configuration"
|
||||
else
|
||||
echo "Existing configuration found at /etc/torrent/mover.conf"
|
||||
echo "New configuration is at /etc/torrent/mover.conf.new"
|
||||
echo "You can compare them with: diff /etc/torrent/mover.conf /etc/torrent/mover.conf.new"
|
||||
fi
|
||||
|
||||
# Run torrent-config to validate the configuration
|
||||
echo "Validating configuration..."
|
||||
if /usr/local/bin/torrent-config validate 2>/dev/null; then
|
||||
echo "Configuration validation passed."
|
||||
else
|
||||
echo "Configuration requires setup. Please run 'torrent-config edit' to configure."
|
||||
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
|
||||
|
||||
# Install helper scripts
|
||||
echo "Installing helper scripts..."
|
||||
if [ -f "${SCRIPT_DIR}/usr/local/bin/torrent-processor" ]; then
|
||||
cp "${SCRIPT_DIR}/usr/local/bin/torrent-processor" /usr/local/bin/
|
||||
chmod 755 /usr/local/bin/torrent-processor
|
||||
echo "- Installed torrent-processor"
|
||||
fi
|
||||
|
||||
if [ -f "${SCRIPT_DIR}/usr/local/bin/smart-processor" ]; then
|
||||
cp "${SCRIPT_DIR}/usr/local/bin/smart-processor" /usr/local/bin/
|
||||
chmod 755 /usr/local/bin/smart-processor
|
||||
echo "- Installed smart-processor"
|
||||
fi
|
||||
|
||||
# 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!"
|
183
usr/local/bin/smart-processor
Executable file
183
usr/local/bin/smart-processor
Executable file
@@ -0,0 +1,183 @@
|
||||
#\!/bin/bash
|
||||
|
||||
# Source configuration
|
||||
source /etc/torrent/mover.conf
|
||||
|
||||
# Reset processed log
|
||||
> /var/log/torrent_processed.log
|
||||
|
||||
# Process all torrents - smart version for shared directories
|
||||
echo "Starting smart torrent processor..."
|
||||
echo "This script will identify and copy files for completed torrents"
|
||||
echo "----------------------------------------------------------------"
|
||||
|
||||
# Make sure destination directories exist
|
||||
mkdir -p /mnt/dsnas1/{Books,Movies,TV,Games,Apps,Music,Other}
|
||||
|
||||
# Get list of torrents
|
||||
IDS=$(transmission-remote "${TRANSMISSION_IP}:${TRANSMISSION_PORT}" \
|
||||
--auth "${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}" \
|
||||
--list | tail -n +2 | head -n -1 | awk '{print $1}' | grep -v "Sum:" | grep -v "[a-zA-Z]")
|
||||
|
||||
# Count torrents
|
||||
TOTAL_TORRENTS=$(echo "$IDS" | wc -l)
|
||||
echo "Found $TOTAL_TORRENTS torrents to process"
|
||||
|
||||
# Process each torrent
|
||||
COUNT=0
|
||||
for id in $IDS; do
|
||||
# Progress counter
|
||||
COUNT=$((COUNT+1))
|
||||
|
||||
# Get torrent info
|
||||
INFO=$(transmission-remote "${TRANSMISSION_IP}:${TRANSMISSION_PORT}" \
|
||||
--auth "${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}" \
|
||||
--torrent $id --info)
|
||||
|
||||
# Extract key information
|
||||
NAME=$(echo "$INFO" | grep "Name:" | awk -F": " '{print $2}' | xargs)
|
||||
HASH=$(echo "$INFO" | grep "Hash:" | awk '{print $2}')
|
||||
PERCENT=$(echo "$INFO" | grep "Percent Done:" | awk '{gsub(/%/, ""); print $3 == "None" ? 0 : $3}')
|
||||
LOCATION=$(echo "$INFO" | grep -i "Location:" | awk -F": " '{print $2}' | xargs)
|
||||
|
||||
# Skip if not 100% complete
|
||||
if [ $(bc <<< "$PERCENT < 100") -eq 1 ]; then
|
||||
echo "[$COUNT/$TOTAL_TORRENTS] Skipping incomplete torrent $id: $NAME ($PERCENT%)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Skip if already processed
|
||||
if grep -q "$HASH" /var/log/torrent_processed.log; then
|
||||
echo "[$COUNT/$TOTAL_TORRENTS] Skipping already processed torrent $id: $NAME"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "[$COUNT/$TOTAL_TORRENTS] Processing torrent $id: $NAME"
|
||||
|
||||
# Apply path mapping
|
||||
SRC="${LOCATION/#$TRANSMISSION_PATH_PREFIX/$LOCAL_PATH_PREFIX}"
|
||||
|
||||
# Set destination based on content type
|
||||
DST="$DEFAULT_DST"
|
||||
|
||||
if [[ "$LOCATION" == */Books* || "$NAME" == *eBook* || "$NAME" == *ePub* ]]; then
|
||||
DST="$DIR_BOOKS_DST"
|
||||
echo " Categorized as: Book"
|
||||
elif [[ "$LOCATION" == */Movies* || "$NAME" == *1080p* || "$NAME" == *720p* ]]; then
|
||||
DST="$DIR_MOVIES_DST"
|
||||
echo " Categorized as: Movie"
|
||||
elif [[ "$LOCATION" == */TV* || "$NAME" == *S0* || "$NAME" == *S1* ]]; then
|
||||
DST="$DIR_TV_DST"
|
||||
echo " Categorized as: TV Show"
|
||||
elif [[ "$LOCATION" == */Games* || "$NAME" == *Game* ]]; then
|
||||
DST="$DIR_GAMES_DST"
|
||||
echo " Categorized as: Game"
|
||||
elif [[ "$LOCATION" == */Apps* || "$NAME" == *App* ]]; then
|
||||
DST="$DIR_APPS_DST"
|
||||
echo " Categorized as: App"
|
||||
elif [[ "$LOCATION" == */Music* || "$NAME" == *MP3* ]]; then
|
||||
DST="$DIR_MUSIC_DST"
|
||||
echo " Categorized as: Music"
|
||||
else
|
||||
echo " Categorized as: Other"
|
||||
fi
|
||||
|
||||
# Make sure destination exists
|
||||
mkdir -p "$DST"
|
||||
|
||||
# Now handle the file copying based on directory structure
|
||||
if [ -d "$SRC" ]; then
|
||||
echo " Source path: $SRC"
|
||||
echo " Destination: $DST"
|
||||
|
||||
# Use find to locate specific content files (ignore small files like NFO)
|
||||
FILES_FOUND=0
|
||||
echo " Looking for media files or content..."
|
||||
|
||||
# Try to find files matching this specific torrent name
|
||||
NAME_PATTERN=$(echo "$NAME" | cut -d'-' -f1 | tr '.' ' ' | xargs | tr '[:upper:]' '[:lower:]')
|
||||
NAME_PATTERN=${NAME_PATTERN// /.}
|
||||
|
||||
echo " Searching for files matching pattern: $NAME_PATTERN"
|
||||
|
||||
# Search for matching files or directories
|
||||
MATCHING_FILES=()
|
||||
while IFS= read -r file; do
|
||||
file_basename=$(basename "$file" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
if [[ "$file_basename" == *"$NAME_PATTERN"* ]]; then
|
||||
size=$(stat -c%s "$file")
|
||||
MATCHING_FILES+=("$file")
|
||||
echo " ✓ Match: $(basename "$file") ($(numfmt --to=iec $size))"
|
||||
fi
|
||||
done < <(find "$SRC" -type f -size +10k | sort -rn -k5 | head -n 20)
|
||||
|
||||
if [ ${#MATCHING_FILES[@]} -gt 0 ]; then
|
||||
echo " Found ${#MATCHING_FILES[@]} matching files for this torrent"
|
||||
|
||||
# Copy up to 3 matched files
|
||||
for ((i=0; i<3 && i<${#MATCHING_FILES[@]}; i++)); do
|
||||
file="${MATCHING_FILES[$i]}"
|
||||
echo " Copying: $(basename "$file") to $DST/"
|
||||
cp -v "$file" "$DST/"
|
||||
FILES_FOUND=$((FILES_FOUND+1))
|
||||
done
|
||||
else
|
||||
echo " No exact matches found - falling back to content type detection"
|
||||
|
||||
# Get a list of content files ordered by size (largest first)
|
||||
while IFS= read -r file; do
|
||||
extension="${file##*.}"
|
||||
extension="${extension,,}" # Convert to lowercase
|
||||
filename=$(basename "$file")
|
||||
|
||||
# Skip small files under 1MB (likely not content)
|
||||
size=$(stat -c%s "$file")
|
||||
|
||||
# Only include files based on type
|
||||
if [[ "$DST" == "$DIR_MOVIES_DST" && "$extension" == @(mkv|mp4|avi) ]]; then
|
||||
echo " Found movie: $filename (Size: $(numfmt --to=iec $size))"
|
||||
echo " Copying to $DST/"
|
||||
cp -v "$file" "$DST/"
|
||||
FILES_FOUND=$((FILES_FOUND+1))
|
||||
elif [[ "$DST" == "$DIR_BOOKS_DST" && "$extension" == @(epub|pdf|mobi) ]]; then
|
||||
echo " Found book: $filename (Size: $(numfmt --to=iec $size))"
|
||||
echo " Copying to $DST/"
|
||||
cp -v "$file" "$DST/"
|
||||
FILES_FOUND=$((FILES_FOUND+1))
|
||||
elif [[ "$DST" == "$DIR_TV_DST" && "$extension" == @(mkv|mp4|avi) ]]; then
|
||||
echo " Found TV episode: $filename (Size: $(numfmt --to=iec $size))"
|
||||
echo " Copying to $DST/"
|
||||
cp -v "$file" "$DST/"
|
||||
FILES_FOUND=$((FILES_FOUND+1))
|
||||
elif [[ "$size" -gt 1000000 ]]; then # 1MB for other content types
|
||||
echo " Found content: $filename (Size: $(numfmt --to=iec $size))"
|
||||
echo " Copying to $DST/"
|
||||
cp -v "$file" "$DST/"
|
||||
FILES_FOUND=$((FILES_FOUND+1))
|
||||
fi
|
||||
|
||||
# Limit to first 3 content files to avoid excessive copying
|
||||
if [ $FILES_FOUND -ge 3 ]; then
|
||||
echo " Reached limit of 3 content files"
|
||||
break
|
||||
fi
|
||||
done < <(find "$SRC" -type f -size +100k | sort -rn -k5 | head -n 10)
|
||||
fi
|
||||
|
||||
if [ $FILES_FOUND -gt 0 ]; then
|
||||
echo " ✅ Successfully copied $FILES_FOUND files"
|
||||
# Mark as processed
|
||||
echo "$HASH" >> /var/log/torrent_processed.log
|
||||
else
|
||||
echo " ❌ No suitable content files found"
|
||||
fi
|
||||
else
|
||||
echo " ❌ Source directory not found: $SRC"
|
||||
fi
|
||||
|
||||
echo "------------------------------------------------------"
|
||||
done
|
||||
|
||||
echo "Smart torrent processing completed"
|
||||
echo "Processed torrents are recorded in /var/log/torrent_processed.log"
|
481
usr/local/bin/torrent-config
Executable file
481
usr/local/bin/torrent-config
Executable 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
|
||||
fi
|
||||
|
||||
# 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
|
@@ -1,14 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Torrent Mover v7.2 - 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,17 +78,32 @@ main() {
|
||||
"${DIR_BOOKS_DST}"
|
||||
"${DEFAULT_DST}"
|
||||
)
|
||||
|
||||
# Add optional directories if defined
|
||||
[[ -n "${DIR_TV_DST}" ]] && REQUIRED_DIRS+=("${DIR_TV_DST}")
|
||||
[[ -n "${DIR_MUSIC_DST}" ]] && REQUIRED_DIRS+=("${DIR_MUSIC_DST}")
|
||||
|
||||
# Create required directories if they don't exist
|
||||
log_info "Creating required directories if they don't exist..."
|
||||
for dir in "${REQUIRED_DIRS[@]}"; do
|
||||
if [[ ! -d "${dir}" ]]; then
|
||||
log_error "Directory missing: ${dir}"
|
||||
exit 1
|
||||
if [[ -n "$dir" ]]; then
|
||||
if [[ ! -d "$dir" ]]; then
|
||||
log_info "Creating directory: $dir"
|
||||
if mkdir -p "$dir"; then
|
||||
# Try to set permissions but don't fail if it doesn't work
|
||||
chmod 775 "$dir" 2>/dev/null || log_warn "Could not set permissions on $dir"
|
||||
chown ${TORRENT_USER:-debian-transmission}:${TORRENT_GROUP:-debian-transmission} "$dir" 2>/dev/null || log_warn "Could not set ownership on $dir"
|
||||
log_info "Created directory: $dir"
|
||||
else
|
||||
log_error "Failed to create directory: $dir"
|
||||
fi
|
||||
fi
|
||||
if [[ ! -w "${dir}" ]]; then
|
||||
log_error "Write permission denied: ${dir}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Now validate that all required directories exist and are writable
|
||||
validate_directories "${REQUIRED_DIRS[@]}" || exit 1
|
||||
|
||||
init_checksum_db
|
||||
|
||||
if (( CACHE_WARMUP )); then
|
||||
@@ -429,14 +111,36 @@ 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
|
||||
log_debug "Getting list of torrents..."
|
||||
local torrent_ids
|
||||
torrent_ids=$(get_torrents)
|
||||
log_info "Found $(echo "$torrent_ids" | wc -l) torrents"
|
||||
|
||||
# Use a regular for loop instead of a pipe to while
|
||||
# to avoid the subshell issue that causes processed_source_dirs to be lost
|
||||
readarray -t torrent_ids_array <<< "$torrent_ids"
|
||||
|
||||
# Print the torrent IDs to debug (always, not just in debug mode)
|
||||
if [[ ${#torrent_ids_array[@]} -eq 0 ]]; then
|
||||
log_info "No torrents found to process"
|
||||
else
|
||||
log_info "Torrent IDs to process: ${torrent_ids_array[*]}"
|
||||
fi
|
||||
for id in "${torrent_ids_array[@]}"; do
|
||||
# Skip empty IDs
|
||||
if [[ -z "$id" ]]; then
|
||||
log_debug "Skipping empty torrent ID"
|
||||
continue
|
||||
fi
|
||||
|
||||
log_debug "Processing torrent ID: $id"
|
||||
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
|
||||
@@ -452,26 +156,87 @@ main() {
|
||||
# Extract Transmission-reported directory and translate to local path.
|
||||
local reported_dir
|
||||
reported_dir=$(grep -i "Location:" <<< "${info}" | awk -F": " '{print $2}' | xargs)
|
||||
log_debug "Raw reported directory: '${reported_dir}'"
|
||||
|
||||
# If the reported directory is empty, try to derive it from the name
|
||||
if [[ -z "${reported_dir}" ]]; then
|
||||
local name
|
||||
name=$(grep -i "Name:" <<< "${info}" | awk -F": " '{print $2}' | xargs)
|
||||
log_debug "Torrent name: '${name}'"
|
||||
|
||||
# Check if there are labels we can use
|
||||
local labels
|
||||
labels=$(grep -i "Labels:" <<< "${info}" | awk -F": " '{print $2}' | xargs)
|
||||
log_debug "Torrent labels: '${labels}'"
|
||||
|
||||
if [[ "${labels}" == *"Books"* ]]; then
|
||||
reported_dir="/downloads/Books"
|
||||
elif [[ "${labels}" == *"Movies"* ]]; then
|
||||
reported_dir="/downloads/Movies"
|
||||
elif [[ "${labels}" == *"TV"* ]]; then
|
||||
reported_dir="/downloads/TV"
|
||||
elif [[ "${labels}" == *"Games"* ]]; then
|
||||
reported_dir="/downloads/Games"
|
||||
elif [[ "${labels}" == *"Apps"* ]]; then
|
||||
reported_dir="/downloads/Apps"
|
||||
elif [[ "${labels}" == *"Music"* ]]; then
|
||||
reported_dir="/downloads/Music"
|
||||
else
|
||||
# Default to Other if we can't determine
|
||||
reported_dir="/downloads/Other"
|
||||
fi
|
||||
log_debug "Derived directory from labels: '${reported_dir}'"
|
||||
fi
|
||||
|
||||
local dir
|
||||
dir=$(translate_source "${reported_dir}")
|
||||
log_info "Torrent source directory reported: '${reported_dir}' translated to '${dir}'"
|
||||
log_info "Torrent source directory: '${reported_dir}' translated to '${dir}'"
|
||||
|
||||
# Initialize empty directory mapping if needed
|
||||
if [[ -z "$dir" ]]; then
|
||||
log_warn "Empty directory path detected, using default"
|
||||
dir="${LOCAL_PATH_PREFIX}/Other"
|
||||
fi
|
||||
|
||||
local dst
|
||||
dst=$(get_destination "${dir}")
|
||||
|
||||
# Detect same-path mappings (different mounts)
|
||||
if [[ "${dir}" != "${dst}" && "${dir}" =~ ^/mnt/dsnas2/ && "${dst}" =~ ^/mnt/dsnas1/ ]]; then
|
||||
local dir_suffix="${dir#/mnt/dsnas2/}"
|
||||
local dst_suffix="${dst#/mnt/dsnas1/}"
|
||||
if [[ "${dir_suffix}" == "${dst_suffix}" ]]; then
|
||||
log_info "Source and destination are the same logical location with different mounts: ${dir_suffix}"
|
||||
mark_processed "${hash}"
|
||||
continue # Skip to next torrent
|
||||
fi
|
||||
fi
|
||||
|
||||
# Initialize warned_dirs for this directory if needed
|
||||
if [[ -n "${dir}" ]]; then
|
||||
[[ -z "${warned_dirs["${dir}"]+x}" ]] && warned_dirs["${dir}"]=0
|
||||
fi
|
||||
|
||||
# Avoid processing the same directory more than once.
|
||||
if [[ -n "${processed_source_dirs["${dir}"]+x}" ]]; then
|
||||
log_info "Directory ${dir} has already been processed; skipping copy for torrent ${id}"
|
||||
elif (( $(bc <<< "${percent_done} >= 100") )) && ! is_processed "${hash}"; then
|
||||
log_info "Processing completed torrent ${id} (${percent_done}% done)"
|
||||
if [[ "${dst}" == "${DEFAULT_DST}" ]] && (( warned_dirs["${dir}"] == 0 )); then
|
||||
if [[ "${dst}" == "${DEFAULT_DST}" ]] && [[ -n "${dir}" ]] && (( warned_dirs["${dir}"] == 0 )); then
|
||||
log_warn "Using default destination for: ${dir}"
|
||||
warned_dirs["${dir}"]=1
|
||||
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 +254,21 @@ 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}"
|
||||
# Print count of processed directories
|
||||
if [[ "${DEBUG}" -eq 1 ]]; then
|
||||
log_debug "Processed source directories count: ${#processed_source_dirs[@]}"
|
||||
for dir in "${!processed_source_dirs[@]}"; do
|
||||
log_debug "Processed directory: $dir"
|
||||
done
|
||||
fi
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
######################
|
||||
|
368
usr/local/bin/torrent-processor
Executable file
368
usr/local/bin/torrent-processor
Executable file
@@ -0,0 +1,368 @@
|
||||
#\!/bin/bash
|
||||
|
||||
# Source configuration
|
||||
source /etc/torrent/mover.conf
|
||||
|
||||
# Create destination directories
|
||||
mkdir -p /mnt/dsnas1/{Books,Movies,TV,Games,Apps,Music,Other}
|
||||
|
||||
# Function to display help
|
||||
show_help() {
|
||||
echo "Torrent Processor - Helper for torrent-mover"
|
||||
echo ""
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --reset Clear processed log to re-process all torrents"
|
||||
echo " --books Process only book torrents"
|
||||
echo " --movies Process only movie torrents"
|
||||
echo " --tv Process only TV show torrents"
|
||||
echo " --apps Process only application torrents"
|
||||
echo " --games Process only game torrents"
|
||||
echo " --id NUMBER Process a specific torrent ID"
|
||||
echo " --help Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 --reset --books Process all book torrents (even if previously processed)"
|
||||
echo " $0 --id 123 Process only torrent with ID 123"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Parse command line options
|
||||
RESET=0
|
||||
CATEGORY=""
|
||||
TORRENT_ID=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
key="$1"
|
||||
case $key in
|
||||
--reset)
|
||||
RESET=1
|
||||
shift
|
||||
;;
|
||||
--books)
|
||||
CATEGORY="books"
|
||||
shift
|
||||
;;
|
||||
--movies)
|
||||
CATEGORY="movies"
|
||||
shift
|
||||
;;
|
||||
--tv)
|
||||
CATEGORY="tv"
|
||||
shift
|
||||
;;
|
||||
--apps)
|
||||
CATEGORY="apps"
|
||||
shift
|
||||
;;
|
||||
--games)
|
||||
CATEGORY="games"
|
||||
shift
|
||||
;;
|
||||
--id)
|
||||
TORRENT_ID="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $key"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Reset processed log if requested
|
||||
if [ $RESET -eq 1 ]; then
|
||||
echo "Clearing processed log to re-process all torrents"
|
||||
> /var/log/torrent_processed.log
|
||||
fi
|
||||
|
||||
# Remove lock file if it exists
|
||||
rm -f /var/lock/torrent-mover.lock
|
||||
|
||||
# Run torrent-mover based on options
|
||||
if [ -n "$TORRENT_ID" ]; then
|
||||
echo "Processing torrent ID: $TORRENT_ID"
|
||||
|
||||
# Get torrent details
|
||||
info=$(transmission-remote "${TRANSMISSION_IP}:${TRANSMISSION_PORT}" \
|
||||
--auth "${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}" \
|
||||
--torrent $TORRENT_ID --info)
|
||||
|
||||
name=$(echo "$info" | grep "Name:" | awk -F": " '{print $2}' | xargs)
|
||||
echo "Torrent name: $name"
|
||||
|
||||
# Run torrent-mover with specific torrent ID
|
||||
torrent_id="$TORRENT_ID"
|
||||
|
||||
# Check if output directory exists for this torrent
|
||||
output_dir=$(grep "Location:" <<< "$info" | awk -F": " '{print $2}' | xargs)
|
||||
if [[ -n "$output_dir" ]]; then
|
||||
echo "Torrent location: $output_dir"
|
||||
fi
|
||||
|
||||
# We need to modify torrent-mover to handle single IDs
|
||||
# For now, we'll write a small temporary script to process just this ID
|
||||
TMP_SCRIPT=$(mktemp)
|
||||
cat > "$TMP_SCRIPT" << EOF
|
||||
#!/bin/bash
|
||||
source /etc/torrent/mover.conf
|
||||
source /usr/local/lib/torrent-mover/common.sh
|
||||
source /usr/local/lib/torrent-mover/file_operations.sh
|
||||
source /usr/local/lib/torrent-mover/transmission_handler.sh
|
||||
source /usr/local/lib/torrent-mover/archive_handler.sh
|
||||
|
||||
# Set debug mode
|
||||
DEBUG=1
|
||||
|
||||
# Process just this one torrent
|
||||
process_single_torrent() {
|
||||
local id="\$1"
|
||||
log_debug "Processing single torrent ID: \$id"
|
||||
|
||||
# Get torrent info
|
||||
local info cmd
|
||||
cmd="transmission-remote \${TRANSMISSION_IP}:\${TRANSMISSION_PORT} -n \${TRANSMISSION_USER}:\${TRANSMISSION_PASSWORD} -t \${id} -i"
|
||||
info=\$(eval "\$cmd")
|
||||
|
||||
if [[ -z "\$info" ]]; then
|
||||
log_error "Failed to get info for torrent \$id"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Process torrent info just like in the main script
|
||||
local hash
|
||||
hash=\$(grep "Hash:" <<< "\${info}" | awk '{print \$2}')
|
||||
local ratio
|
||||
ratio=\$(grep "Ratio:" <<< "\${info}" | awk '{print \$2 == "None" ? 0 : \$2}' | tr -cd '0-9.')
|
||||
ratio=\${ratio:-0}
|
||||
local time
|
||||
time=\$(grep "Seeding Time:" <<< "\${info}" | awk '{print \$3 == "None" ? 0 : \$3}' | tr -cd '0-9.')
|
||||
time=\${time:-0}
|
||||
local percent_done
|
||||
percent_done=\$(grep "Percent Done:" <<< "\${info}" | awk '{gsub(/%/, ""); print \$3 == "None" ? 0 : \$3}')
|
||||
percent_done=\${percent_done:-0}
|
||||
|
||||
# Extract Transmission-reported directory and translate to local path.
|
||||
local reported_dir
|
||||
reported_dir=\$(grep -i "Location:" <<< "\${info}" | awk -F": " '{print \$2}' | xargs)
|
||||
log_debug "Raw reported directory: '\${reported_dir}'"
|
||||
|
||||
# If the reported directory is empty, try to derive it from the name
|
||||
if [[ -z "\${reported_dir}" ]]; then
|
||||
local name
|
||||
name=\$(grep -i "Name:" <<< "\${info}" | awk -F": " '{print \$2}' | xargs)
|
||||
log_debug "Torrent name: '\${name}'"
|
||||
|
||||
# Check if there are labels we can use
|
||||
local labels
|
||||
labels=\$(grep -i "Labels:" <<< "\${info}" | awk -F": " '{print \$2}' | xargs)
|
||||
log_debug "Torrent labels: '\${labels}'"
|
||||
|
||||
if [[ "\${labels}" == *"Books"* ]]; then
|
||||
reported_dir="/downloads/Books"
|
||||
elif [[ "\${labels}" == *"Movies"* ]]; then
|
||||
reported_dir="/downloads/Movies"
|
||||
elif [[ "\${labels}" == *"TV"* ]]; then
|
||||
reported_dir="/downloads/TV"
|
||||
elif [[ "\${labels}" == *"Games"* ]]; then
|
||||
reported_dir="/downloads/Games"
|
||||
elif [[ "\${labels}" == *"Apps"* ]]; then
|
||||
reported_dir="/downloads/Apps"
|
||||
elif [[ "\${labels}" == *"Music"* ]]; then
|
||||
reported_dir="/downloads/Music"
|
||||
else
|
||||
# Default to Other if we can't determine
|
||||
reported_dir="/downloads/Other"
|
||||
fi
|
||||
log_debug "Derived directory from labels: '\${reported_dir}'"
|
||||
fi
|
||||
|
||||
local dir
|
||||
dir=\$(translate_source "\${reported_dir}")
|
||||
log_info "Torrent source directory: '\${reported_dir}' translated to '\${dir}'"
|
||||
|
||||
# Initialize empty directory mapping if needed
|
||||
if [[ -z "\$dir" ]]; then
|
||||
log_warn "Empty directory path detected, using default"
|
||||
dir="\${LOCAL_PATH_PREFIX}/Other"
|
||||
fi
|
||||
|
||||
local dst
|
||||
dst=\$(get_destination "\${dir}")
|
||||
|
||||
# Process the torrent
|
||||
if (( \$(bc <<< "\${percent_done} >= 100") )) && ! is_processed "\${hash}"; then
|
||||
log_info "Processing completed torrent \${id} (\${percent_done}% done)"
|
||||
process_copy "\${id}" "\${hash}" "\${dir}" "\${dst}"
|
||||
else
|
||||
log_info "Torrent \${id} already processed or not complete"
|
||||
fi
|
||||
|
||||
# Check seed ratio/time criteria
|
||||
if (( \$(bc <<< "\${ratio} >= \${SEED_RATIO}") )) || (( \$(bc <<< "\${time} >= \${SEED_TIME}") )); then
|
||||
log_info "Removing torrent \${id} (Ratio: \${ratio}, Time: \${time})"
|
||||
process_removal "\${id}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main function
|
||||
process_single_torrent "$torrent_id"
|
||||
EOF
|
||||
|
||||
chmod +x "$TMP_SCRIPT"
|
||||
"$TMP_SCRIPT"
|
||||
rm -f "$TMP_SCRIPT"
|
||||
elif [ -n "$CATEGORY" ]; then
|
||||
echo "Processing category: $CATEGORY"
|
||||
|
||||
# Set category-specific filter
|
||||
CATEGORY_PATH=""
|
||||
PATTERN=""
|
||||
case $CATEGORY in
|
||||
books)
|
||||
echo "Looking for book torrents..."
|
||||
CATEGORY_PATH="/downloads/Books"
|
||||
PATTERN="*books*|*ebook*|*epub*|*pdf*"
|
||||
;;
|
||||
movies)
|
||||
echo "Looking for movie torrents..."
|
||||
CATEGORY_PATH="/downloads/Movies"
|
||||
PATTERN="*movies*|*film*|*video*"
|
||||
;;
|
||||
tv)
|
||||
echo "Looking for TV show torrents..."
|
||||
CATEGORY_PATH="/downloads/TV"
|
||||
PATTERN="*tv*|*series*|*episode*"
|
||||
;;
|
||||
apps)
|
||||
echo "Looking for application torrents..."
|
||||
CATEGORY_PATH="/downloads/Apps"
|
||||
PATTERN="*apps*|*applications*|*programs*|*software*"
|
||||
;;
|
||||
games)
|
||||
echo "Looking for game torrents..."
|
||||
CATEGORY_PATH="/downloads/Games"
|
||||
PATTERN="*games*"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Create a script to process just this category
|
||||
TMP_SCRIPT=$(mktemp)
|
||||
cat > "$TMP_SCRIPT" << EOF
|
||||
#!/bin/bash
|
||||
source /etc/torrent/mover.conf
|
||||
source /usr/local/lib/torrent-mover/common.sh
|
||||
source /usr/local/lib/torrent-mover/file_operations.sh
|
||||
source /usr/local/lib/torrent-mover/transmission_handler.sh
|
||||
source /usr/local/lib/torrent-mover/archive_handler.sh
|
||||
|
||||
# Set debug mode
|
||||
DEBUG=1
|
||||
|
||||
# Get all torrents
|
||||
get_torrent_ids() {
|
||||
local cmd="transmission-remote \${TRANSMISSION_IP}:\${TRANSMISSION_PORT} -n \${TRANSMISSION_USER}:\${TRANSMISSION_PASSWORD} -l"
|
||||
local output
|
||||
output=\$(retry_command "\$cmd" 3 20)
|
||||
echo "\$output" | awk 'NR>1 && NF>1 {gsub(/^[ ]+/, "", \$1); if (\$1 ~ /^[0-9]+\$/) print \$1}'
|
||||
}
|
||||
|
||||
# Process category torrents
|
||||
process_category_torrents() {
|
||||
local category_path="$CATEGORY_PATH"
|
||||
local pattern="$PATTERN"
|
||||
log_debug "Processing category: $CATEGORY with path \$category_path and pattern '\$pattern'"
|
||||
|
||||
# Get list of all torrents
|
||||
local torrent_ids=\$(get_torrent_ids)
|
||||
|
||||
# Process each torrent
|
||||
for id in \$torrent_ids; do
|
||||
# Get torrent info
|
||||
local info cmd
|
||||
cmd="transmission-remote \${TRANSMISSION_IP}:\${TRANSMISSION_PORT} -n \${TRANSMISSION_USER}:\${TRANSMISSION_PASSWORD} -t \${id} -i"
|
||||
info=\$(eval "\$cmd")
|
||||
|
||||
if [[ -z "\$info" ]]; then
|
||||
log_warn "Failed to get info for torrent \$id, skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract name and location
|
||||
local name=\$(grep -i "Name:" <<< "\$info" | awk -F": " '{print \$2}' | xargs)
|
||||
local reported_dir=\$(grep -i "Location:" <<< "\$info" | awk -F": " '{print \$2}' | xargs)
|
||||
local labels=\$(grep -i "Labels:" <<< "\$info" | awk -F": " '{print \$2}' | xargs)
|
||||
|
||||
# Check if this torrent matches our category
|
||||
if [[ "\$reported_dir" == "\$category_path" ]] ||
|
||||
[[ "\$labels" == *"$CATEGORY"* ]] ||
|
||||
[[ "\$name" =~ \$pattern ]]; then
|
||||
|
||||
log_info "Found matching torrent: \$id - \$name"
|
||||
|
||||
# Process torrent info
|
||||
local hash=\$(grep "Hash:" <<< "\$info" | awk '{print \$2}')
|
||||
local ratio=\$(grep "Ratio:" <<< "\$info" | awk '{print \$2 == "None" ? 0 : \$2}' | tr -cd '0-9.')
|
||||
ratio=\${ratio:-0}
|
||||
local time=\$(grep "Seeding Time:" <<< "\$info" | awk '{print \$3 == "None" ? 0 : \$3}' | tr -cd '0-9.')
|
||||
time=\${time:-0}
|
||||
local percent_done=\$(grep "Percent Done:" <<< "\$info" | awk '{gsub(/%/, ""); print \$3 == "None" ? 0 : \$3}')
|
||||
percent_done=\${percent_done:-0}
|
||||
|
||||
# If the reported directory is empty, derive it
|
||||
if [[ -z "\$reported_dir" ]]; then
|
||||
reported_dir="\$category_path"
|
||||
log_debug "Using derived directory: '\$reported_dir'"
|
||||
fi
|
||||
|
||||
# Process the torrent
|
||||
local dir=\$(translate_source "\$reported_dir")
|
||||
log_info "Torrent source directory: '\$reported_dir' translated to '\$dir'"
|
||||
|
||||
# Initialize empty directory mapping if needed
|
||||
if [[ -z "\$dir" ]]; then
|
||||
log_warn "Empty directory path detected, using default"
|
||||
dir="\${LOCAL_PATH_PREFIX}/$CATEGORY"
|
||||
fi
|
||||
|
||||
local dst=\$(get_destination "\$dir")
|
||||
|
||||
# Process the torrent
|
||||
if (( \$(bc <<< "\${percent_done} >= 100") )) && ! is_processed "\${hash}"; then
|
||||
log_info "Processing completed torrent \${id} (\${percent_done}% done)"
|
||||
process_copy "\${id}" "\${hash}" "\${dir}" "\${dst}"
|
||||
else
|
||||
log_info "Torrent \${id} already processed or not complete"
|
||||
fi
|
||||
|
||||
# Check seed ratio/time criteria
|
||||
if (( \$(bc <<< "\${ratio} >= \${SEED_RATIO}") )) || (( \$(bc <<< "\${time} >= \${SEED_TIME}") )); then
|
||||
log_info "Removing torrent \${id} (Ratio: \${ratio}, Time: \${time})"
|
||||
process_removal "\${id}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Main function
|
||||
process_category_torrents
|
||||
EOF
|
||||
|
||||
chmod +x "$TMP_SCRIPT"
|
||||
"$TMP_SCRIPT"
|
||||
rm -f "$TMP_SCRIPT"
|
||||
else
|
||||
echo "Processing all torrents"
|
||||
# Run the main torrent-mover script directly
|
||||
/usr/local/bin/torrent-mover --debug
|
||||
fi
|
||||
|
||||
echo "Processing complete\!"
|
||||
echo "Check /var/log/torrent_mover.log for details"
|
134
usr/local/lib/torrent-mover/archive_handler.sh
Normal file
134
usr/local/lib/torrent-mover/archive_handler.sh
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/bin/bash
|
||||
# Archive extraction handler for torrent-mover
|
||||
|
||||
# extract_single_archive: Extract a single archive with proper error handling
|
||||
extract_single_archive() {
|
||||
local archive="$1"
|
||||
local target_dir="$2"
|
||||
local archive_type="${archive##*.}"
|
||||
local extract_success=1
|
||||
local tmp_marker="${target_dir}/.extraction_in_progress"
|
||||
|
||||
# Create extraction marker to indicate incomplete extraction
|
||||
touch "${tmp_marker}"
|
||||
|
||||
# Ensure proper permissions for extraction directory
|
||||
chmod 775 "${target_dir}"
|
||||
chown ${TORRENT_USER:-debian-transmission}:${TORRENT_GROUP:-debian-transmission} "${target_dir}"
|
||||
|
||||
# Extract based on archive type
|
||||
case "${archive_type,,}" in # Use lowercase comparison
|
||||
rar)
|
||||
log_debug "Extracting RAR archive: ${archive}"
|
||||
# Check which unrar variant is available
|
||||
if command -v unrar-free &>/dev/null; then
|
||||
# unrar-free has different syntax
|
||||
retry_command "unrar-free x \"${archive}\" \"${target_dir}\"" 3 10
|
||||
else
|
||||
retry_command "unrar x -o- \"${archive}\" \"${target_dir}\"" 3 10
|
||||
fi
|
||||
extract_success=$?
|
||||
;;
|
||||
zip)
|
||||
log_debug "Extracting ZIP archive: ${archive}"
|
||||
retry_command "unzip -o \"${archive}\" -d \"${target_dir}\"" 3 10
|
||||
extract_success=$?
|
||||
;;
|
||||
7z|7zip)
|
||||
log_debug "Extracting 7Z archive: ${archive}"
|
||||
retry_command "7z x \"${archive}\" -o\"${target_dir}\"" 3 10
|
||||
extract_success=$?
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown archive type: ${archive_type}"
|
||||
extract_success=1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Apply consistent permissions to all extracted files and directories
|
||||
if [[ ${extract_success} -eq 0 ]]; then
|
||||
log_debug "Setting permissions for extracted files in ${target_dir}"
|
||||
find "${target_dir}" -type d -exec chmod 775 {} \;
|
||||
find "${target_dir}" -type f -exec chmod 664 {} \;
|
||||
find "${target_dir}" -exec chown ${TORRENT_USER:-debian-transmission}:${TORRENT_GROUP:-debian-transmission} {} \;
|
||||
|
||||
# Remove the extraction marker to indicate successful completion
|
||||
rm -f "${tmp_marker}"
|
||||
return 0
|
||||
else
|
||||
log_error "Extraction failed for ${archive}"
|
||||
# Keep marker to indicate failed extraction
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# handle_archives: Process all archives in a source directory
|
||||
# Returns: 0 if all archives extracted successfully or no archives found, 1 if any failed
|
||||
handle_archives() {
|
||||
local src="$1" dst="$2"
|
||||
local overall_success=0
|
||||
local archive_found=0
|
||||
local extraction_errors=0
|
||||
|
||||
# Check if source and destination are valid
|
||||
if [[ ! -d "${src}" ]]; then
|
||||
log_error "Source directory missing: ${src}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "${dst}" ]]; then
|
||||
log_error "Destination directory missing: ${dst}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Find all archives and extract them
|
||||
find "${src}" -type f \( -iname "*.rar" -o -iname "*.zip" -o -iname "*.7z" \) | while read -r arch; do
|
||||
archive_found=1
|
||||
log_info "Processing archive: ${arch}"
|
||||
|
||||
# Create extraction subdirectory
|
||||
local base
|
||||
base=$(basename "${arch}")
|
||||
local subdir="${dst}/${base%.*}"
|
||||
|
||||
if ! mkdir -p "${subdir}"; then
|
||||
log_error "Failed to create subdirectory ${subdir} for archive extraction"
|
||||
extraction_errors=$((extraction_errors + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract the archive
|
||||
if ! extract_single_archive "${arch}" "${subdir}"; then
|
||||
log_error "Extraction failed for ${arch}"
|
||||
extraction_errors=$((extraction_errors + 1))
|
||||
else
|
||||
log_info "Archive ${arch} extracted successfully to ${subdir}"
|
||||
log_info "Archive ${arch} retained in source until ratio limits are reached."
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for cleanup of any incomplete extractions from previous runs
|
||||
find "${dst}" -name ".extraction_in_progress" | while read -r marker; do
|
||||
local problem_dir=$(dirname "${marker}")
|
||||
log_warn "Found incomplete extraction in ${problem_dir} from previous run"
|
||||
|
||||
# Option 1: Remove incomplete directory
|
||||
# rm -rf "${problem_dir}"
|
||||
|
||||
# Option 2: Mark as incomplete but leave content
|
||||
touch "${problem_dir}/.incomplete_extraction"
|
||||
rm -f "${marker}"
|
||||
done
|
||||
|
||||
# Return success if no archives found or all extracted successfully
|
||||
if [[ ${archive_found} -eq 0 ]]; then
|
||||
log_debug "No archives found in ${src}"
|
||||
return 0
|
||||
elif [[ ${extraction_errors} -eq 0 ]]; then
|
||||
log_info "All archives extracted successfully"
|
||||
return 0
|
||||
else
|
||||
log_warn "${extraction_errors} archives failed to extract properly"
|
||||
return 1
|
||||
fi
|
||||
}
|
290
usr/local/lib/torrent-mover/common.sh
Normal file
290
usr/local/lib/torrent-mover/common.sh
Normal file
@@ -0,0 +1,290 @@
|
||||
#!/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
|
||||
if [[ "${USE_SYSLOG}" == "true" ]]; then
|
||||
logger -t torrent-mover "[DEBUG] $*" || true
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
log_info() {
|
||||
echo -e "[INFO] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2
|
||||
if [[ "${USE_SYSLOG}" == "true" ]]; then
|
||||
logger -t torrent-mover "[INFO] $*" || true
|
||||
fi
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "[WARN] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2
|
||||
if [[ "${USE_SYSLOG}" == "true" ]]; then
|
||||
logger -t torrent-mover "[WARN] $*" || true
|
||||
fi
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "[ERROR] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2
|
||||
if [[ "${USE_SYSLOG}" == "true" ]]; then
|
||||
logger -t torrent-mover "[ERROR] $*" || true
|
||||
fi
|
||||
}
|
||||
|
||||
# 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" "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 for unrar or unrar-free
|
||||
if command -v unrar &>/dev/null; then
|
||||
log_debug "Found unrar command"
|
||||
elif command -v unrar-free &>/dev/null; then
|
||||
log_debug "Found unrar-free command"
|
||||
# Create an alias for unrar to point to unrar-free
|
||||
alias unrar="unrar-free"
|
||||
else
|
||||
log_error "Missing dependency: unrar or unrar-free"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# check_disk_usage: Warn if disk usage is over 90%.
|
||||
check_disk_usage() {
|
||||
local dir="$1"
|
||||
[[ -z "${dir}" ]] && return
|
||||
|
||||
log_debug "Checking disk usage for directory: ${dir}"
|
||||
|
||||
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}')
|
||||
|
||||
if [[ -z "${mount_point}" ]]; then
|
||||
log_warn "Could not determine mount point for: ${dir}"
|
||||
return
|
||||
fi
|
||||
|
||||
log_debug "Mount point for ${dir} is ${mount_point}"
|
||||
|
||||
# Initialize CHECKED_MOUNTS as an empty array if not already done
|
||||
if [[ -z "${CHECKED_MOUNTS+x}" ]]; then
|
||||
declare -A CHECKED_MOUNTS
|
||||
fi
|
||||
|
||||
# Check if we've already checked this mount point
|
||||
if [[ -z "${CHECKED_MOUNTS[${mount_point}]+x}" ]]; then
|
||||
local usage
|
||||
usage=$(df -P "${dir}" | awk 'NR==2 {sub(/%/, "", $5); print $5}')
|
||||
log_debug "Usage for ${mount_point}: ${usage}%"
|
||||
|
||||
if (( usage >= 90 )); then
|
||||
log_warn "Storage warning: ${mount_point} at ${usage}% capacity"
|
||||
fi
|
||||
|
||||
CHECKED_MOUNTS[${mount_point}]=1
|
||||
else
|
||||
log_debug "Mount point ${mount_point} already checked"
|
||||
fi
|
||||
}
|
||||
|
||||
# run_command_safely: Safer version of command execution that prevents injection
|
||||
run_command_safely() {
|
||||
# Instead of using eval with a command string, this function accepts the command and arguments separately
|
||||
# This prevents command injection vulnerabilities
|
||||
if [[ $# -eq 0 ]]; then
|
||||
log_error "No command provided to run_command_safely"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_debug "Running command: $*"
|
||||
"$@"
|
||||
return $?
|
||||
}
|
||||
|
||||
# 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
|
||||
local exit_code=0
|
||||
local command_output=""
|
||||
|
||||
# Create a temporary file for capturing output
|
||||
local output_file
|
||||
output_file=$(mktemp)
|
||||
|
||||
# Use a more verbose logging for this command - always log, not just in debug mode
|
||||
log_info "Executing command: $cmd"
|
||||
|
||||
while (( attempt <= max_attempts )); do
|
||||
log_info "Attempt $attempt of $max_attempts: $cmd"
|
||||
|
||||
# Execute command directly and capture output and exit code
|
||||
command_output=$(eval "$cmd" 2>&1)
|
||||
exit_code=$?
|
||||
echo "$command_output" > "${output_file}"
|
||||
|
||||
# Always log the first 10 lines of output
|
||||
log_info "Command output (first 10 lines):"
|
||||
head -n 10 "${output_file}" | while IFS= read -r line; do
|
||||
log_info " > $line"
|
||||
done
|
||||
|
||||
if [[ ${exit_code} -eq 0 ]]; then
|
||||
log_info "Command succeeded on attempt $attempt"
|
||||
rm -f "${output_file}"
|
||||
echo "$command_output"
|
||||
return 0
|
||||
else
|
||||
# Log detailed error information
|
||||
log_warn "Command failed (attempt $attempt, exit code: ${exit_code})"
|
||||
|
||||
if (( attempt == max_attempts )); then
|
||||
log_error "Maximum attempts reached for command, last exit code: ${exit_code}"
|
||||
log_error "Last error output (first 10 lines):"
|
||||
head -n 10 "${output_file}" | while IFS= read -r line; do
|
||||
log_error " > $line"
|
||||
done
|
||||
rm -f "${output_file}"
|
||||
echo "$command_output"
|
||||
return ${exit_code}
|
||||
fi
|
||||
|
||||
# Exponential backoff - wait longer for each successive attempt
|
||||
local adjusted_wait=$((wait_time * attempt))
|
||||
log_info "Waiting ${adjusted_wait} seconds before retry"
|
||||
sleep ${adjusted_wait}
|
||||
(( attempt++ ))
|
||||
fi
|
||||
done
|
||||
|
||||
rm -f "${output_file}"
|
||||
echo "$command_output"
|
||||
return 1
|
||||
}
|
||||
|
||||
# run_in_transaction: Runs commands with an atomic operation guarantee
|
||||
# If any command fails, attempts to roll back changes
|
||||
run_in_transaction() {
|
||||
local action_desc="$1"
|
||||
local cleanup_cmd="$2"
|
||||
local main_cmd="$3"
|
||||
|
||||
log_debug "Starting transaction: ${action_desc}"
|
||||
|
||||
# Create marker file to indicate transaction in progress
|
||||
local transaction_id
|
||||
transaction_id=$(date +%s)-$$
|
||||
local transaction_marker="/tmp/torrent-mover-transaction-${transaction_id}"
|
||||
echo "${action_desc}" > "${transaction_marker}"
|
||||
|
||||
# Execute the main command
|
||||
if ! eval "${main_cmd}"; then
|
||||
log_error "Transaction failed: ${action_desc}"
|
||||
|
||||
# Only run cleanup if it exists
|
||||
if [[ -n "${cleanup_cmd}" ]]; then
|
||||
log_info "Attempting transaction rollback"
|
||||
if ! eval "${cleanup_cmd}"; then
|
||||
log_error "Rollback failed, manual intervention may be required"
|
||||
else
|
||||
log_info "Rollback completed successfully"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean up marker
|
||||
rm -f "${transaction_marker}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Clean up marker on success
|
||||
rm -f "${transaction_marker}"
|
||||
log_debug "Transaction completed successfully: ${action_desc}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# validate_directories: Ensure required directories exist and are writable
|
||||
validate_directories() {
|
||||
local directories=("$@")
|
||||
local error_count=0
|
||||
|
||||
for dir in "${directories[@]}"; do
|
||||
# Skip empty directory paths
|
||||
if [[ -z "${dir}" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ ! -d "${dir}" ]]; then
|
||||
log_error "Directory missing: ${dir}"
|
||||
error_count=$((error_count + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ ! -w "${dir}" ]]; then
|
||||
log_warn "Write permission denied for: ${dir}"
|
||||
log_warn "This may cause problems - the script will continue but operations may fail"
|
||||
# Don't increment error_count to allow script to continue
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${error_count} -gt 0 ]]; then
|
||||
log_error "${error_count} required directories are missing"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
295
usr/local/lib/torrent-mover/file_operations.sh
Normal file
295
usr/local/lib/torrent-mover/file_operations.sh
Normal file
@@ -0,0 +1,295 @@
|
||||
#!/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}"
|
||||
}
|
||||
|
||||
# generate_checksums: Common function to generate checksums efficiently
|
||||
generate_checksums() {
|
||||
local dir="$1"
|
||||
local cache_file="${CHECKSUM_DB}.$(echo "$dir" | md5sum | cut -d' ' -f1)"
|
||||
local last_modified_file
|
||||
|
||||
# Skip if directory doesn't exist
|
||||
if [[ ! -d "${dir}" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get the most recently modified file in the directory
|
||||
last_modified_file=$(find "${dir}" -type f ! \( -iname "*.nfo" -o -iname "*.sfv" \) -exec stat -c "%Y %n" {} \; | sort -nr | head -n1 | cut -d' ' -f2-)
|
||||
|
||||
# If cache exists and no files were modified since last cache, use cache
|
||||
if [[ -f "${cache_file}" ]] && [[ -n "${last_modified_file}" ]]; then
|
||||
local cache_time file_time
|
||||
cache_time=$(stat -c "%Y" "${cache_file}")
|
||||
file_time=$(stat -c "%Y" "${last_modified_file}")
|
||||
|
||||
if (( cache_time >= file_time )); then
|
||||
log_debug "Using cached checksums for ${dir}"
|
||||
cat "${cache_file}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Generate new checksums with parallel processing
|
||||
log_debug "Generating fresh checksums for ${dir}"
|
||||
find "${dir}" -type f ! \( -iname "*.nfo" -o -iname "*.sfv" \) -print0 | \
|
||||
parallel -0 -j ${PARALLEL_THREADS:-$(nproc)} md5sum | sort | tee "${cache_file}"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# file_metadata: Returns an md5 hash for file metadata.
|
||||
file_metadata() {
|
||||
generate_checksums "$1" | 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=$(generate_checksums "${src}")
|
||||
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=$(generate_checksums "${target}")
|
||||
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
|
||||
}
|
||||
|
||||
# check_seeding_status: Check if torrent is still seeding
|
||||
check_seeding_status() {
|
||||
local id="$1"
|
||||
local status
|
||||
|
||||
# Get torrent status from transmission
|
||||
status=$(transmission-remote --auth "${TRANSMISSION_USER}:${TRANSMISSION_PASS}" --torrent "${id}" --info | grep "State:" | awk '{print $2}')
|
||||
|
||||
# Return 0 if seeding (meaning it's active), 1 if it's not seeding
|
||||
if [[ "$status" == "Seeding" ]]; then
|
||||
log_info "Torrent ${id} is actively seeding"
|
||||
return 0
|
||||
else
|
||||
log_info "Torrent ${id} is not seeding (status: ${status})"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# safe_move_files: Either move files or create hardlinks depending on seeding status
|
||||
safe_move_files() {
|
||||
local dst="$1" src="$2" id="$3"
|
||||
|
||||
# If torrent is seeding, use hardlinks instead of moving
|
||||
if check_seeding_status "${id}"; then
|
||||
log_info "Using hardlinks for seeding torrent ${id}"
|
||||
if (( PARALLEL_PROCESSING )); then
|
||||
# Using cp with --link to create hardlinks instead of copying
|
||||
retry_command "find \"${src}\" -type f -print0 | parallel -0 -j ${PARALLEL_THREADS:-$(nproc)} cp --link {} \"${dst}/\" 2>/dev/null || cp {} \"${dst}/\"" 3 15
|
||||
# Handle directories separately - we need to create them first
|
||||
retry_command "find \"${src}\" -type d -print0 | parallel -0 -j ${PARALLEL_THREADS:-$(nproc)} mkdir -p \"${dst}/{}\"" 3 15
|
||||
else
|
||||
# Non-parallel hardlink creation
|
||||
retry_command "find \"${src}\" -type f -exec cp --link {} \"${dst}/\" \; 2>/dev/null || cp {} \"${dst}/\"" 3 15
|
||||
retry_command "find \"${src}\" -type d -exec mkdir -p \"${dst}/{}\" \;" 3 15
|
||||
fi
|
||||
else
|
||||
# If not seeding, proceed with normal move operation
|
||||
move_files "${dst}" "${src}"
|
||||
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"
|
||||
local operation_result=0
|
||||
|
||||
# Check if source and destination are the same or if we've already processed this
|
||||
if [[ "${src}" == "${dst}" ]]; then
|
||||
log_info "Source and destination are the same - skipping: ${src}"
|
||||
mark_processed "${hash}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! -d "${src}" ]]; then
|
||||
log_error "Source directory missing: ${src}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Create destination with proper error handling
|
||||
if [[ ! -d "${dst}" ]]; then
|
||||
log_info "Creating destination directory: ${dst}"
|
||||
if ! mkdir -p "${dst}"; then
|
||||
log_error "Failed to create directory: ${dst}"
|
||||
return 1
|
||||
fi
|
||||
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 0
|
||||
fi
|
||||
|
||||
# Extract archives first
|
||||
if ! handle_archives "${src}" "${dst}"; then
|
||||
log_warn "Archive extraction had issues for ${src}, continuing with regular files"
|
||||
fi
|
||||
|
||||
# Process files atomically
|
||||
case "${COPY_MODE}" in
|
||||
move)
|
||||
log_info "Moving files from ${src} to ${dst}"
|
||||
safe_move_files "${dst}" "${src}" "${id}"
|
||||
operation_result=$?
|
||||
;;
|
||||
copy)
|
||||
log_info "Copying files from ${src} to ${dst}"
|
||||
copy_files "${dst}" "${src}"
|
||||
operation_result=$?
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ${operation_result} -eq 0 ]]; then
|
||||
if [[ "${CHECK_TRANSFER_INTEGRITY}" == "true" ]]; then
|
||||
log_info "Verifying integrity of transferred files..."
|
||||
local src_checksum target_checksum
|
||||
src_checksum=$(generate_checksums "${src}")
|
||||
target_checksum=$(generate_checksums "${dst}")
|
||||
|
||||
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
|
||||
}
|
201
usr/local/lib/torrent-mover/transmission_handler.sh
Normal file
201
usr/local/lib/torrent-mover/transmission_handler.sh
Normal file
@@ -0,0 +1,201 @@
|
||||
#!/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"
|
||||
|
||||
# Check if source_path is valid before accessing the array
|
||||
if [[ -z "${source_path}" ]]; then
|
||||
log_warn "Empty source path provided to get_destination"
|
||||
return "${DEFAULT_DST}"
|
||||
fi
|
||||
|
||||
# Check if path is already in the cache
|
||||
if [[ -n "${PATH_CACHE["${source_path}"]+x}" ]]; then
|
||||
local cached_destination="${PATH_CACHE["${source_path}"]}"
|
||||
log_debug "Using cached destination for ${source_path}: ${cached_destination}"
|
||||
echo "${cached_destination}"
|
||||
return
|
||||
fi
|
||||
|
||||
# Skip recursive path analysis - only log once
|
||||
if [[ "${source_path}" =~ ^/mnt/dsnas1/ ]]; then
|
||||
# Already in destination format, return as is
|
||||
log_debug "Path already in destination format: ${source_path}"
|
||||
PATH_CACHE["${source_path}"]="${source_path}"
|
||||
echo "${source_path}"
|
||||
return
|
||||
fi
|
||||
|
||||
# For paths in dsnas2, check if they map to same structure in dsnas1
|
||||
if [[ "${source_path}" =~ ^/mnt/dsnas2/ ]]; then
|
||||
local dir_suffix="${source_path#/mnt/dsnas2/}"
|
||||
local potential_dest="/mnt/dsnas1/${dir_suffix}"
|
||||
|
||||
# If the directories match exactly in structure, only on different mounts,
|
||||
# return the source to avoid needless copying
|
||||
if [[ -d "${potential_dest}" ]]; then
|
||||
log_debug "Path maps to same structure on different mount: ${source_path} -> ${source_path}"
|
||||
PATH_CACHE["${source_path}"]="${source_path}"
|
||||
echo "${source_path}"
|
||||
return
|
||||
fi
|
||||
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}"
|
||||
|
||||
# Only set in cache if source_path is not empty
|
||||
if [[ -n "${source_path}" ]]; then
|
||||
PATH_CACHE["${source_path}"]="${destination}"
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
local cmd="transmission-remote ${TRANSMISSION_IP}:${TRANSMISSION_PORT} -n ${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD} -t ${id} --remove-and-delete"
|
||||
retry_command "$cmd" 3 15
|
||||
}
|
||||
|
||||
# get_torrents: Retrieves a list of torrents from Transmission
|
||||
get_torrents() {
|
||||
# Log connection parameters (redacted password)
|
||||
log_info "Transmission connection parameters:"
|
||||
log_info " IP: ${TRANSMISSION_IP}:${TRANSMISSION_PORT}"
|
||||
log_info " Username: ${TRANSMISSION_USER}"
|
||||
log_info " Password: [redacted]"
|
||||
|
||||
# Try a direct command without using retry_command to get clearer error messages
|
||||
log_info "Direct transmission-remote access test:"
|
||||
local test_output
|
||||
test_output=$(transmission-remote "${TRANSMISSION_IP}:${TRANSMISSION_PORT}" -n "${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}" -l 2>&1)
|
||||
local test_exit=$?
|
||||
if [[ $test_exit -ne 0 ]]; then
|
||||
log_error "Direct transmission-remote test failed with exit code: $test_exit"
|
||||
log_error "Error output: $test_output"
|
||||
# Continue anyway to see retry attempt logs
|
||||
else
|
||||
log_info "Direct transmission-remote test succeeded"
|
||||
fi
|
||||
|
||||
# Execute the actual command with retries
|
||||
local real_cmd="transmission-remote ${TRANSMISSION_IP}:${TRANSMISSION_PORT} -n ${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD} -l"
|
||||
local output
|
||||
output=$(retry_command "$real_cmd" 3 20)
|
||||
|
||||
# Line-by-line raw output inspection (debugging)
|
||||
log_info "Raw command output detailed analysis:"
|
||||
if [[ -z "$output" ]]; then
|
||||
log_error "Command produced EMPTY output"
|
||||
else
|
||||
log_info "Output length: $(echo "$output" | wc -l) lines"
|
||||
echo "$output" | while IFS= read -r line; do
|
||||
log_info " LINE: '$line'"
|
||||
done
|
||||
fi
|
||||
|
||||
# Extract IDs directly using awk with detailed debugging
|
||||
log_info "Extracting torrent IDs from output..."
|
||||
local line_num=0
|
||||
local found_ids=0
|
||||
echo "$output" | while IFS= read -r line; do
|
||||
line_num=$((line_num + 1))
|
||||
# Skip header line
|
||||
if [[ $line_num -eq 1 ]]; then
|
||||
log_info " Skipping header: '$line'"
|
||||
continue
|
||||
fi
|
||||
# Check for torrent ID in first column
|
||||
local potential_id
|
||||
potential_id=$(echo "$line" | awk '{gsub(/^[ ]+/, "", $1); print $1}')
|
||||
log_info " Line $line_num: potential ID '$potential_id'"
|
||||
if [[ "$potential_id" =~ ^[0-9]+$ ]]; then
|
||||
log_info " Found valid ID: $potential_id"
|
||||
found_ids=$((found_ids + 1))
|
||||
echo "$potential_id"
|
||||
else
|
||||
log_info " Not a valid ID: '$potential_id'"
|
||||
fi
|
||||
done | tee /tmp/torrent_ids.txt
|
||||
|
||||
# Read back the file to get around pipe subshell issues
|
||||
local torrent_ids
|
||||
torrent_ids=$(cat /tmp/torrent_ids.txt)
|
||||
rm -f /tmp/torrent_ids.txt
|
||||
|
||||
# Check if we found any torrents
|
||||
if [[ -z "$torrent_ids" ]]; then
|
||||
log_error "NO TORRENT IDs FOUND in transmission output"
|
||||
else
|
||||
log_info "Found torrent IDs: $torrent_ids"
|
||||
fi
|
||||
|
||||
# Fallback to hardcoded ID for testing if nothing found
|
||||
if [[ -z "$torrent_ids" && "${DRY_RUN}" -eq 1 ]]; then
|
||||
log_info "DRY RUN MODE: Adding test torrent ID 1 for debugging"
|
||||
echo "1"
|
||||
else
|
||||
echo "$torrent_ids"
|
||||
fi
|
||||
}
|
||||
|
||||
# get_torrent_info: Gets detailed info for a specific torrent
|
||||
get_torrent_info() {
|
||||
local id="$1"
|
||||
local cmd="transmission-remote ${TRANSMISSION_IP}:${TRANSMISSION_PORT} -n ${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD} -t ${id} -i"
|
||||
retry_command "$cmd" 3 15
|
||||
}
|
Reference in New Issue
Block a user