Compare commits
16 Commits
3287566fb7
...
main
Author | SHA1 | Date | |
---|---|---|---|
91106a244c | |||
1119f38fd6 | |||
e64e1115a7 | |||
bf41b9ad71 | |||
4f7cb91bc5 | |||
fb56817e76 | |||
f572a241ef | |||
![]() |
4c7ebaf5fe | ||
![]() |
d799a2e8bd | ||
![]() |
bb2ebaaa5d | ||
![]() |
c924f096e7 | ||
6c164193b3 | |||
5972dc2e1c | |||
21db2cea6f | |||
ecb39f4fb0 | |||
cbf1de8a91 |
342
README.md
342
README.md
@@ -1,128 +1,286 @@
|
|||||||
Torrent Mover v8.0
|
# Torrent Mover v9.1
|
||||||
Torrent Mover is a Bash script designed to automate the processing of completed torrents in Transmission. It moves or copies downloaded files from a Transmission‑reported download location to designated destination directories on your system. This enhanced version includes robust locking, advanced error handling, parallel processing, configurable path mapping, and improved archive extraction while ensuring file integrity.
|
|
||||||
|
|
||||||
Features
|
## Description
|
||||||
Automatic Torrent Processing:
|
|
||||||
Monitors Transmission for completed torrents and processes them based on configurable seeding criteria.
|
|
||||||
|
|
||||||
Configurable Path Mapping:
|
**Torrent Mover** is a Bash script designed to automate the processing of completed torrents in Transmission.
|
||||||
Uses Transmission’s reported download path (e.g. /downloads) and maps it to your local file system (e.g. /mnt/dsnas2) via configurable settings.
|
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.
|
||||||
|
|
||||||
Robust Locking:
|
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.
|
||||||
Employs flock to ensure that only one instance of the script runs at a time, preventing conflicts.
|
|
||||||
|
|
||||||
Advanced Error Handling & Logging:
|
## Features
|
||||||
A global error handler traps unexpected errors and logs detailed messages. Logs are output to a specified log file (and optionally to syslog) and support DEBUG mode.
|
|
||||||
|
|
||||||
Parallel File Operations:
|
### Core Features
|
||||||
Utilizes GNU Parallel for moving, copying, and generating file checksums, making file operations efficient and multi-threaded.
|
- **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.
|
||||||
|
|
||||||
Archive Extraction with Directory Preservation:
|
### Advanced Content Organization
|
||||||
When an archive (RAR, ZIP, 7z) is encountered in the source, it is extracted into a subdirectory (named after the archive, minus its extension) within the destination. Archives remain in the source until Transmission removes them based on seeding ratio/time limits.
|
- **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.
|
||||||
|
|
||||||
Directory Deduplication:
|
### Enhanced Security & Reliability
|
||||||
Prevents re‑processing the same source directory if multiple torrents reference it, ensuring that files aren’t processed repeatedly.
|
- **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.
|
||||||
|
|
||||||
Optional Integrity Verification:
|
### Performance & Engineering
|
||||||
If enabled, the script re‑calculates and compares file checksums after file transfer to verify integrity.
|
- **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
|
## Requirements
|
||||||
Bash
|
|
||||||
Transmission-remote (for interfacing with Transmission)
|
|
||||||
GNU Parallel
|
|
||||||
unrar, unzip, 7z (for archive extraction)
|
|
||||||
bc (for numerical comparisons)
|
|
||||||
Installation
|
|
||||||
Download the Script:
|
|
||||||
Save the script (e.g., torrent-mover.sh) to your desired location (e.g., /usr/local/bin/).
|
|
||||||
|
|
||||||
Make It Executable:
|
- Bash
|
||||||
|
- transmission-remote
|
||||||
|
- GNU Parallel
|
||||||
|
- unrar, unzip, 7z
|
||||||
|
- bc
|
||||||
|
|
||||||
chmod +x /usr/local/bin/torrent-mover.sh
|
## Installation
|
||||||
Create/Edit the Configuration File:
|
|
||||||
The script expects a configuration file at /etc/torrent/mover.conf. See the Configuration section for details.
|
|
||||||
|
|
||||||
Configuration
|
1. Run the installation script as root:
|
||||||
Create or modify the configuration file /etc/torrent/mover.conf with variables similar to the following:
|
```
|
||||||
|
sudo ./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
# Transmission settings
|
2. The script will:
|
||||||
TRANSMISSION_IP="192.168.1.100" # Replace with your Transmission server's IP
|
- Install all necessary dependencies
|
||||||
TRANSMISSION_PORT="9091" # Replace with your Transmission server's port
|
- Create a dedicated non-root user for security
|
||||||
TRANSMISSION_USER="your_username" # Transmission username (if set)
|
- Set up the configuration file in `/etc/torrent/mover.conf`
|
||||||
TRANSMISSION_PASSWORD="your_password" # Transmission password (if set)
|
- Install systemd service and timer
|
||||||
|
- Configure file permissions and log rotation
|
||||||
|
|
||||||
# Path mapping settings
|
3. Enable the service to run every 15 minutes:
|
||||||
TRANSMISSION_PATH_PREFIX="/downloads"
|
```
|
||||||
LOCAL_PATH_PREFIX="/mnt/dsnas2"
|
sudo systemctl enable --now torrent-mover.timer
|
||||||
|
```
|
||||||
|
|
||||||
# Destination directories
|
## Configuration
|
||||||
DIR_GAMES_DST="/mnt/dsnas1/Games"
|
|
||||||
DIR_APPS_DST="/mnt/dsnas1/Apps"
|
|
||||||
DIR_MOVIES_DST="/mnt/dsnas1/Movies"
|
|
||||||
DIR_BOOKS_DST="/mnt/dsnas1/Books"
|
|
||||||
DEFAULT_DST="/mnt/dsnas1/Other"
|
|
||||||
|
|
||||||
# Additional storage directories (comma-separated list)
|
Edit the configuration file at `/etc/torrent/mover.conf` to customize the behavior of Torrent Mover:
|
||||||
STORAGE_DIRS="/mnt/dsnas/Movies"
|
|
||||||
|
|
||||||
# Performance settings
|
### Connection Configuration
|
||||||
PARALLEL_THREADS="32"
|
```bash
|
||||||
PARALLEL_PROCESSING=1
|
# 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)
|
||||||
|
|
||||||
# Operation mode: "move" or "copy"
|
# Path mapping configuration
|
||||||
COPY_MODE="copy"
|
TRANSMISSION_PATH_PREFIX="/downloads" # Path prefix reported by Transmission
|
||||||
|
LOCAL_PATH_PREFIX="/mnt/dsnas2" # Corresponding local path prefix
|
||||||
|
```
|
||||||
|
|
||||||
|
### Content Organization
|
||||||
|
```bash
|
||||||
|
# Primary content destination directories
|
||||||
|
DIR_GAMES_DST="/mnt/dsnas1/Games" # Games destination
|
||||||
|
DIR_APPS_DST="/mnt/dsnas1/Apps" # Applications destination
|
||||||
|
DIR_MOVIES_DST="/mnt/dsnas1/Movies" # Movies destination
|
||||||
|
DIR_BOOKS_DST="/mnt/dsnas1/Books" # Books/eBooks destination
|
||||||
|
DIR_TV_DST="/mnt/dsnas1/TV" # TV series destination
|
||||||
|
DIR_MUSIC_DST="/mnt/dsnas1/Music" # Music destination
|
||||||
|
DEFAULT_DST="/mnt/dsnas1/Other" # Default for unrecognized content
|
||||||
|
|
||||||
|
# Additional storage libraries (comma-separated)
|
||||||
|
STORAGE_DIRS="/mnt/dsnas/Movies,/mnt/external/Movies" # Additional movie libraries
|
||||||
|
STORAGE_TV_DIRS="/mnt/dsnas/TV,/mnt/external/TV" # Additional TV libraries
|
||||||
|
|
||||||
|
# Custom pattern matching for advanced categorization
|
||||||
|
# Format: "regex_pattern=destination_path;another_pattern=another_path"
|
||||||
|
CUSTOM_PATTERNS=".*documentary.*=${DIR_MOVIES_DST}/Documentary;
|
||||||
|
.*anime.*=${DIR_TV_DST}/Anime;
|
||||||
|
.*linux.*=${DIR_APPS_DST}/Linux;
|
||||||
|
.*tutorial.*=${DIR_BOOKS_DST}/Tutorials"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security & Performance
|
||||||
|
```bash
|
||||||
|
# Security settings - dedicated non-root user
|
||||||
|
TORRENT_USER="torrent-mover" # Dedicated service user
|
||||||
|
TORRENT_GROUP="torrent-mover" # User's primary group
|
||||||
|
|
||||||
|
# Error recovery configuration
|
||||||
|
MAX_RETRY_ATTEMPTS="3" # Maximum retry attempts for failed operations
|
||||||
|
RETRY_WAIT_TIME="15" # Seconds to wait between retry attempts
|
||||||
|
|
||||||
|
# Performance tuning
|
||||||
|
PARALLEL_THREADS="32" # Number of parallel threads (match CPU cores)
|
||||||
|
PARALLEL_PROCESSING=1 # Enable (1) or disable (0) parallel processing
|
||||||
|
|
||||||
|
# Operation mode
|
||||||
|
COPY_MODE="copy" # "copy" to preserve or "move" to relocate files
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logging & Integrity
|
||||||
|
```bash
|
||||||
# File tracking & integrity
|
# File tracking & integrity
|
||||||
PROCESSED_LOG="/var/log/torrent_processed.log"
|
PROCESSED_LOG="/var/log/torrent_processed.log" # Tracks processed torrents
|
||||||
CHECKSUM_DB="/var/lib/torrent/checksums.db"
|
CHECKSUM_DB="/var/lib/torrent/checksums.db" # Stores file checksums
|
||||||
|
|
||||||
# Logging settings
|
# Logging configuration
|
||||||
LOG_FILE="/var/log/torrent_mover.log"
|
LOG_FILE="/var/log/torrent_mover.log" # Main log file location
|
||||||
LOG_LEVEL="INFO" # Change to "DEBUG" for more verbose logging
|
LOG_LEVEL="INFO" # Logging level: "INFO" or "DEBUG"
|
||||||
USE_SYSLOG="false" # Set to "true" to log to syslog as well
|
USE_SYSLOG="false" # Also log to system syslog: "true" or "false"
|
||||||
|
|
||||||
# Optional integrity verification after file transfer ("true" to enable)
|
# Data integrity protection
|
||||||
CHECK_TRANSFER_INTEGRITY="true"
|
CHECK_TRANSFER_INTEGRITY="true" # Verify file integrity after transfers
|
||||||
Usage
|
```
|
||||||
Run the script with various command-line options:
|
|
||||||
|
|
||||||
Dry-run mode (simulate operations):
|
## Usage
|
||||||
|
|
||||||
/usr/local/bin/torrent-mover.sh --dry-run
|
### Main Torrent Mover Script
|
||||||
Interactive mode (prompt for confirmation):
|
|
||||||
|
|
||||||
/usr/local/bin/torrent-mover.sh --interactive
|
Run the main script using the following options:
|
||||||
Cache warmup mode (pre-calculate checksums):
|
|
||||||
|
|
||||||
/usr/local/bin/torrent-mover.sh --cache-warmup
|
- **Dry-run mode (simulate operations):**
|
||||||
Debug mode (verbose logging):
|
```
|
||||||
|
/usr/local/bin/torrent-mover --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
/usr/local/bin/torrent-mover.sh --debug
|
- **Interactive mode (prompt for confirmation):**
|
||||||
You can combine these options as needed. For example:
|
```
|
||||||
|
/usr/local/bin/torrent-mover --interactive
|
||||||
|
```
|
||||||
|
|
||||||
/usr/local/bin/torrent-mover.sh --dry-run --debug
|
- **Cache warmup mode (pre-calculate checksums):**
|
||||||
How It Works
|
```
|
||||||
Locking:
|
/usr/local/bin/torrent-mover --cache-warmup
|
||||||
The script uses flock on a lock file to ensure that only one instance runs at a time.
|
```
|
||||||
|
|
||||||
Path Translation:
|
- **Debug mode (verbose logging):**
|
||||||
The Transmission-reported download path is translated into the actual local path using the mapping provided in the configuration.
|
```
|
||||||
|
/usr/local/bin/torrent-mover --debug
|
||||||
|
```
|
||||||
|
|
||||||
Torrent Processing:
|
You can combine options as needed. For example:
|
||||||
The script uses transmission-remote to list and retrieve torrent information. It processes torrents that are 100% complete (based on percent done) and checks if they’ve already been processed.
|
```
|
||||||
|
/usr/local/bin/torrent-mover --dry-run --debug
|
||||||
|
```
|
||||||
|
|
||||||
File Verification & Deduplication:
|
### Helper Scripts
|
||||||
Before copying files, the script compares file checksums between the source and destination. It skips processing if an exact match is found and avoids re‑processing directories already handled for previous torrents.
|
|
||||||
|
|
||||||
Archive Extraction:
|
The system includes additional helper scripts for more advanced usage:
|
||||||
Archives in the source are extracted into dedicated subdirectories at the destination. The original archive file is kept until Transmission’s seeding criteria are met (and Transmission subsequently removes the torrent).
|
|
||||||
|
|
||||||
Seeding Criteria:
|
- **Torrent Processor:**
|
||||||
The script checks seeding ratio and seeding time values. When the criteria are met, it instructs Transmission (via transmission-remote) to remove the torrent.
|
```
|
||||||
|
/usr/local/bin/torrent-processor [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
Integrity Check (Optional):
|
Available options:
|
||||||
If enabled, the script verifies file integrity by comparing md5 checksums of the source and destination files after transfer.
|
- `--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
|
||||||
|
|
||||||
License
|
Examples:
|
||||||
This script is provided as-is. Use at your own risk. Contributions and improvements are welcome!
|
```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_APPS_DST="/mnt/dsnas1/Apps"
|
||||||
DIR_MOVIES_DST="/mnt/dsnas1/Movies"
|
DIR_MOVIES_DST="/mnt/dsnas1/Movies"
|
||||||
DIR_BOOKS_DST="/mnt/dsnas1/Books"
|
DIR_BOOKS_DST="/mnt/dsnas1/Books"
|
||||||
|
DIR_TV_DST="/mnt/dsnas1/TV"
|
||||||
|
DIR_MUSIC_DST="/mnt/dsnas1/Music"
|
||||||
DEFAULT_DST="/mnt/dsnas1/Other"
|
DEFAULT_DST="/mnt/dsnas1/Other"
|
||||||
|
|
||||||
# Storage directories (comma-separated)
|
# Storage directories (comma-separated)
|
||||||
STORAGE_DIRS="/mnt/dsnas/Movies"
|
STORAGE_DIRS="/mnt/dsnas/Movies"
|
||||||
|
STORAGE_TV_DIRS="/mnt/dsnas/TV"
|
||||||
|
|
||||||
# Path mapping
|
# Path mapping
|
||||||
|
# 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"
|
TRANSMISSION_PATH_PREFIX="/downloads"
|
||||||
LOCAL_PATH_PREFIX="/mnt/dsnas2"
|
LOCAL_PATH_PREFIX="/mnt/dsnas2"
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
# Default user/group for torrent operations (usually debian-transmission)
|
||||||
|
TORRENT_USER="debian-transmission"
|
||||||
|
TORRENT_GROUP="debian-transmission"
|
||||||
|
|
||||||
|
# Custom pattern matching for content categorization
|
||||||
|
# Format: "pattern1=destination1;pattern2=destination2"
|
||||||
|
# Example: ".*\.linux.*=${DIR_LINUX_DST};.*documentary.*=${DIR_DOCUMENTARY_DST}"
|
||||||
|
CUSTOM_PATTERNS=".*documentary.*=${DIR_MOVIES_DST}/Documentary;.*anime.*=${DIR_TV_DST}/Anime;.*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
|
# Performance settings
|
||||||
PARALLEL_THREADS="32" # Match CPU core count
|
PARALLEL_THREADS="32" # Match CPU core count
|
||||||
PARALLEL_PROCESSING=1
|
PARALLEL_PROCESSING=1
|
||||||
@@ -43,7 +64,9 @@ CHECK_TRANSFER_INTEGRITY="true"
|
|||||||
# Optionally, set USE_SYSLOG="true" to also log messages to syslog.
|
# Optionally, set USE_SYSLOG="true" to also log messages to syslog.
|
||||||
USE_SYSLOG="false"
|
USE_SYSLOG="false"
|
||||||
|
|
||||||
# Auto-create directories
|
# Auto-create directories - commented out from config file
|
||||||
mkdir -p "${DIR_GAMES_DST}" "${DIR_APPS_DST}" \
|
# These should be created in a script, not in the config file
|
||||||
"${DIR_MOVIES_DST}" "${DIR_BOOKS_DST}" \
|
# mkdir -p "${DIR_GAMES_DST}" "${DIR_APPS_DST}" \
|
||||||
"${DEFAULT_DST}" 2>/dev/null || true
|
# "${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 repository configuration
|
||||||
GIT_REPO="http://192.168.0.236:3000/masterdraco/torrent"
|
GIT_REPO="http://192.168.0.236:3000/masterdraco/torrent"
|
||||||
|
INSTALL_DIR="/tmp/torrent-install"
|
||||||
|
|
||||||
# Check root privileges
|
# Check root privileges
|
||||||
if [ "$EUID" -ne 0 ]; then
|
if [ "$EUID" -ne 0 ]; then
|
||||||
@@ -14,12 +15,13 @@ fi
|
|||||||
echo "Checking dependencies..."
|
echo "Checking dependencies..."
|
||||||
declare -A PKGS=(
|
declare -A PKGS=(
|
||||||
[transmission-cli]="transmission-remote"
|
[transmission-cli]="transmission-remote"
|
||||||
[unrar]="unrar"
|
[unrar-free]="unrar-free"
|
||||||
[unzip]="unzip"
|
[unzip]="unzip"
|
||||||
[p7zip-full]="7z"
|
[p7zip-full]="7z"
|
||||||
[parallel]="parallel"
|
[parallel]="parallel"
|
||||||
[bc]="bc"
|
[bc]="bc"
|
||||||
[git]="git"
|
[git]="git"
|
||||||
|
[logrotate]="logrotate"
|
||||||
)
|
)
|
||||||
|
|
||||||
for pkg in "${!PKGS[@]}"; do
|
for pkg in "${!PKGS[@]}"; do
|
||||||
@@ -31,22 +33,155 @@ for pkg in "${!PKGS[@]}"; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
# Get files from Repo
|
# Get files from Repo
|
||||||
git pull http://192.168.0.236:3000/masterdraco/torrent.git
|
echo "Getting latest files from repository..."
|
||||||
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
|
cd "$INSTALL_DIR"
|
||||||
|
git fetch
|
||||||
|
git reset --hard origin/main
|
||||||
|
else
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
git clone "$GIT_REPO" "$INSTALL_DIR"
|
||||||
|
cd "$INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
# Create directory structure
|
# Create directory structure
|
||||||
echo "Creating directory structure..."
|
echo "Creating directory structure..."
|
||||||
mkdir -p /etc/torrent
|
mkdir -p /etc/torrent
|
||||||
mkdir -p /usr/local/bin
|
mkdir -p /usr/local/bin
|
||||||
|
mkdir -p /usr/local/lib/torrent-mover
|
||||||
|
mkdir -p /var/lib/torrent
|
||||||
|
mkdir -p /var/log/torrent
|
||||||
|
mkdir -p /etc/systemd/system
|
||||||
|
|
||||||
|
# Create dedicated user for security
|
||||||
|
TORRENT_USER="torrent-mover"
|
||||||
|
TORRENT_GROUP="torrent-mover"
|
||||||
|
|
||||||
|
# Check if user exists and create if not
|
||||||
|
if ! id "$TORRENT_USER" &>/dev/null; then
|
||||||
|
echo "Creating dedicated $TORRENT_USER user for security..."
|
||||||
|
useradd -r -s /bin/false "$TORRENT_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
# Install files
|
# Install files
|
||||||
echo "Installing files..."
|
echo "Installing files..."
|
||||||
cp -v etc/torrent/mover.conf /etc/torrent/
|
install -Dm644 etc/torrent/mover.conf /etc/torrent/mover.conf.new
|
||||||
cp -v usr/local/bin/torrent-mover /usr/local/bin/
|
install -Dm755 usr/local/bin/torrent-mover /usr/local/bin/torrent-mover
|
||||||
chmod +x /usr/local/bin/torrent-mover
|
install -Dm755 usr/local/bin/torrent-config /usr/local/bin/torrent-config
|
||||||
|
|
||||||
|
# Install library modules
|
||||||
|
for module in usr/local/lib/torrent-mover/*.sh; do
|
||||||
|
if [ -f "$module" ]; then
|
||||||
|
install -Dm755 "$module" "/usr/local/lib/torrent-mover/$(basename "$module")"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Create backup directory for configuration files
|
||||||
|
mkdir -p /etc/torrent/backups
|
||||||
|
chown $TORRENT_USER:$TORRENT_GROUP /etc/torrent/backups
|
||||||
|
|
||||||
|
# If this is a first-time install, copy the default config
|
||||||
|
if [ ! -f "/etc/torrent/mover.conf" ]; then
|
||||||
|
mv /etc/torrent/mover.conf.new /etc/torrent/mover.conf
|
||||||
|
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
|
# Set permissions
|
||||||
echo "Setting permissions..."
|
echo "Setting permissions..."
|
||||||
chmod 600 /etc/torrent/mover.conf
|
chmod 600 /etc/torrent/mover.conf*
|
||||||
chown root:root /etc/torrent/mover.conf
|
chown root:root /etc/torrent/mover.conf*
|
||||||
|
chmod 644 /etc/systemd/system/torrent-mover.service
|
||||||
|
chmod 644 /etc/systemd/system/torrent-mover.timer
|
||||||
|
|
||||||
|
# Set permissions for data directories
|
||||||
|
chown $TORRENT_USER:$TORRENT_GROUP /var/lib/torrent
|
||||||
|
chmod 755 /var/lib/torrent
|
||||||
|
touch /var/log/torrent_mover.log /var/log/torrent_processed.log
|
||||||
|
chown $TORRENT_USER:$TORRENT_GROUP /var/log/torrent_mover.log /var/log/torrent_processed.log
|
||||||
|
chmod 640 /var/log/torrent_mover.log /var/log/torrent_processed.log
|
||||||
|
|
||||||
|
# Ensure torrent-mover user can access required directories
|
||||||
|
echo "Setting up group memberships..."
|
||||||
|
if getent group debian-transmission >/dev/null; then
|
||||||
|
usermod -a -G debian-transmission $TORRENT_USER
|
||||||
|
echo "Added $TORRENT_USER to debian-transmission group"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reload systemd and enable timer
|
||||||
|
systemctl daemon-reload
|
||||||
|
echo "To enable automatic execution every 15 minutes, run:"
|
||||||
|
echo " systemctl enable --now torrent-mover.timer"
|
||||||
|
echo
|
||||||
|
echo "Installation complete!"
|
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
|
#!/bin/bash
|
||||||
# Torrent Mover v7.2 - Enhanced & Robust Version with Directory Deduplication,
|
# Torrent Mover v8.0 - Enhanced & Robust Version with modular architecture,
|
||||||
# Improved Archive Handling (keeping archives until ratio limits are reached)
|
# improved error handling, security, and content categorization
|
||||||
#
|
#
|
||||||
# This script processes completed torrents reported by Transmission,
|
# This script processes completed torrents reported by Transmission,
|
||||||
# moving or copying files to designated destination directories.
|
# moving or copying files to designated destination directories.
|
||||||
# It includes robust locking, advanced error handling & notifications,
|
# It includes robust locking, advanced error handling & notifications,
|
||||||
# improved logging, optional post-transfer integrity checks, configurable path mapping,
|
# improved logging, optional post-transfer integrity checks, configurable path mapping,
|
||||||
# and improved archive extraction that preserves directory structure.
|
# and improved archive extraction that preserves directory structure.
|
||||||
#
|
|
||||||
# Future improvements might include using 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 #
|
# Robust Locking with flock #
|
||||||
@@ -17,52 +19,6 @@ LOCK_FILE="/var/lock/torrent-mover.lock"
|
|||||||
exec 200>"${LOCK_FILE}" || { echo "Cannot open lock file" >&2; exit 1; }
|
exec 200>"${LOCK_FILE}" || { echo "Cannot open lock file" >&2; exit 1; }
|
||||||
flock -n 200 || { echo "Another instance is running." >&2; exit 1; }
|
flock -n 200 || { echo "Another instance is running." >&2; exit 1; }
|
||||||
|
|
||||||
##############################
|
|
||||||
# Global Runtime Variables #
|
|
||||||
##############################
|
|
||||||
DRY_RUN=0
|
|
||||||
INTERACTIVE=0
|
|
||||||
CACHE_WARMUP=0
|
|
||||||
DEBUG=0 # Set to 1 if LOG_LEVEL is DEBUG or --debug is passed
|
|
||||||
|
|
||||||
# To avoid reprocessing the same source directory (across different torrents)
|
|
||||||
declare -A processed_source_dirs
|
|
||||||
|
|
||||||
####################
|
|
||||||
# Logging Functions#
|
|
||||||
####################
|
|
||||||
# All log messages go to stderr.
|
|
||||||
log_debug() {
|
|
||||||
if [[ "${DEBUG}" -eq 1 ]]; then
|
|
||||||
echo -e "[DEBUG] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2
|
|
||||||
[[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[DEBUG] $*"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
log_info() {
|
|
||||||
echo -e "[INFO] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2
|
|
||||||
[[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[INFO] $*"
|
|
||||||
}
|
|
||||||
log_warn() {
|
|
||||||
echo -e "[WARN] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2
|
|
||||||
[[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[WARN] $*"
|
|
||||||
}
|
|
||||||
log_error() {
|
|
||||||
echo -e "[ERROR] $(date '+%F %T') - $*" | tee -a "${LOG_FILE}" >&2
|
|
||||||
[[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[ERROR] $*"
|
|
||||||
}
|
|
||||||
|
|
||||||
#################################
|
|
||||||
# Error Handling & Notifications#
|
|
||||||
#################################
|
|
||||||
error_handler() {
|
|
||||||
local lineno="$1"
|
|
||||||
local msg="$2"
|
|
||||||
log_error "Error on line ${lineno}: ${msg}"
|
|
||||||
# Optionally send a notification (e.g., email)
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
trap 'error_handler ${LINENO} "$BASH_COMMAND"' ERR
|
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# Configuration & Validation #
|
# Configuration & Validation #
|
||||||
##############################
|
##############################
|
||||||
@@ -79,6 +35,20 @@ if [[ -z "${TRANSMISSION_PATH_PREFIX:-}" || -z "${LOCAL_PATH_PREFIX:-}" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Load modules
|
||||||
|
for module in "${LIB_DIR}"/*.sh; do
|
||||||
|
if [[ -f "$module" ]]; then
|
||||||
|
source "$module"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Set defaults for new configuration options
|
||||||
|
TORRENT_USER="${TORRENT_USER:-debian-transmission}"
|
||||||
|
TORRENT_GROUP="${TORRENT_GROUP:-debian-transmission}"
|
||||||
|
MAX_RETRY_ATTEMPTS="${MAX_RETRY_ATTEMPTS:-3}"
|
||||||
|
RETRY_WAIT_TIME="${RETRY_WAIT_TIME:-15}"
|
||||||
|
|
||||||
|
# Enable DEBUG mode if set in config
|
||||||
if [[ "${LOG_LEVEL}" == "DEBUG" ]]; then
|
if [[ "${LOG_LEVEL}" == "DEBUG" ]]; then
|
||||||
DEBUG=1
|
DEBUG=1
|
||||||
fi
|
fi
|
||||||
@@ -89,313 +59,10 @@ if [[ -n "${STORAGE_DIRS}" ]]; then
|
|||||||
IFS=',' read -ra STORAGE_DIRS_ARRAY <<< "${STORAGE_DIRS}"
|
IFS=',' read -ra STORAGE_DIRS_ARRAY <<< "${STORAGE_DIRS}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
##############################
|
#################################
|
||||||
# Helper & Utility Functions #
|
# Error Handling & Notifications#
|
||||||
##############################
|
#################################
|
||||||
|
trap 'error_handler ${LINENO} "$BASH_COMMAND"' ERR
|
||||||
# translate_source: Converts the 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
|
|
||||||
}
|
|
||||||
|
|
||||||
#################
|
#################
|
||||||
# Main Function #
|
# Main Function #
|
||||||
@@ -411,17 +78,32 @@ main() {
|
|||||||
"${DIR_BOOKS_DST}"
|
"${DIR_BOOKS_DST}"
|
||||||
"${DEFAULT_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
|
for dir in "${REQUIRED_DIRS[@]}"; do
|
||||||
if [[ ! -d "${dir}" ]]; then
|
if [[ -n "$dir" ]]; then
|
||||||
log_error "Directory missing: ${dir}"
|
if [[ ! -d "$dir" ]]; then
|
||||||
exit 1
|
log_info "Creating directory: $dir"
|
||||||
fi
|
if mkdir -p "$dir"; then
|
||||||
if [[ ! -w "${dir}" ]]; then
|
# Try to set permissions but don't fail if it doesn't work
|
||||||
log_error "Write permission denied: ${dir}"
|
chmod 775 "$dir" 2>/dev/null || log_warn "Could not set permissions on $dir"
|
||||||
exit 1
|
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
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Now validate that all required directories exist and are writable
|
||||||
|
validate_directories "${REQUIRED_DIRS[@]}" || exit 1
|
||||||
|
|
||||||
init_checksum_db
|
init_checksum_db
|
||||||
|
|
||||||
if (( CACHE_WARMUP )); then
|
if (( CACHE_WARMUP )); then
|
||||||
@@ -429,14 +111,36 @@ main() {
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_info "Starting processing"
|
log_info "Starting processing with user: ${TORRENT_USER}"
|
||||||
declare -A warned_dirs=()
|
declare -A warned_dirs=()
|
||||||
transmission-remote "${TRANSMISSION_IP}:${TRANSMISSION_PORT}" \
|
|
||||||
-n "${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}" -l | awk 'NR>1 && $1 ~ /^[0-9]+$/ {print $1}' | while read -r id; do
|
|
||||||
|
|
||||||
|
# Get list of torrents from Transmission
|
||||||
|
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
|
local info
|
||||||
info=$(transmission-remote "${TRANSMISSION_IP}:${TRANSMISSION_PORT}" \
|
info=$(get_torrent_info "${id}")
|
||||||
-n "${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}" -t "${id}" -i)
|
|
||||||
local hash
|
local hash
|
||||||
hash=$(grep "Hash:" <<< "${info}" | awk '{print $2}')
|
hash=$(grep "Hash:" <<< "${info}" | awk '{print $2}')
|
||||||
local ratio
|
local ratio
|
||||||
@@ -452,26 +156,87 @@ main() {
|
|||||||
# Extract Transmission-reported directory and translate to local path.
|
# Extract Transmission-reported directory and translate to local path.
|
||||||
local reported_dir
|
local reported_dir
|
||||||
reported_dir=$(grep -i "Location:" <<< "${info}" | awk -F": " '{print $2}' | xargs)
|
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
|
local dir
|
||||||
dir=$(translate_source "${reported_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
|
local dst
|
||||||
dst=$(get_destination "${dir}")
|
dst=$(get_destination "${dir}")
|
||||||
[[ -z "${warned_dirs["${dir}"]+x}" ]] && warned_dirs["${dir}"]=0
|
|
||||||
|
# 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.
|
# Avoid processing the same directory more than once.
|
||||||
if [[ -n "${processed_source_dirs["${dir}"]+x}" ]]; then
|
if [[ -n "${processed_source_dirs["${dir}"]+x}" ]]; then
|
||||||
log_info "Directory ${dir} has already been processed; skipping copy for torrent ${id}"
|
log_info "Directory ${dir} has already been processed; skipping copy for torrent ${id}"
|
||||||
elif (( $(bc <<< "${percent_done} >= 100") )) && ! is_processed "${hash}"; then
|
elif (( $(bc <<< "${percent_done} >= 100") )) && ! is_processed "${hash}"; then
|
||||||
log_info "Processing completed torrent ${id} (${percent_done}% done)"
|
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}"
|
log_warn "Using default destination for: ${dir}"
|
||||||
warned_dirs["${dir}"]=1
|
warned_dirs["${dir}"]=1
|
||||||
fi
|
fi
|
||||||
local targets=("${dst}")
|
local targets=("${dst}")
|
||||||
case "${dst}" in
|
case "${dst}" in
|
||||||
"${DIR_MOVIES_DST}") targets+=("${STORAGE_DIRS_ARRAY[@]}");;
|
"${DIR_MOVIES_DST}")
|
||||||
|
targets+=("${STORAGE_DIRS_ARRAY[@]}")
|
||||||
|
;;
|
||||||
|
"${DIR_TV_DST}")
|
||||||
|
# If there are TV storage dirs, include them
|
||||||
|
[[ -n "${STORAGE_TV_DIRS}" ]] && IFS=',' read -ra TV_DIRS <<< "${STORAGE_TV_DIRS}" && targets+=("${TV_DIRS[@]}")
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if ! files_need_processing "${dir}" "${targets[@]}"; then
|
if ! files_need_processing "${dir}" "${targets[@]}"; then
|
||||||
log_info "Skipping copy - files already exist in:"
|
log_info "Skipping copy - files already exist in:"
|
||||||
for target in "${targets[@]}"; do
|
for target in "${targets[@]}"; do
|
||||||
@@ -489,11 +254,21 @@ main() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
check_disk_usage "${DIR_GAMES_DST}"
|
# Print count of processed directories
|
||||||
check_disk_usage "${DIR_APPS_DST}"
|
if [[ "${DEBUG}" -eq 1 ]]; then
|
||||||
check_disk_usage "${DIR_MOVIES_DST}"
|
log_debug "Processed source directories count: ${#processed_source_dirs[@]}"
|
||||||
check_disk_usage "${DIR_BOOKS_DST}"
|
for dir in "${!processed_source_dirs[@]}"; do
|
||||||
check_disk_usage "${DEFAULT_DST}"
|
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