torrent/usr/local/bin/torrent-mover
2025-02-23 23:22:58 +01:00

328 lines
9.7 KiB
Bash
Executable File

#!/bin/bash
# Torrent Mover v5.3 - Singleton Implementation
# Singleton pattern
LOCK_FILE="/var/lock/torrent-mover.lock"
MAX_AGE=300 # 5 minutes in seconds
# Check for existing lock
if [ -f "${LOCK_FILE}" ]; then
PID=$(cat "${LOCK_FILE}")
# Check if process exists
if ps -p "${PID}" > /dev/null 2>&1; then
echo "Already running (PID: ${PID}), exiting."
exit 1
else
# Check lock file age
if [ $(($(date +%s) - $(date -r "${LOCK_FILE}" +%s))) -lt ${MAX_AGE} ]; then
echo "Recent crash detected, waiting..."
exit 1
fi
echo "Removing stale lock (PID: ${PID})"
rm -f "${LOCK_FILE}"
fi
fi
# Create new lock
echo $$ > "${LOCK_FILE}"
trap 'rm -f "${LOCK_FILE}"' EXIT TERM INT
set -o errexit
set -o nounset
set -o pipefail
# Load configuration
CONFIG_FILE="/etc/torrent/mover.conf"
source "${CONFIG_FILE}"
# Runtime flags
DRY_RUN=0
INTERACTIVE=0
CACHE_WARMUP=0
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Initialize storage directories
STORAGE_DIRS_ARRAY=()
if [[ -n "${STORAGE_DIRS}" ]]; then
IFS=',' read -ra STORAGE_DIRS_ARRAY <<< "${STORAGE_DIRS}"
fi
# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%F %T') - $*" | tee -a "${LOG_FILE}"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%F %T') - $*" | tee -a "${LOG_FILE}"; }
log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%F %T') - $*" | tee -a "${LOG_FILE}"; }
# Dependency check
check_dependencies() {
local deps=("transmission-remote" "unrar" "unzip" "7z" "parallel" "bc")
for dep in "${deps[@]}"; do
if ! command -v "${dep}" >/dev/null 2>&1; then
log_error "Missing dependency: ${dep}"
exit 1
fi
done
}
# Disk usage monitoring
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=$(df -P "${dir}" | awk 'NR==2 {print $6}')
[[ -z "${mount_point}" ]] && return
if [[ -z "${CHECKED_MOUNTS["${mount_point}"]+x}" ]]; then
local 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
}
# Checksum database
init_checksum_db() {
mkdir -p "$(dirname "${CHECKSUM_DB}")"
touch "${CHECKSUM_DB}"
chmod 600 "${CHECKSUM_DB}"
}
record_checksums() {
log_info "Generating checksums with ${PARALLEL_THREADS:-$(nproc)} threads"
find "$@" -type f \( -iname "*.nfo" -o -iname "*.sfv" \) -prune -o -type f -print0 | \
parallel -0 -j ${PARALLEL_THREADS:-$(nproc)} md5sum | \
sort > "${CHECKSUM_DB}.tmp"
mv "${CHECKSUM_DB}.tmp" "${CHECKSUM_DB}"
}
file_metadata() {
find "$1" -type f \( -iname "*.nfo" -o -iname "*.sfv" \) -prune -o -type f -printf "%s %T@ %p\n" | \
sort | \
md5sum | \
awk '{print $1}'
}
files_need_processing() {
local src="$1" shift
local targets=("$@")
[[ ! -d "${src}" ]] && return 1
local src_meta=$(file_metadata "${src}")
for target in "${targets[@]}"; do
[[ ! -d "${target}" ]] && continue
local target_meta=$(file_metadata "${target}")
[[ "${src_meta}" == "${target_meta}" ]] && return 1
done
local src_checksums=$(find "${src}" -type f \( -iname "*.nfo" -o -iname "*.sfv" \) -prune -o -type f -exec md5sum {} \; | sort)
for target in "${targets[@]}"; do
[[ ! -d "${target}" ]] && continue
local target_checksums=$(find "${target}" -type f \( -iname "*.nfo" -o -iname "*.sfv" \) -prune -o -type f -exec md5sum {} \; | sort)
diff <(echo "${src_checksums}") <(echo "${target_checksums}") >/dev/null && return 1
done
return 0
}
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() {
grep -q "^${1}$" "${PROCESSED_LOG}" 2>/dev/null
}
mark_processed() {
echo "${1}" >> "${PROCESSED_LOG}"
}
get_destination() {
case "${1}" in
*Games*) echo "${DIR_GAMES_DST}";;
*Apps*) echo "${DIR_APPS_DST}";;
*Movies*) echo "${DIR_MOVIES_DST}";;
*Books*) echo "${DIR_BOOKS_DST}";;
*) echo "${DEFAULT_DST}";;
esac
}
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 ${arch##*/}"
case "${arch##*.}" in
rar) unrar x -o- "${arch}" "${dst}";;
zip) unzip -o "${arch}" -d "${dst}";;
7z) 7z x "${arch}" -o"${dst}";;
esac
done
}
move_files() {
if (( PARALLEL_PROCESSING )); then
parallel -j ${PARALLEL_THREADS:-$(nproc)} mv {} "${1}" ::: "${2}"/*
else
mv "${2}"/* "${1}"
fi
}
copy_files() {
if (( PARALLEL_PROCESSING )); then
parallel -j ${PARALLEL_THREADS:-$(nproc)} cp -r {} "${1}" ::: "${2}"/*
else
cp -r "${2}"/* "${1}"
fi
}
process_copy() {
local id="$1" hash="$2" src="$3" dst="$4"
if (( DRY_RUN )); then
log_info "[DRY RUN] Would process torrent ${id}:"
log_info " - Copy files from ${src} to ${dst}"
return
fi
mkdir -p "${dst}"
handle_archives "${src}" "${dst}"
case "${COPY_MODE}" in
move) move_files "${dst}" "${src}";;
copy) copy_files "${dst}" "${src}";;
esac
}
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() {
check_dependencies
init_checksum_db
if (( CACHE_WARMUP )); then
warm_cache
exit 0
fi
log_info "Starting processing"
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
local info=$(transmission-remote "${TRANSMISSION_IP}:${TRANSMISSION_PORT}" \
-n "${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}" -t "${id}" -i)
local hash=$(grep "Hash:" <<< "${info}" | awk '{print $2}')
# Sanitize numeric values with fallbacks
local ratio=$(grep "Ratio:" <<< "${info}" | awk '{print $2 == "None" ? 0 : $2}' | tr -cd '0-9.')
ratio=${ratio:-0} # Handle empty values
local time=$(grep "Seeding Time:" <<< "${info}" | awk '{print $3 == "None" ? 0 : $3}' | tr -cd '0-9.')
time=${time:-0} # Handle empty values
local percent_done=$(grep "Percent Done:" <<< "${info}" | awk '{gsub(/%/, ""); print $3 == "None" ? 0 : $3}')
percent_done=${percent_done:-0} # Handle empty values
local dir=$(grep "Location:" <<< "${info}" | cut -d' ' -f4-)
local dst=$(get_destination "${dir}")
# Initialize warning tracking
[[ -z "${warned_dirs["${dir}"]+x}" ]] && warned_dirs["${dir}"]=0
# 1. Handle completed downloads
if (( $(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
log_warn "Using default destination for: ${dir}"
warned_dirs["${dir}"]=1
fi
# Determine check targets
local targets=("${dst}")
if [[ "${dst}" == "${DIR_MOVIES_DST}" ]]; then
targets+=("${STORAGE_DIRS_ARRAY[@]}")
fi
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}"
fi
mark_processed "${hash}"
fi
# 2. Handle seeding criteria
if (( $(bc <<< "${ratio} >= ${SEED_RATIO}") )) || (( $(bc <<< "${time} >= ${SEED_TIME}") )); then
log_info "Removing torrent ${id} (Ratio: ${ratio}, Time: ${time})"
process_removal "${id}"
fi
done
# Final disk checks
check_disk_usage "${DIR_GAMES_DST}"
check_disk_usage "${DIR_APPS_DST}"
check_disk_usage "${DIR_MOVIES_DST}"
check_disk_usage "${DIR_BOOKS_DST}"
check_disk_usage "${DEFAULT_DST}"
}
# Argument handling
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=1; shift ;;
--interactive) INTERACTIVE=1; shift ;;
--cache-warmup) CACHE_WARMUP=1; shift ;;
--help)
echo "Usage: $0 [--dry-run] [--interactive] [--cache-warmup]"
exit 0
;;
*) echo "Invalid option: $1"; exit 1 ;;
esac
done
# Execution
if (( INTERACTIVE )); then
read -rp "Confirm processing? (y/n) " choice
[[ "${choice}" =~ ^[Yy]$ ]] || exit 0
fi
main