Fix torrent processing issues in transmission_handler.sh

- 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
This commit is contained in:
2025-03-04 17:15:51 +01:00
parent 4c7ebaf5fe
commit f572a241ef
8 changed files with 504 additions and 21 deletions
+183
View 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"
+59 -4
View File
@@ -115,7 +115,19 @@ main() {
declare -A warned_dirs=()
# Get list of torrents from Transmission
get_torrents | while read -r id; do
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}")
@@ -134,19 +146,62 @@ main() {
# Extract Transmission-reported directory and translate to local path.
local reported_dir
reported_dir=$(grep -i "Location:" <<< "${info}" | awk -F": " '{print $2}' | xargs)
log_debug "Raw reported directory: '${reported_dir}'"
# If the reported directory is empty, try to derive it from the name
if [[ -z "${reported_dir}" ]]; then
local name
name=$(grep -i "Name:" <<< "${info}" | awk -F": " '{print $2}' | xargs)
log_debug "Torrent name: '${name}'"
# Check if there are labels we can use
local labels
labels=$(grep -i "Labels:" <<< "${info}" | awk -F": " '{print $2}' | xargs)
log_debug "Torrent labels: '${labels}'"
if [[ "${labels}" == *"Books"* ]]; then
reported_dir="/downloads/Books"
elif [[ "${labels}" == *"Movies"* ]]; then
reported_dir="/downloads/Movies"
elif [[ "${labels}" == *"TV"* ]]; then
reported_dir="/downloads/TV"
elif [[ "${labels}" == *"Games"* ]]; then
reported_dir="/downloads/Games"
elif [[ "${labels}" == *"Apps"* ]]; then
reported_dir="/downloads/Apps"
elif [[ "${labels}" == *"Music"* ]]; then
reported_dir="/downloads/Music"
else
# Default to Other if we can't determine
reported_dir="/downloads/Other"
fi
log_debug "Derived directory from labels: '${reported_dir}'"
fi
local dir
dir=$(translate_source "${reported_dir}")
log_info "Torrent source directory reported: '${reported_dir}' translated to '${dir}'"
log_info "Torrent source directory: '${reported_dir}' translated to '${dir}'"
# Initialize empty directory mapping if needed
if [[ -z "$dir" ]]; then
log_warn "Empty directory path detected, using default"
dir="${LOCAL_PATH_PREFIX}/Other"
fi
local dst
dst=$(get_destination "${dir}")
[[ -z "${warned_dirs["${dir}"]+x}" ]] && warned_dirs["${dir}"]=0
# Initialize warned_dirs for this directory if needed
if [[ -n "${dir}" ]]; then
[[ -z "${warned_dirs["${dir}"]+x}" ]] && warned_dirs["${dir}"]=0
fi
# Avoid processing the same directory more than once.
if [[ -n "${processed_source_dirs["${dir}"]+x}" ]]; then
log_info "Directory ${dir} has already been processed; skipping copy for torrent ${id}"
elif (( $(bc <<< "${percent_done} >= 100") )) && ! is_processed "${hash}"; then
log_info "Processing completed torrent ${id} (${percent_done}% done)"
if [[ "${dst}" == "${DEFAULT_DST}" ]] && (( warned_dirs["${dir}"] == 0 )); then
if [[ "${dst}" == "${DEFAULT_DST}" ]] && [[ -n "${dir}" ]] && (( warned_dirs["${dir}"] == 0 )); then
log_warn "Using default destination for: ${dir}"
warned_dirs["${dir}"]=1
fi
+133
View File
@@ -0,0 +1,133 @@
#\!/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
/usr/local/bin/torrent-mover --debug
elif [ -n "$CATEGORY" ]; then
echo "Processing category: $CATEGORY"
# Set category-specific filter
case $CATEGORY in
books)
echo "Looking for book torrents..."
;;
movies)
echo "Looking for movie torrents..."
;;
tv)
echo "Looking for TV show torrents..."
;;
apps)
echo "Looking for application torrents..."
;;
games)
echo "Looking for game torrents..."
;;
esac
# Run torrent-mover
/usr/local/bin/torrent-mover --debug
else
echo "Processing all torrents"
/usr/local/bin/torrent-mover --debug
fi
echo "Processing complete\!"
echo "Check /var/log/torrent_mover.log for details"
+37 -7
View File
@@ -18,23 +18,31 @@ declare -A PATH_CACHE
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] $*"
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
[[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[INFO] $*"
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
[[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[WARN] $*"
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
[[ "${USE_SYSLOG}" == "true" ]] && logger -t torrent-mover "[ERROR] $*"
if [[ "${USE_SYSLOG}" == "true" ]]; then
logger -t torrent-mover "[ERROR] $*" || true
fi
}
# Error Handling & Notifications
@@ -93,20 +101,42 @@ check_dependencies() {
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}')
[[ -z "${mount_point}" ]] && return
if [[ -z "${CHECKED_MOUNTS["${mount_point}"]+x}" ]]; then
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
CHECKED_MOUNTS[${mount_point}]=1
else
log_debug "Mount point ${mount_point} already checked"
fi
}
@@ -4,6 +4,13 @@
# 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
if [[ -n "${PATH_CACHE["${source_path}"]+x}" ]]; then
echo "${PATH_CACHE["${source_path}"]}"
return
@@ -56,7 +63,12 @@ get_destination() {
fi
log_info "Mapped to: ${destination}"
PATH_CACHE["${source_path}"]="${destination}"
# Only set in cache if source_path is not empty
if [[ -n "${source_path}" ]]; then
PATH_CACHE["${source_path}"]="${destination}"
fi
echo "${destination}"
}
@@ -68,17 +80,26 @@ process_removal() {
return
fi
retry_command "transmission-remote \"${TRANSMISSION_IP}:${TRANSMISSION_PORT}\" -n \"${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}\" -t \"${id}\" --remove-and-delete" 3 15
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() {
retry_command "transmission-remote \"${TRANSMISSION_IP}:${TRANSMISSION_PORT}\" -n \"${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}\" -l" 3 20 |
awk 'NR>1 && $1 ~ /^[0-9]+$/ {print $1}'
local cmd="transmission-remote ${TRANSMISSION_IP}:${TRANSMISSION_PORT} -n ${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD} -l"
log_debug "Running command: $cmd"
local output
output=$(retry_command "$cmd" 3 20)
# Extract IDs directly using awk
# Skip the header line (NR>1) and print the first column
# The IDs are right-aligned with spaces in front, so we need to trim them
echo "$output" | awk 'NR>1 && NF>1 {gsub(/^[ ]+/, "", $1); if ($1 ~ /^[0-9]+$/) print $1}'
}
# get_torrent_info: Gets detailed info for a specific torrent
get_torrent_info() {
local id="$1"
retry_command "transmission-remote \"${TRANSMISSION_IP}:${TRANSMISSION_PORT}\" -n \"${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}\" -t \"${id}\" -i" 3 15
local cmd="transmission-remote ${TRANSMISSION_IP}:${TRANSMISSION_PORT} -n ${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD} -t ${id} -i"
retry_command "$cmd" 3 15
}