some program redesign
This commit is contained in:
45
usr/local/lib/torrent-mover/archive_handler.sh
Normal file
45
usr/local/lib/torrent-mover/archive_handler.sh
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
# Archive extraction handler for torrent-mover
|
||||
|
||||
# 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.
|
||||
# The archive is 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; }
|
||||
|
||||
# Apply proper permissions to the extraction directory
|
||||
chmod 775 "${subdir}"
|
||||
chown ${TORRENT_USER:-debian-transmission}:${TORRENT_GROUP:-debian-transmission} "${subdir}"
|
||||
|
||||
local extract_success=0
|
||||
case "${arch##*.}" in
|
||||
rar)
|
||||
retry_command "unrar x -o- \"${arch}\" \"${subdir}\"" 3 10
|
||||
extract_success=$?
|
||||
;;
|
||||
zip)
|
||||
retry_command "unzip -o \"${arch}\" -d \"${subdir}\"" 3 10
|
||||
extract_success=$?
|
||||
;;
|
||||
7z)
|
||||
retry_command "7z x \"${arch}\" -o\"${subdir}\"" 3 10
|
||||
extract_success=$?
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ $extract_success -eq 0 ]; then
|
||||
log_info "Archive ${arch} extracted successfully to ${subdir}"
|
||||
log_info "Archive ${arch} retained in source until ratio limits are reached."
|
||||
else
|
||||
log_error "Failed to extract archive ${arch}"
|
||||
fi
|
||||
done
|
||||
}
|
139
usr/local/lib/torrent-mover/common.sh
Normal file
139
usr/local/lib/torrent-mover/common.sh
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/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
|
||||
[[ "${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)
|
||||
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" "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%.
|
||||
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
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
while (( attempt <= max_attempts )); do
|
||||
log_debug "Attempt $attempt of $max_attempts: $cmd"
|
||||
if eval "$cmd"; then
|
||||
return 0
|
||||
else
|
||||
log_warn "Command failed (attempt $attempt): $cmd"
|
||||
if (( attempt == max_attempts )); then
|
||||
log_error "Maximum attempts reached for: $cmd"
|
||||
return 1
|
||||
fi
|
||||
sleep "$wait_time"
|
||||
(( attempt++ ))
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# validate_directories: Ensure required directories exist and are writable
|
||||
validate_directories() {
|
||||
local directories=("$@")
|
||||
for dir in "${directories[@]}"; do
|
||||
if [[ ! -d "${dir}" ]]; then
|
||||
log_error "Directory missing: ${dir}"
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -w "${dir}" ]]; then
|
||||
log_error "Write permission denied: ${dir}"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
191
usr/local/lib/torrent-mover/file_operations.sh
Normal file
191
usr/local/lib/torrent-mover/file_operations.sh
Normal file
@@ -0,0 +1,191 @@
|
||||
#!/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}"
|
||||
}
|
||||
|
||||
# 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}"
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
# 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 ${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
|
||||
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}"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
84
usr/local/lib/torrent-mover/transmission_handler.sh
Normal file
84
usr/local/lib/torrent-mover/transmission_handler.sh
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/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"
|
||||
if [[ -n "${PATH_CACHE["${source_path}"]+x}" ]]; then
|
||||
echo "${PATH_CACHE["${source_path}"]}"
|
||||
return
|
||||
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}"
|
||||
PATH_CACHE["${source_path}"]="${destination}"
|
||||
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
|
||||
|
||||
retry_command "transmission-remote \"${TRANSMISSION_IP}:${TRANSMISSION_PORT}\" -n \"${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}\" -t \"${id}\" --remove-and-delete" 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}'
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
Reference in New Issue
Block a user