- Fix quote handling in transmission-remote commands - Add robust handling for empty torrent IDs - Improve path handling for empty directories - Update version to 9.1 with shared directory handling - Fix empty array subscript errors On branch main Your branch is up to date with 'origin/main'. Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: README.md modified: etc/torrent/mover.conf modified: install.sh new file: usr/local/bin/smart-processor modified: usr/local/bin/torrent-mover new file: usr/local/bin/torrent-processor modified: usr/local/lib/torrent-mover/common.sh modified: usr/local/lib/torrent-mover/transmission_handler.sh
255 lines
9.2 KiB
Bash
Executable File
255 lines
9.2 KiB
Bash
Executable File
#!/bin/bash
|
|
# Torrent Mover v8.0 - Enhanced & Robust Version with modular architecture,
|
|
# improved error handling, security, and content categorization
|
|
#
|
|
# This script processes completed torrents reported by Transmission,
|
|
# moving or copying files to designated destination directories.
|
|
# It includes robust locking, advanced error handling & notifications,
|
|
# improved logging, optional post-transfer integrity checks, configurable path mapping,
|
|
# and improved archive extraction that preserves directory structure.
|
|
|
|
# Set script location for importing modules
|
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
|
LIB_DIR="/usr/local/lib/torrent-mover"
|
|
|
|
##############################
|
|
# Robust Locking with flock #
|
|
##############################
|
|
LOCK_FILE="/var/lock/torrent-mover.lock"
|
|
exec 200>"${LOCK_FILE}" || { echo "Cannot open lock file" >&2; exit 1; }
|
|
flock -n 200 || { echo "Another instance is running." >&2; exit 1; }
|
|
|
|
##############################
|
|
# Configuration & Validation #
|
|
##############################
|
|
CONFIG_FILE="/etc/torrent/mover.conf"
|
|
if [[ ! -f "${CONFIG_FILE}" ]]; then
|
|
echo "FATAL: Configuration file missing: ${CONFIG_FILE}" >&2
|
|
exit 1
|
|
fi
|
|
source "${CONFIG_FILE}"
|
|
|
|
# Validate required configuration values.
|
|
if [[ -z "${TRANSMISSION_PATH_PREFIX:-}" || -z "${LOCAL_PATH_PREFIX:-}" ]]; then
|
|
echo "FATAL: TRANSMISSION_PATH_PREFIX and LOCAL_PATH_PREFIX must be set in ${CONFIG_FILE}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Load modules
|
|
for module in "${LIB_DIR}"/*.sh; do
|
|
if [[ -f "$module" ]]; then
|
|
source "$module"
|
|
fi
|
|
done
|
|
|
|
# Set defaults for new configuration options
|
|
TORRENT_USER="${TORRENT_USER:-debian-transmission}"
|
|
TORRENT_GROUP="${TORRENT_GROUP:-debian-transmission}"
|
|
MAX_RETRY_ATTEMPTS="${MAX_RETRY_ATTEMPTS:-3}"
|
|
RETRY_WAIT_TIME="${RETRY_WAIT_TIME:-15}"
|
|
|
|
# Enable DEBUG mode if set in config
|
|
if [[ "${LOG_LEVEL}" == "DEBUG" ]]; then
|
|
DEBUG=1
|
|
fi
|
|
|
|
# Parse STORAGE_DIRS into an array.
|
|
STORAGE_DIRS_ARRAY=()
|
|
if [[ -n "${STORAGE_DIRS}" ]]; then
|
|
IFS=',' read -ra STORAGE_DIRS_ARRAY <<< "${STORAGE_DIRS}"
|
|
fi
|
|
|
|
#################################
|
|
# Error Handling & Notifications#
|
|
#################################
|
|
trap 'error_handler ${LINENO} "$BASH_COMMAND"' ERR
|
|
|
|
#################
|
|
# Main Function #
|
|
#################
|
|
main() {
|
|
check_dependencies
|
|
|
|
# Validate destination directories.
|
|
declare -a REQUIRED_DIRS=(
|
|
"${DIR_GAMES_DST}"
|
|
"${DIR_APPS_DST}"
|
|
"${DIR_MOVIES_DST}"
|
|
"${DIR_BOOKS_DST}"
|
|
"${DEFAULT_DST}"
|
|
)
|
|
|
|
# Add optional directories if defined
|
|
[[ -n "${DIR_TV_DST}" ]] && REQUIRED_DIRS+=("${DIR_TV_DST}")
|
|
[[ -n "${DIR_MUSIC_DST}" ]] && REQUIRED_DIRS+=("${DIR_MUSIC_DST}")
|
|
|
|
# Create required directories if they don't exist
|
|
log_info "Creating required directories if they don't exist..."
|
|
for dir in "${REQUIRED_DIRS[@]}"; do
|
|
if [[ -n "$dir" ]]; then
|
|
if [[ ! -d "$dir" ]]; then
|
|
log_info "Creating directory: $dir"
|
|
if mkdir -p "$dir"; then
|
|
# Try to set permissions but don't fail if it doesn't work
|
|
chmod 775 "$dir" 2>/dev/null || log_warn "Could not set permissions on $dir"
|
|
chown ${TORRENT_USER:-debian-transmission}:${TORRENT_GROUP:-debian-transmission} "$dir" 2>/dev/null || log_warn "Could not set ownership on $dir"
|
|
log_info "Created directory: $dir"
|
|
else
|
|
log_error "Failed to create directory: $dir"
|
|
fi
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Now validate that all required directories exist and are writable
|
|
validate_directories "${REQUIRED_DIRS[@]}" || exit 1
|
|
|
|
init_checksum_db
|
|
|
|
if (( CACHE_WARMUP )); then
|
|
warm_cache
|
|
exit 0
|
|
fi
|
|
|
|
log_info "Starting processing with user: ${TORRENT_USER}"
|
|
declare -A warned_dirs=()
|
|
|
|
# Get list of torrents from Transmission
|
|
log_debug "Getting list of torrents..."
|
|
local torrent_ids
|
|
torrent_ids=$(get_torrents)
|
|
log_debug "Found $(echo "$torrent_ids" | wc -l) torrents"
|
|
|
|
echo "$torrent_ids" | while read -r id; do
|
|
# Skip empty IDs
|
|
if [[ -z "$id" ]]; then
|
|
log_debug "Skipping empty torrent ID"
|
|
continue
|
|
fi
|
|
|
|
log_debug "Processing torrent ID: $id"
|
|
local info
|
|
info=$(get_torrent_info "${id}")
|
|
|
|
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}")
|
|
|
|
# Initialize warned_dirs for this directory if needed
|
|
if [[ -n "${dir}" ]]; then
|
|
[[ -z "${warned_dirs["${dir}"]+x}" ]] && warned_dirs["${dir}"]=0
|
|
fi
|
|
|
|
# Avoid processing the same directory more than once.
|
|
if [[ -n "${processed_source_dirs["${dir}"]+x}" ]]; then
|
|
log_info "Directory ${dir} has already been processed; skipping copy for torrent ${id}"
|
|
elif (( $(bc <<< "${percent_done} >= 100") )) && ! is_processed "${hash}"; then
|
|
log_info "Processing completed torrent ${id} (${percent_done}% done)"
|
|
if [[ "${dst}" == "${DEFAULT_DST}" ]] && [[ -n "${dir}" ]] && (( warned_dirs["${dir}"] == 0 )); then
|
|
log_warn "Using default destination for: ${dir}"
|
|
warned_dirs["${dir}"]=1
|
|
fi
|
|
local targets=("${dst}")
|
|
case "${dst}" in
|
|
"${DIR_MOVIES_DST}")
|
|
targets+=("${STORAGE_DIRS_ARRAY[@]}")
|
|
;;
|
|
"${DIR_TV_DST}")
|
|
# If there are TV storage dirs, include them
|
|
[[ -n "${STORAGE_TV_DIRS}" ]] && IFS=',' read -ra TV_DIRS <<< "${STORAGE_TV_DIRS}" && targets+=("${TV_DIRS[@]}")
|
|
;;
|
|
esac
|
|
|
|
if ! files_need_processing "${dir}" "${targets[@]}"; then
|
|
log_info "Skipping copy - files already exist in:"
|
|
for target in "${targets[@]}"; do
|
|
[[ -d "${target}" ]] && log_info " - ${target}"
|
|
done
|
|
else
|
|
process_copy "${id}" "${hash}" "${dir}" "${dst}"
|
|
processed_source_dirs["${dir}"]=1
|
|
fi
|
|
fi
|
|
|
|
if (( $(bc <<< "${ratio} >= ${SEED_RATIO}") )) || (( $(bc <<< "${time} >= ${SEED_TIME}") )); then
|
|
log_info "Removing torrent ${id} (Ratio: ${ratio}, Time: ${time})"
|
|
process_removal "${id}"
|
|
fi
|
|
done
|
|
|
|
# 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
|
|
}
|
|
|
|
######################
|
|
# Command-line Parsing #
|
|
######################
|
|
parse_args "$@"
|
|
|
|
if (( INTERACTIVE )); then
|
|
read -rp "Confirm processing? (y/n) " choice
|
|
[[ "${choice}" =~ ^[Yy]$ ]] || exit 0
|
|
fi
|
|
|
|
main |