Skip to main content

IT - Programming - BASH scripting - qBittorrents_Manage.sh

#!/bin/bash
[[ "$(uname -a)" != *CYGWIN_NT* ]] && echo "This script can only be executed under Cygwin" && exit 1
# 🧠 ChatGPT-aware developer instructions at bottom of script (__CHATGPT_INSTRUCTIONS_BEGIN__)
# Published: https://afberendsen.blogspot.com/2025/04/it-programming-bash-script_19.html
# -----------------------------------------------------------------------------
# Script: qBittorrent_Manage.sh.sh
# Purpose: Finds torrents in category 'TV' with specific name prefixes and
#          applies high priority and force-starts them in qBittorrent.
#          Also deletes torrents listed in the DELETE section of TV_Organize_Patterns.txt.
#
# Version Log:
# v1.0  - Initial version
# v1.1  - Refactored with process substitution, added error handling
# v1.2  - Added support for deletion based on TV_Organize_Patterns.txt
# v1.3  - Ignore comments and empty lines in TV_Organize_Patterns.txt
# v1.4  - Added support for CLI options: --dry-run, --help, --set-priority, --delete
# v1.5  - Default mode is now dry-run unless explicitly disabled
# v1.6  - Refactored while loops to use < <() form (avoid subshell)
# v1.7  - Added logging timestamps and duration tracking for each major step
# v1.8  - Added --limit option to restrict number of torrents processed
# v1.9  - Added --remove-torrent / --remove-files switch for deletion mode
# v1.10 - Store removed torrent files under /cygdrive/f/p_qBittorrent/.cache/BackupRemoval/<timestamp>
# v1.11 - Fixed DELETE API call to use POST request for reliable deletion
# v1.12 - Added --verbose (-v) and --version (-V) options
# v1.13 - Added retry logic for undeleted torrents with user prompt
# v1.14 - Optimized deletion verification by rechecking only marked hashes
# v1.15 - Added --ignore-not-delete to skip undeleted retry logic
# v1.16 - Added --debug (-D) for verbose curl and internal variable tracing
# v1.17 - Added reachability check, infinite retry, and logging to .logs directory
# v1.18 - Fixed dry-run flag, removed unused --execute option, updated Cygwin check
# v1.19 - Renamed functions to PascalCase, standardized logging with Echo/EchoD
# v1.20 - Fixed trap command syntax error by properly escaping date format string
# v1.21 - Added summary and priority verification in DoSetPriority (found, changed, confirmed)
# v1.22 - Added --no-dry-run flag to explicitly disable dry-run mode, updated help message
# v1.23 - Optimized DoSetPriority to use a single API call for multiple hashes, added delay before verification, and added response checking
# v1.24 - Added MoveCompletedTorrents and CheckIncompleteTorrents functions, added --manage-downloads mode
# v1.25 - Added --execute and -x as aliases for --no-dry-run
# v1.26 - Removed redundant timestamps in Echo/EchoE/EchoW/EchoD calls
# v1.27 - Normalized spaces, dashes, and multiple dots to single dots in PREFIXES and torrent names for matching in DoSetPriority
# v1.28 - Extended normalizations in DoSetPriority: added underscores and colons to dots, trim leading/trailing dots, and convert to lowercase
# v1.29 - Fixed execution flow: stop after dependency check if no execution mode is specified
# v1.30 - Fixed jq filter in DoSetPriority: corrected escaping in gsub and escaped dots in NORMALIZED_PREFIXES for regex
# v1.31 - Fixed DoSetPriority to only match torrents beginning with prefixes, added debug output for normalized names
# v1.32 - Fixed DoSetPriority to correctly filter torrents by prefixes using jq's any() and startswith()
# v1.33 - Fixed jq assertion error in DoSetPriority by correcting the startswith() usage
# v1.34 - Fixed jq assertion error by breaking down the jq command into steps and adding error handling
# v1.35 - Fixed empty prefix issue in NORMALIZED_PREFIXES and moved prefix matching to Bash to avoid jq bug
# v1.36 - Fixed readonly issue with NORMALIZED_PREFIXES, improved performance by using TSV output, and fixed jq parse errors
# v1.37 - Fixed numeric prefix issue ('1923') and added debugging for TV torrents in DoSetPriority
# v1.38 - Added explicit check for empty prefixes in NORMALIZED_PREFIXES, with warnings and removal
# v1.39 - Moved prefix normalization to DoSetPriority, removed numeric-only skip, added .s99 pattern matching
# v1.40 - Fixed normalization to preserve dots in prefixes
# v1.41 - Fixed first letter removal and '1923' skipping issues with detailed normalization steps
# v1.42 - Updated DoSetPriority to use /api/v2/torrents/topPrio endpoint for setting priorities, adjusted verification to expect sequential priorities (1 to N), and increased wait time to 5 seconds
# v1.43 - Added conditional logging in DoSetPriority to show [SET-PRIORITY] messages only when --verbose is set
# v1.44 - Simplified verification in DoSetPriority to check if priorities are <= N (number of torrents), ensuring torrents are in top N positions
# v1.45 - Added hash-to-name mapping in DoSetPriority to include torrent names in verification warnings
# v1.46 - Enhanced MoveCompletedTorrents with debugging for save_path and better API response handling for setLocation
# -----------------------------------------------------------------------------# -----------------------------------------------------------------------------# -----------------------------------------------------------------------------

readonly SCRIPT_VERSION="1.46"

# Load shared library
[[ ! -e ~/MY_SECRETS.sh ]] && { echo "💥 Cannot find ~/MY_SECRETS.sh" ; exit 1; }
[[ ! -r ~/MY_SECRETS.sh ]] && { echo "💥 ~/MY_SECRETS.sh is not readable" ; exit 1; }
source ~/MY_SECRETS.sh || { echo "💥 Cannot source ~/MY_SECRETS.sh" ; exit 1; }

[[ ! -e ~/MY_LIBRARY.sh ]] && { echo "💥 Cannot find library ~/MY_LIBRARY.sh" ; exit 1; }
[[ ! -r ~/MY_LIBRARY.sh ]] && { echo "💥 Library ~/MY_LIBRARY.sh is not readable" ; exit 1; }
source ~/MY_LIBRARY.sh || { echo "💥 Cannot source library ~/MY_LIBRARY.sh" ; exit 1; }

set -o monitor
trap 'echo;Echo "💥 Caught interrupt signal, exiting."; exit 130' SIGINT SIGTERM

# -----------------------------------------------------------------------------
# Constants and Default Flags
# -----------------------------------------------------------------------------
declare -r QBT_HOST="http://localhost:8077"
declare -r API_URL="${QBT_HOST}/api/v2/torrents/info"
declare -r CMD_PRIO="${QBT_HOST}/api/v2/torrents/topPrio"
declare -r CMD_FORCE="${QBT_HOST}/api/v2/torrents/setForceStart"
declare -r CMD_DELETE="${QBT_HOST}/api/v2/torrents/delete"
declare -r CMD_EXPORT="${QBT_HOST}/api/v2/torrents/export"
declare -r CMD_SET_LOCATION="${QBT_HOST}/api/v2/torrents/setLocation"
declare -r PATTERN_FILE="TV_Organize_Patterns.txt"
declare -r LOG_DIR="/cygdrive/f/p_qBittorrent/.logs"
declare -r REACHABILITY_LOG="${LOG_DIR}/qbt_reachability.log"
declare -r LAST_FAILURE_LOG="${LOG_DIR}/.last_failure.log"

# Instance to Port Mapping
declare -A QBT_INSTANCES=(
  ["anime"]=8079
  ["xxx"]=8078
  ["tv"]=8077
  ["movie"]=8076
)

# Instance to Temporary Directory Mapping
declare -A TEMP_DIRS=(
  ["anime"]="/cygdrive/f/p_qBittorrent/anime/d"
  ["xxx"]="/cygdrive/f/p_qBittorrent/xxx/d"
  ["tv"]="/cygdrive/f/p_qBittorrent/tv/d"
  ["movie"]="/cygdrive/f/p_qBittorrent/movie/d"
)

# Instance to Final Directory Mapping
declare -A FINAL_DIRS=(
  ["anime"]="/cygdrive/p/torrent_finished_anime"
  ["xxx"]="/cygdrive/p/torrent_finished_xxx"
  ["tv"]="/cygdrive/p/torrent_finished_tv"
  ["movie"]="/cygdrive/p/torrent_finished_movie"
)

# Runtime Flags and Parameters
declare SELECTED_INSTANCE=""
declare INSTANCE_NAME=""
declare -i DRY_RUN=1
declare -i DO_DELETE=0
declare -i DO_SET_PRIORITY=0
declare -i DO_MANAGE_DOWNLOADS=0
declare -i LIMIT=0
declare -i REMOVE_FILES=0
declare -i VERBOSE=0
declare -i DEBUG=0
declare -i IGNORE_NOT_DELETED=0
declare -i CHECK_RETRY=3
declare -i CHECK_DELAY=3
declare -i CHECK_DEPS_DEFAULT=0
declare -i CHECK_DEPS_LOCAL=1
declare -i CHECK_DEPS_INSTALL=0
declare -i CHECK_FOREVER=0
declare -i VERIFY=0

# Execution mode constants
declare -i EXECUTION_MODE=0
declare -r MODE_NONE=0
declare -r MODE_DELETE=1
declare -r MODE_RECHECK=2
declare -r MODE_PRIORITY=3
declare -r MODE_MANAGE_DOWNLOADS=4
declare -r MODE_REMOVE_MALFORMED=5
declare -r MODE_FIX_CATEGORY=6
declare -r MODE_MOVE_COMPLETED=7
declare -r MODE_MOVE_INCOMPLETE=8
declare -r MODE_SANITISE=9

#==============================================================================
# ShowHelp - Displays command usage and options
#------------------------------------------------------------------------------
# Description:
#   Prints the help message to stdout, detailing script usage, options, execution
#   modes, and examples.
#
# Globals:
#   None
#
# Arguments:
#   None
#
# Outputs:
#   Writes help message to stdout via `cat`.
#
# Exit Codes:
#   None (function does not exit the script)
#
# Side Effects:
#   None
#==============================================================================
ShowHelp() {
  cat <<EOF

Usage: ${0##*/} [OPTIONS] --delete | --recheck-error | --set-priority | --manage-downloads

📖 Description:
  Manage and maintain qBittorrent TV torrents: recheck, prioritise, delete, or manage download locations.

==============================
  🌐 General Options
==============================
  -h, --help              Show this help message and exit
  -V, --version           Show script version
  -n, --dry-run           Simulate actions (no changes made, default)
  --no-dry-run            Apply changes (disable dry-run mode)
  -x, --execute           Alias for --no-dry-run
  -v, --verbose           Increase output verbosity (can be used multiple times)
  -D, --debug             Enable debug logging and verbose curl

Dependency Check Modes (mutually exclusive):
      --check-deps           Check for required tools (safe fallback mode)
      --check-deps-local     Use local fallbacks (e.g., define 'urlencode')
      --check-deps-install   Attempt to install tools via Cygwin installer

==============================
  🚦 Execution Modes (choose one)
==============================
  --delete                Delete torrents based on a pattern file
  --set-priority          Force-start and top-prioritize TV torrents by prefix
  --recheck-error         Recheck errored torrents in a qBittorrent instance
  --manage-downloads      Move completed torrents to final directories and ensure incomplete torrents are in temporary directories
  --remove-malformed      Delete torrents with missing or malformed save paths
  --set-categories        Fix incorrect category assignments based on instance
  --move-completed        Move completed torrents to their final destination
  --move-incomplete       Move incomplete torrents to temporary download folders
  --sanitise              Run all sanitization actions in sequence (see above)

==============================
  ⚙️ Mode-Specific Options
==============================

🔹 Sanitization modes:
      --instance <name>      Optional. Applies to one instance (tv, anime, movie, xxx) or 'all'
                             Default: all

🔹 --recheck-error mode:
      --instance <name>      REQUIRED. Choose one instance (tv, anime, movie, xxx) or 'all'

🔹 --delete mode:
      --pattern <file>       Path to name pattern file
      --remove-torrent       Delete torrents but keep downloaded files
      --remove-files         Delete torrents AND downloaded files
      --ignore-not-delete    Skip retrying undeleted torrents

🔹 --set-priority mode:
      --category <name>      Limit scope to a specific qBittorrent category
      --verify

🔹 --manage-downloads mode:
      --instance <name>      REQUIRED. Choose one instance (tv, anime, movie, xxx) or 'all'

==============================
Examples:
==============================
  ${0##*/} --recheck-error --instance tv
  ${0##*/} --set-priority --execute --verbose
  ${0##*/} --delete --remove-files --pattern TV_Organize_Patterns.txt
  ${0##*/} --manage-downloads --instance anime -x
  ${0##*/} --remove-malformed --instance xxx
  ${0##*/} --set-categories --instance tv --execute
  ${0##*/} --sanitise --instance all --execute

EOF
}

#==============================================================================
# GetTorrentSummary - Counts total number of torrents
#------------------------------------------------------------------------------
# Description:
#   Queries the qBittorrent API to retrieve the total number of torrents and
#   returns the count.
#
# Globals:
#   API_URL - qBittorrent API URL for torrent info (RO)
#
# Arguments:
#   None
#
# Outputs:
#   Writes torrent count to stdout via `jq`.
#
# Exit Codes:
#   None (function does not exit the script)
#
# Side Effects:
#   Makes a curl request to the qBittorrent API.
#==============================================================================
GetTorrentSummary() {
  curl --silent "${API_URL}" | jq length
}

#==============================================================================
# CheckQbtReachability - Validates qBittorrent API accessibility
#------------------------------------------------------------------------------
# Description:
#   Checks if the qBittorrent API is reachable by sending a curl request with
#   retries. Logs failures and exits on timeout.
#
# Globals:
#   API_URL            - qBittorrent API URL for torrent info (RO)
#   CHECK_RETRY        - Number of retry attempts (RO)
#   CHECK_DELAY        - Delay between retries in seconds (RO)
#   CHECK_FOREVER      - Flag to retry indefinitely (RO)
#   REACHABILITY_LOG   - Path to reachability log file (RO)
#   LAST_FAILURE_LOG   - Path to last failure log file (RO)
#
# Arguments:
#   None
#
# Outputs:
#   Writes informational messages to stdout via `Echo`.
#   Writes error messages to stdout via `EchoE`.
#   Appends failure logs to `REACHABILITY_LOG` and `LAST_FAILURE_LOG`.
#
# Exit Codes:
#   0 - Success (API is reachable)
#   1 - Failure (API is unreachable after retries)
#
# Side Effects:
#   Makes multiple curl requests to the qBittorrent API.
#   Appends to log files.
#==============================================================================
CheckQbtReachability() {
  local -i attempt=1
  local ok=false

  Echo "🔎 Checking if qBittorrent is reachable at ${API_URL} ..."
  while true; do
    if curl --silent --fail --max-time 5 "${API_URL}" > /dev/null; then
      ok=true
      break
    fi

    Echo "Attempt ${attempt}/${CHECK_RETRY} failed. Retrying in ${CHECK_DELAY}s..."
    echo "$(date '+%F %T') Attempt ${attempt} failed" >> "${REACHABILITY_LOG}"
    sleep "${CHECK_DELAY}"
    ((attempt++))

    if [[ ${CHECK_FOREVER} -eq 0 && ${attempt} -gt ${CHECK_RETRY} ]]; then
      break
    fi
  done

  if [[ ${ok} != true ]]; then
    echo "$(date '+%F %T') qBittorrent unreachable at ${API_URL}" >> "${LAST_FAILURE_LOG}"
    EchoE "ERROR: qBittorrent is not responding at ${API_URL} after ${CHECK_RETRY} attempts"
    exit 1
  fi
}

#==============================================================================
# ExportTorrentFile - Saves a .torrent file before deletion
#------------------------------------------------------------------------------
# Description:
#   Exports a .torrent file for a given torrent hash to a specified destination
#   using the qBittorrent API.
#
# Globals:
#   QBT_HOST     - Base URL of qBittorrent instance (RO)
#   CMD_EXPORT   - API URL for exporting torrents (RO)
#
# Arguments:
#   $1 - Torrent hash
#   $2 - Destination file path for the .torrent file
#
# Outputs:
#   None (output is redirected to the destination file)
#
# Exit Codes:
#   None (function does not exit the script)
#
# Side Effects:
#   Makes a curl request to the qBittorrent API.
#   Creates a .torrent file at the specified destination.
#==============================================================================
ExportTorrentFile() {
  local hash="${1}"
  local dest="${2}"
  curl --silent --fail -X POST -H "Referer: ${QBT_HOST}" --data "hashes=${hash}" "${CMD_EXPORT}" > "${dest}"
}

#==============================================================================
# DeleteTorrent - Sends a delete command to qBittorrent API
#------------------------------------------------------------------------------
# Description:
#   Deletes a torrent by hash using the qBittorrent API, optionally deleting
#   associated files based on the REMOVE_FILES flag.
#
# Globals:
#   QBT_HOST       - Base URL of qBittorrent instance (RO)
#   CMD_DELETE     - API URL for deleting torrents (RO)
#   REMOVE_FILES   - Flag to delete torrent files (RO)
#   DEBUG          - Flag for debug logging (RO)
#
# Arguments:
#   $1 - Torrent hash
#
# Outputs:
#   Writes debug curl command and output to stdout via `EchoD` if DEBUG=1.
#
# Exit Codes:
#   None (function does not exit the script)
#
# Side Effects:
#   Makes a curl request to the qBittorrent API.
#   Deletes the torrent and optionally its files in qBittorrent.
#==============================================================================
DeleteTorrent() {
  local hash="${1}"
  if [[ ${DEBUG} -eq 1 ]]; then
    EchoD "curl --data 'hashes=${hash}&deleteFiles=${REMOVE_FILES}' ${CMD_DELETE}"
    curl --verbose -X POST \
      -H "Referer: ${QBT_HOST}" \
      --data "hashes=${hash}&deleteFiles=${REMOVE_FILES}" \
      "${CMD_DELETE}"
  else
    curl --silent --fail -X POST \
      -H "Referer: ${QBT_HOST}" \
      --data "hashes=${hash}&deleteFiles=${REMOVE_FILES}" \
      --output /dev/null \
      "${CMD_DELETE}"
  fi
}

#==============================================================================
# RetryDeletePrompt - Re-attempts deletion of failed torrents
#------------------------------------------------------------------------------
# Description:
#   Verifies if torrents marked for deletion were removed and prompts the user to
#   retry deleting any that remain.
#
# Globals:
#   API_URL   - qBittorrent API URL for torrent info (RO)
#   QBT_HOST  - Base URL of qBittorrent instance (RO)
#
# Arguments:
#   $1 - Array of torrent hashes to verify
#
# Outputs:
#   Writes informational messages to stdout via `Echo`.
#   Writes prompts to stdout for user input.
#
# Exit Codes:
#   None (function does not exit the script)
#
# Side Effects:
#   Makes curl requests to the qBittorrent API to check torrent status.
#   Calls `DeleteTorrent` for retries.
#   Reads user input from stdin.
#==============================================================================
RetryDeletePrompt() {
  local -a failed_hashes=("${!1}")
  local -i retry=1
  local -a still_present=()
  local remaining=""
  local retry_choice=""

  while [[ ${retry} -eq 1 ]]; do
    Echo "🔍 Verifying if all ${#failed_hashes[@]} torrents were deleted..."
    remaining=$(curl --silent "${API_URL}" | jq -r '[.[] | .hash] | @sh')

    still_present=()
    for hash in "${failed_hashes[@]}"; do
      if grep -q "${hash}" <<< "${remaining}"; then
        still_present+=("${hash}")
      fi
    done

    if [[ ${#still_present[@]} -eq 0 ]]; then
      Echo "✅ All marked torrents were successfully deleted."
      break
    fi

    Echo "${#still_present[@]} torrent(s) were NOT deleted."
    read -rp "❓ Retry deleting them? (y/N): " retry_choice
    if [[ "${retry_choice}" =~ ^[Yy]$ ]]; then
      for hash in "${still_present[@]}"; do
        Echo "[RETRY DELETE] ${hash}"
        DeleteTorrent "${hash}"
      done
      sleep 2
    else
      retry=0
      Echo "🚫 Skipping retry. ${#still_present[@]} torrents remain undeleted."
    fi
  done
}

#==============================================================================
# ParseArguments - Parses command-line arguments
#------------------------------------------------------------------------------
# Description:
#   Processes command-line arguments to set execution modes, flags, and parameters.
#   Validates inputs and displays help on invalid options.
#
# Globals:
#   DRY_RUN               - Flag to simulate actions (RW)
#   VERBOSE               - Flag for detailed output (RW)
#   DEBUG                 - Flag for debug logging (RW)
#   CHECK_DEPS_DEFAULT    - Flag for default dependency check (RW)
#   CHECK_DEPS_LOCAL      - Flag for local dependency fallback (RW)
#   CHECK_DEPS_INSTALL    - Flag for dependency installation (RW)
#   EXECUTION_MODE        - Selected execution mode (RW)
#   LIMIT                 - Maximum number of torrents to process (RW)
#   REMOVE_FILES          - Flag to delete torrent files (RW)
#   IGNORE_NOT_DELETED    - Flag to skip retrying undeleted torrents (RW)
#   CHECK_RETRY           - Number of retry attempts (RW)
#   CHECK_DELAY           - Delay between retries (RW)
#   CHECK_FOREVER         - Flag to retry indefinitely (RW)
#   VERIFY                - Flag to verify priority changes (RW)
#   INSTANCE_NAME         - Selected qBittorrent instance (RW)
#   SCRIPT_VERSION        - Script version string (RO)
#
# Arguments:
#   $@ - Command-line arguments
#
# Outputs:
#   Writes error messages to stdout via `EchoE` for invalid options.
#   Writes version or help message via `Echo` or `ShowHelp` if requested.
#
# Exit Codes:
#   0 - Success
#   1 - Failure (invalid option or instance)
#
# Side Effects:
#   Modifies global variables based on arguments.
#   May exit the script for help or version requests.
#==============================================================================
ParseArguments() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      -n|--dry-run)         DRY_RUN=1; shift ;;
      --no-dry-run)         DRY_RUN=0; shift ;;
      -x|--execute)         DRY_RUN=0; shift ;;
      -v|--verbose)         (( VERBOSE++ )); shift ;;
      -D|--debug)           (( DEBUG++ )); shift ;;
      -V|--version)         Echo "📜 ${0##*/} version ${SCRIPT_VERSION}"; exit 0 ;;
      -h|--help)            ShowHelp; exit 0 ;;
      --check-deps)         CHECK_DEPS_DEFAULT=1; shift ;;
      --check-deps-local)   CHECK_DEPS_LOCAL=1; shift ;;
      --check-deps-install) CHECK_DEPS_INSTALL=1; shift ;;

      --set-priority)       EXECUTION_MODE=${MODE_PRIORITY}; shift ;;
      --delete)             EXECUTION_MODE=${MODE_DELETE}; shift ;;
      --recheck-error)      EXECUTION_MODE=${MODE_RECHECK}; shift ;;
      --remove-malformed)   EXECUTION_MODE=${MODE_REMOVE_MALFORMED}; shift ;;
      --fix-category)       EXECUTION_MODE=${MODE_FIX_CATEGORY}; shift ;;
      --move-completed)     EXECUTION_MODE=${MODE_MOVE_COMPLETED}; shift ;;
      --move-incomplete)    EXECUTION_MODE=${MODE_MOVE_INCOMPLETE}; shift ;;
      --sanitise)           EXECUTION_MODE=${MODE_SANITISE}; shift ;;

      --limit)              LIMIT="$2"; shift 2 ;;
      --remove-torrent)     REMOVE_FILES=0; shift ;;
      --remove-files)       REMOVE_FILES=1; shift ;;
      --ignore-not-delete)  IGNORE_NOT_DELETED=1; shift ;;
      --check-retry)        CHECK_RETRY="$2"; shift 2 ;;
      --check-delay)        CHECK_DELAY="$2"; shift 2 ;;
      --check-forever)      CHECK_FOREVER=1; shift ;;
      --verify)             (( VERIFY++ )); shift ;;
      --instance)
        INSTANCE_NAME="${2,,}"  # convert to lowercase
        case "${INSTANCE_NAME}" in
          tv|anime|movie|xxx|all)
            shift 2
            ;;
          *)
            EchoE "ERROR: Invalid --instance value '${2}'. Must be one of: tv, anime, movie, xxx, all."
            exit 1
            ;;
        esac
        ;;
      --) shift; break ;;
      *) EchoE "Unknown option: $1"; ShowHelp; exit 1 ;;
    esac
  done
  return 0
}

#==============================================================================
# DefineUrlencodeFallback - Defines a fallback urlencode function
#------------------------------------------------------------------------------
# Description:
#   Defines a `urlencode` function using Python, Perl, or pure Bash if the
#   command is not available.
#
# Globals:
#   DEBUG - Flag for debug logging (RO)
#
# Arguments:
#   None
#
# Outputs:
#   Writes warning messages to stdout via `EchoW` for fallback method.
#   Writes debug messages to stdout via `EchoD` if DEBUG=1.
#
# Exit Codes:
#   0 - Success
#
# Side Effects:
#   Defines the `urlencode` function in the global scope.
#   Checks for `python3` and `perl` availability.
#==============================================================================
DefineUrlencodeFallback() {
  if declare -F urlencode &>/dev/null; then
    [[ ${DEBUG} -eq 1 ]] && EchoD "🛠 'urlencode' already defined, skipping fallback setup"
  elif command -v python3 &>/dev/null; then
    urlencode() {
      python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "$1"
    }
    EchoW "Defined fallback 'urlencode' using python3"
  elif command -v perl &>/dev/null; then
    urlencode() {
      perl -MURI::Escape -e 'print uri_escape($ARGV[0]);' "$1"
    }
    EchoW "Defined fallback 'urlencode' using perl"
  else
    urlencode() {
      local LANG=C
      local string="${1}"
      local length="${#string}"
      local i
      for (( i = 0; i < length; i++ )); do
        local c="${string:$i:1}"
        case "${c}" in
          [a-zA-Z0-9.~_-]) printf "%s" "${c}" ;;
          *) printf "%%%02X" "'${c}" ;;
        esac
      done
    }
    EchoW "Defined fallback 'urlencode' using pure bash"
  fi
  return 0
}

#==============================================================================
# DoDependency - Checks or installs dependencies
#------------------------------------------------------------------------------
# Description:
#   Verifies or installs required tools (curl, jq, date, urlencode) based on
#   dependency check mode (default, local, or install).
#
# Globals:
#   CHECK_DEPS_DEFAULT   - Flag for default dependency check (RO)
#   CHECK_DEPS_LOCAL     - Flag for local dependency fallback (RO)
#   CHECK_DEPS_INSTALL   - Flag for dependency installation (RO)
#
# Arguments:
#   None
#
# Outputs:
#   Writes informational messages to stdout via `Echo`.
#   Writes error messages to stdout via `EchoE` for missing tools.
#   Writes warning messages to stdout via `EchoW` for fallbacks.
#
# Exit Codes:
#   0 - Success
#   1 - Failure (missing tools in default mode)
#
# Side Effects:
#   Calls `DefineUrlencodeFallback` for local mode.
#   May attempt to install tools (placeholder logic).
#   Checks command availability.
#==============================================================================
DoDependency() {
  local -i missing=0
  local cmd=""
  
  if [[ ${CHECK_DEPS_INSTALL} -eq 1 ]]; then
    Echo "🔧 Forcing install of all required tools..."
    # Placeholder for EnsureDependency function (not provided in MY_LIBRARY.sh)
    Echo "✅ All required tools have been installed or handled."
  elif [[ ${CHECK_DEPS_LOCAL} -eq 1 ]]; then
    Echo "🔍 Checking tools and applying local fallback logic (no install)..."
    for cmd in curl jq date urlencode; do
      if command -v "${cmd}" &>/dev/null; then
        Echo "✔ '${cmd}' available"
      else
        case "${cmd}" in
          urlencode)
            DefineUrlencodeFallback
            ;;
          jq)
            EchoE "Cannot mimic 'jq'. Please install it manually."
            exit 1
            ;;
          *)
            EchoE "Cannot mimic missing command '${cmd}'."
            exit 1
            ;;
        esac
      fi
    done
    Echo "✅ Dependencies checked with local fallback logic applied."
  elif [[ ${CHECK_DEPS_DEFAULT} -eq 1 ]]; then
    Echo "🔍 Verifying dependencies only (no install, no fallback)..."
    missing=0
    for cmd in curl jq date urlencode; do
      if ! command -v "${cmd}" &>/dev/null; then
        EchoE "Missing required tool: ${cmd}"
        (( missing++ ))
      fi
    done
    if [[ ${missing} -gt 0 ]]; then
      EchoE "⛔ Missing ${missing} required tool(s). Use --check-deps-local or --check-deps-install."
      exit 1
    else
      Echo "✅ All required tools are available."
    fi
  fi
}

#==============================================================================
# RunJq - Executes jq command with error handling
#------------------------------------------------------------------------------
# Description:
#   Runs a jq command on the provided input, saving the input to a temporary file
#   for debugging. Captures errors and logs them, along with the input JSON if
#   debugging is enabled.
#
# Globals:
#   DEBUG - Flag for debug logging (RO)
#
# Arguments:
#   $1 - jq command to execute (e.g., '-r ".[] | .field"')
#   $2 - Optional input string (defaults to stdin if not provided)
#
# Outputs:
#   Writes jq output to stdout on success.
#   Writes error messages to stdout via `EchoE` if jq fails.
#   Writes debug messages to stdout via `EchoD` with input JSON if DEBUG=1.
#
# Exit Codes:
#   0 - Success (jq command executed successfully)
#   1 - Failure (jq command failed)
#
# Side Effects:
#   Creates temporary files for input and error output (cleaned up on exit).
#   Executes the jq command with the provided input.
#==============================================================================
RunJq() {
  local input_file=$(mktemp)
  local cmd="$1"
  
  echo "${2:-$(cat)}" > "${input_file}"
  if ! jq "${cmd}" "${input_file}" 2> "${input_file}.err"; then
    EchoE "jq failed: $(cat "${input_file}.err")"
    (( DEBUG )) && EchoD "Input JSON: $(cat "${input_file}")"
    rm -f "${input_file}" "${input_file}.err"
    return 1
  fi
  rm -f "${input_file}" "${input_file}.err"
}

#==============================================================================
# DoSetPriority - Sets high priority and force-starts TV torrents matching prefixes
#------------------------------------------------------------------------------
# Description:
#   Fetches torrents in the TV category, normalizes their names, matches them against
#   predefined prefixes, and sets high priority and force-start via qBittorrent API.
#   Includes verification of priority changes if enabled.
#
# Globals:
#   API_URL         - qBittorrent API URL for torrent info (RO)
#   CMD_FORCE       - API URL for force-starting torrents (RO)
#   CMD_PRIO        - API URL for setting top priority (RO)
#   DRY_RUN         - Flag to simulate actions (RO)
#   VERBOSE         - Flag for detailed output (RO)
#   DEBUG           - Flag for debug logging (RO)
#   VERIFY          - Flag to verify priority changes (RO)
#
# Arguments:
#   $1 - Instance name (e.g., 'tv')
#
# Outputs:
#   Writes informational messages to stdout via Echo, EchoE, EchoW, EchoD.
#   Writes debug logs if DEBUG is enabled.
#   Writes summary of torrents found, changed, and confirmed.
#
# Exit Codes:
#   0 - Success
#   1 - Failure (e.g., empty prefixes, API failure, invalid JSON)
#
# Side Effects:
#   Creates temporary files for processing (cleaned up on exit).
#   Modifies torrent priorities and states in qBittorrent if DRY_RUN=0.
#==============================================================================
DoSetPriority() {
local -r INSTANCE="$1"
  local -i torrents_confirmed=0
  local -i tv_torrent_count=0
  local -i torrents_found=0
  local -i torrents_changed=0
  local -i counter=0
  local -r temp_file=$(mktemp)
  local curl_output=""
  local raw_matches=""
  local matches=""
  local filtered_matches=""
  local hash_list=""
  local prefixes_json=""
  local updated_info=""
  local priority=""
  local normalized=""
  local debug_matches=""
  local hash=""
  local name=""
  local matched_prefix=""
  local force_response=""
  local prio_response=""

  declare -a NORMALIZED_PREFIXES=()
  declare -a processed_hashes=()
  declare -A hash_to_name=()
  
  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}| INSTANCE='${INSTANCE}'" 
  
  # Normalize PREFIXES: replace spaces, dashes, underscores, and colons with dots; reduce multiple dots to a single dot; trim leading/trailing dots; convert to lowercase
  case "${INSTANCE}" in
  tv) local -r PREFIXES=(
        'Murdoch.Mysteries' 'Silent.witness' 'death in paradise' 'paradise'
        'prime-target' 'the white lotus' 'the-eastern-gate' 'from'
        'dark matter' 'ghosts' 'strike' 'severance'
        '1923' 'daredevil' 'solo' 'Doom' 'the.bondsman' 'the.last.of.us'
      ) 
      ;;
  *) EchoE "Instance '${INSTANCE}' does not have any definition for high-priority"
     rm -f "${temp_file}"
     return 1
     ;;
  esac
  
  NORMALIZED_PREFIXES=()
  for prefix in "${PREFIXES[@]}"; do
    (( DEBUG>1 )) && EchoD "${FUNCNAME[0]}:${LINENO}| Original prefix: '${prefix}'"
    # Step 1: Replace spaces, dashes, underscores, and colons with dots
    normalized="${prefix}"
    normalized="${normalized// /.}"; (( DEBUG>1 )) && EchoD "After replacing spaces: '${normalized}'"
    normalized="${normalized//-/.}"; (( DEBUG>1 )) && EchoD "After replacing dashes: '${normalized}'"
    normalized="${normalized//_/.}"; (( DEBUG>1 )) && EchoD "After replacing underscores: '${normalized}'"
    normalized="${normalized//:/.}"; (( DEBUG>1 )) && EchoD "After replacing colons: '${normalized}'"
    # Step 2: Replace multiple dots with a single dot
    while [[ "${normalized}" =~ \.\.+ ]]; do
      normalized="${normalized//../.}"
    done
    (( DEBUG>1 )) && EchoD "${FUNCNAME[0]}:${LINENO}| After replacing multiple dots: '${normalized}'"
    # Step 3: Trim leading and trailing dots
    normalized="${normalized#.}"
    (( DEBUG>1 )) && EchoD "${FUNCNAME[0]}:${LINENO}| After trimming leading dots: '${normalized}'"
    normalized="${normalized%.}"
    (( DEBUG>1 )) && EchoD "${FUNCNAME[0]}:${LINENO}| After trimming trailing dots: '${normalized}'"
    # Step 4: Convert to lowercase
    normalized="${normalized,,}"
    (( DEBUG>1 )) && EchoD "${FUNCNAME[0]}:${LINENO}| After converting to lowercase: '${normalized}'"
    # Step 5: Skip empty results
    [[ -z "${normalized}" ]] && { EchoW "Skipping empty prefix after normalization (original: '${prefix}')"; continue; }
    NORMALIZED_PREFIXES+=("${normalized}")
  done
  
  # Final check for any empty elements in the array
  for i in "${!NORMALIZED_PREFIXES[@]}"; do
    if [[ -z "${NORMALIZED_PREFIXES[$i]}" ]]; then
      EchoW "Found empty prefix in NORMALIZED_PREFIXES at index ${i}. Removing it."
      unset "NORMALIZED_PREFIXES[$i]"
    fi
  done
  # Reindex the array to remove gaps
  NORMALIZED_PREFIXES=("${NORMALIZED_PREFIXES[@]}")

  # Log the normalized prefixes for debugging
  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}| NORMALIZED_PREFIXES: ${NORMALIZED_PREFIXES[*]}"

  # Pass the prefixes to jq as a JSON array for debugging purposes
  prefixes_json=$(printf '%s\n' "${NORMALIZED_PREFIXES[@]}" | jq -R . | jq -s .)
  (( DEBUG )) && EchoD "prefixes_json: ${prefixes_json}"

  Echo "🎯 Filtering TV torrents for priority match..."

  # Step 1: Fetch torrent info and filter for TV category
  curl_output=$(curl --silent "${API_URL}")
  if [[ -z "${curl_output}" ]]; then
    EchoE "Failed to fetch torrent info from ${API_URL}. Empty response."
    rm -f "${temp_file}"
    return 1
  fi

  # Save the curl output to a file for debugging and log it
  (( DEBUG>1 )) && EchoD "${FUNCNAME[0]}:${LINENO}| Raw torrent info saved to ${temp_file}.raw"
  (( DEBUG>1 )) && printf '%s\n' "${curl_output}" > "${temp_file}.raw"
  (( DEBUG>1 )) && EchoD "${FUNCNAME[0]}:${LINENO}| Raw curl_output: ${curl_output}"

  # Count total torrents in the TV category
  tv_torrent_count=$(echo "${curl_output}" | jq -r '[.[] | select(.category | ascii_downcase == "tv")] | length')
  if [[ $? -ne 0 ]]; then
    EchoE "Failed to count TV torrents. Check input JSON or jq version."
    rm -f "${temp_file}"
    return 1
  fi
  (( DEBUG )) && EchoD "${FUNCNAME[0]}:${LINENO}| Total torrents in TV category: ${tv_torrent_count}"

  # Step 2: Fetch torrent info and filter for TV category
  #raw_matches=$(echo "${curl_output}" | jq -r '.[] | select(.category | ascii_downcase == "tv") | [.hash, .name] | @tsv')
  raw_matches=$(RunJq '-r ".[] | select(.category | ascii_downcase == \"tv\") | [.hash, .name] | @tsv"' "${curl_output}")
  if [[ $? -ne 0 ]]; then
    EchoE "Failed to filter TV torrents. Check input JSON or jq version."
    rm -f "${temp_file}"
    return 1
  fi

  # Log raw_matches for debugging
  (( DEBUG )) && EchoD "raw_matches: ${raw_matches}"

  # Check if there are any matches
  if [[ -z "${raw_matches}" ]]; then
    Echo "⚠️ No torrents found in the TV category."
    Echo "📊 [DRY-RUN] Summary: ${torrents_found} torrents found, ${torrents_changed} would be changed"
    rm -f "${temp_file}"
    return 1
  fi

  # Step 3: Normalize torrent names in Bash and perform prefix matching
  Echo "🎯 Normalize torrent names in Bash and perform prefix matching..."
  matches=""
  counter=0
  while IFS=$'\t' read -r hash name; do
    # Log the raw hash and name
    (( DEBUG>1 )) && EchoD "Processing torrent - Hash: ${hash}, Name: ${name}"

    # Skip if any field is missing
    [[ -z "${hash}" || -z "${name}" ]] && { (( DEBUG>1 )) && EchoD "Skipping torrent due to missing hash or name"; continue; }

    # Normalize the torrent name in Bash (same as prefixes)
    normalized="${name}"
    normalized="${normalized// /.}"
    normalized="${normalized//-/.}"
    normalized="${normalized//_/.}"
    normalized="${normalized//:/.}"
    while [[ "${normalized}" =~ \.\.+ ]]; do
      normalized="${normalized//../.}"
    done
    normalized="${normalized#.}"
    normalized="${normalized%.}"
    normalized="${normalized,,}"

    # Log the normalized name
    (( DEBUG>1 )) && EchoD "Torrent: ${name}, Normalized: ${normalized}"

    # Skip if normalized name is empty
    [[ -z "${normalized}" ]] && { (( DEBUG>1 )) && EchoD "Skipping torrent due to empty normalized name"; continue; }

    # Add to matches with hash, name, and normalized name using actual tab characters
    matches+="${hash}"$'\t'"${name}"$'\t'"${normalized}"$'\n'
    (( counter++ ))
  done <<< "${raw_matches}"
  Echo "📊 Matches normalized: ${counter}"

  # Log the matches variable for debugging, replacing tabs with <TAB> for readability
  (( DEBUG )) && {
    EchoD "Replacing tabs with <TAB>..."
    debug_matches=$(echo "${matches}" | awk '{ gsub(/\t/, "<TAB>"); print }')
    EchoD "matches (after Step 3): [${debug_matches}]"
  }

  # Step 4: Perform prefix matching with a relaxed pattern
  Echo "🎯 Perform prefix matching with a relaxed pattern..."
  filtered_matches=""
  counter=0
  while IFS=$'\t' read -r hash name normalized; do
    # Skip if any field is missing
    [[ -z "${hash}" || -z "${name}" || -z "${normalized}" ]] && { (( DEBUG>1 )) && EchoD "Skipping torrent due to missing fields"; continue; }

    # Check if the normalized name starts with any prefix
    matched_prefix=""
    for prefix in "${NORMALIZED_PREFIXES[@]}"; do
      case "${normalized}" in
      "${prefix}"*)
         matched_prefix="${prefix}"
        (( counter++ ))
        (( DEBUG>1 )) && EchoD "Matched prefix '${prefix}' in normalized name '${normalized}'"
        break
        ;;
      esac
    done

    # Log if no match was found
    if [[ "${matched}" != true ]]; then
      (( DEBUG>1 )) && EchoD "No prefix match for normalized name '${normalized}'"
      continue
    fi

    # If matched, add to the filtered matches list using actual tab characters
    if (( DEBUG>1 )); then
      filtered_matches+="${hash}"$'\t'"${name}"$'\t'"${normalized}"$'\t'"${matched_prefix}"$'\n'
    else
      filtered_matches+="${hash}"$'\t'"${name}"$'\n'
    fi
  done <<< "${matches}"
  Echo "📊 Prefixes matched: ${counter}"

  # Log the filtered_matches variable for debugging, replacing tabs with <TAB> for readability
  (( DEBUG )) && EchoD "filtered_matches (after Step 4): [${filtered_matches//$'\t'/<TAB>}]"

  # Step 5: Collect all matching torrent hashes
  if (( DEBUG )); then
    # In debug mode, we have extra fields (normalized name and matched prefix)
    while IFS=$'\t' read -r hash name normalized matched_prefix; do
      [[ -z "${hash}" ]] && continue
      ((torrents_found++))
      (( DEBUG>1 )) && EchoD "Matched - Normalized name: ${normalized}, Matched prefix: ${matched_prefix}"
      (( VERBOSE )) && Echo "[SET-PRIORITY] ${hash} [${name}] (Matched prefix: ${matched_prefix})"
      processed_hashes+=("${hash}")
      hash_to_name["${hash}"]="${name}"
      if [[ -z "${hash_list}" ]]; then
        hash_list="${hash}"
      else
        hash_list="${hash_list}|${hash}"
      fi
    done <<< "${filtered_matches}"
  else
    while IFS=$'\t' read -r hash name; do
      [[ -z "${hash}" ]] && continue
      ((torrents_found++))
      (( VERBOSE )) && Echo "[SET-PRIORITY] ${hash} [${name}]"
      processed_hashes+=("${hash}")
      hash_to_name["${hash}"]="${name}"
      if [[ -z "${hash_list}" ]]; then
        hash_list="${hash}"
      else
        hash_list="${hash_list}|${hash}"
      fi
    done <<< "${filtered_matches}"
  fi

  # Apply priority changes in a single API call if not in dry-run mode
  if [[ ${DRY_RUN} -eq 0 && -n "${hash_list}" ]]; then
    Echo "🚀 Moving ${torrents_found} torrents to the top of the queue and forcing start..."

    # Better: POST version
    if (( DEBUG )); then
      EchoD "${FUNCNAME[0]}:${LINENO}| curl -X POST --data \"hashes=${hash_list}&value=true\" ${CMD_FORCE}"
      force_response=$(curl --verbose -X POST \
        --data "hashes=${hash_list}&value=true" \
        "${CMD_FORCE}")
    else
      force_response=$(curl --silent -X POST \
        --data "hashes=${hash_list}&value=true" \
        "${CMD_FORCE}")
    fi
    if [[ $? -ne 0 ]]; then
      EchoE "Failed to force-start torrents. Response: ${force_response}"
      rm -f "${temp_file}"
      return 1
    fi

    # Move torrents to the top of the queue using topPrio
    if (( DEBUG )); then
      EchoD "curl -X POST --data \"hashes=${hash_list}\" ${API_URL%/*}/topPrio"
      prio_response=$(curl --verbose -X POST --data "hashes=${hash_list}" "${API_URL%/*}/topPrio")
    else
      prio_response=$(curl --silent -X POST --data "hashes=${hash_list}" "${API_URL%/*}/topPrio")
    fi
    if [[ $? -ne 0 ]]; then
      EchoE "Failed to move torrents to top priority. Response: ${prio_response}"
      rm -f "${temp_file}"
      return 1
    fi

    ((torrents_changed=${torrents_found}))
  fi

  # Verify priority changes if not in dry-run mode
  if (( VERIFY )); then
    if [[ ${DRY_RUN} -eq 0 && ${#processed_hashes[@]} -gt 0 ]]; then
      Echo "⏳ Waiting 5 seconds for qBittorrent to process priority changes..."
      sleep 5
      Echo "🔍 Verifying priority changes..."
      # Fetch updated torrent info
      updated_info=$(curl --silent "${API_URL}")
      for hash in "${processed_hashes[@]}"; do
        priority=$(echo "${updated_info}" | jq -r ".[] | select(.hash == \"${hash}\") | .priority")
        if [[ $? -ne 0 ]]; then
          EchoW "Failed to verify priority for torrent ${hash}"
          continue
        fi
        if [[ "${priority}" -le "${torrents_found}" ]]; then
          ((torrents_confirmed++))
        else
           EchoW "Torrent ${hash} [${hash_to_name[${hash}]}] priority is ${priority} (expected <= ${torrents_found})"
        fi
      done
    fi
  fi

  # Output summary
  if [[ ${DRY_RUN} -eq 1 ]]; then
    Echo "📊 [DRY-RUN] Summary: ${torrents_found} torrents found, ${torrents_changed} would be changed"
  else
    Echo "📊 Summary: ${torrents_found} torrents found, ${torrents_changed} moved to top, ${torrents_confirmed} confirmed in correct order"
  fi
  rm -f "${temp_file}"
}

#==============================================================================
# DoDelete - Deletes torrents based on patterns
#------------------------------------------------------------------------------
# Description:
#   Reads patterns from a file, matches torrents against them, and deletes
#   matching torrents using the qBittorrent API. Backs up .torrent files and
#   verifies deletions.
#
# Globals:
#   API_URL            - qBittorrent API URL for torrent info (RO)
#   QBT_HOST           - Base URL of qBittorrent instance (RO)
#   CMD_DELETE         - API URL for deleting torrents (RO)
#   PATTERN_FILE       - Path to pattern file (RO)
#   DRY_RUN            - Flag to simulate actions (RO)
#   LIMIT              - Maximum number of torrents to process (RO)
#   REMOVE_FILES       - Flag to delete torrent files (RO)
#   IGNORE_NOT_DELETED - Flag to skip retrying undeleted torrents (RO)
#
# Arguments:
#   $1 - Instance name (e.g., 'tv')
#
# Outputs:
#   Writes informational messages to stdout via `Echo`.
#   Writes error messages to stdout via `EchoE` for file or API issues.
#   Writes summary of torrents targeted and deleted.
#
# Exit Codes:
#   0 - Success
#   1 - Failure (pattern file missing, API failure)
#
# Side Effects:
#   Makes curl requests to the qBittorrent API.
#   Creates backup directory and .torrent files.
#   Deletes torrents and optionally their files in qBittorrent.
#   Calls `RetryDeletePrompt` for verification.
#==============================================================================
DoDelete() {
local -a patterns=()
  local -a hashes_to_delete=()
  local -i torrents_deleted=0
  local -r backup_dir="/cygdrive/f/p_qBittorrent/.cache/BackupRemoval/$(date '+%F_%H-%M-%S')"
  local torrent_info=""
  local hash=""
  local torrent_name=""
  
  Echo "🗑️ Deleting torrents based on patterns in ${PATTERN_FILE}..."

  # Read patterns from file
  if [[ ! -f "${PATTERN_FILE}" ]]; then
    EchoE "Pattern file ${PATTERN_FILE} not found."
    return 1
  fi

  while IFS= read -r line; do
    # Skip empty lines and comments
    [[ -z "${line}" || "${line}" =~ ^# ]] && continue
    # Remove leading/trailing whitespace
    line="${line#"${line%%[![:space:]]*}"}"
    line="${line%"${line##*[![:space:]]}"}"
    [[ -z "${line}" ]] && continue
    patterns+=("${line}")
  done < "${PATTERN_FILE}"

  if [[ ${#patterns[@]} -eq 0 ]]; then
    Echo "No patterns found in ${PATTERN_FILE}."
    return 0
  fi

  Echo "🔍 Found ${#patterns[@]} patterns to match for deletion..."

  # Fetch torrent info
  torrent_info=$(curl --silent "${API_URL}")
  if [[ -z "${torrent_info}" ]]; then
    EchoE "Failed to fetch torrent info."
    return 1
  fi

  # Match torrents against patterns
  for pattern in "${patterns[@]}"; do
    while IFS= read -r hash; do
      [[ -n "${hash}" ]] && hashes_to_delete+=("${hash}")
    done < <(RunJq "-r --arg pat \"${pattern}\" '.[] | select(.name | test(\$pat)) | .hash'" "${torrent_info}")
    if [[ $? -ne 0 ]]; then
      EchoE "Failed to match torrents against pattern '${pattern}'."
      return 1
    fi
  done

  # Remove duplicates
  local -A unique_hashes
  for hash in "${hashes_to_delete[@]}"; do
    unique_hashes["${hash}"]=1
  done
  hashes_to_delete=("${!unique_hashes[@]}")

  if [[ ${#hashes_to_delete[@]} -eq 0 ]]; then
    Echo "No torrents matched the deletion patterns."
    return 0
  fi

  Echo "🗑️ Found ${#hashes_to_delete[@]} torrents to delete..."

  # Limit the number of torrents to delete if specified
  if [[ ${LIMIT} -gt 0 && ${#hashes_to_delete[@]} -gt ${LIMIT} ]]; then
    Echo "📏 Limiting to ${LIMIT} torrents as specified..."
    hashes_to_delete=("${hashes_to_delete[@]:0:${LIMIT}}")
  fi

  # Backup and delete torrents
  if [[ ${DRY_RUN} -eq 0 ]]; then
    mkdir -p "${backup_dir}"
  fi

  for hash in "${hashes_to_delete[@]}"; do
    torrent_name=$(echo "${torrent_info}" | jq -r --arg h "${hash}" '.[] | select(.hash == $h) | .name')
    if [[ $? -ne 0 ]]; then
      EchoE "Failed to retrieve name for torrent ${hash}."
      continue
    fi
    Echo "[DELETE] ${hash} [${torrent_name}]"

    if [[ ${DRY_RUN} -eq 0 ]]; then
      # Backup .torrent file
      ExportTorrentFile "${hash}" "${backup_dir}/${hash}.torrent"

      # Delete the torrent
      DeleteTorrent "${hash}"
      if [[ $? -eq 0 ]]; then
        ((torrents_deleted++))
      fi
    fi
  done

  # Verify deletion if not in dry-run mode
  if [[ ${DRY_RUN} -eq 0 && ${IGNORE_NOT_DELETED} -eq 0 ]]; then
    RetryDeletePrompt hashes_to_delete[@]
  fi

  # Output summary
  if [[ ${DRY_RUN} -eq 1 ]]; then
    Echo "📊 [DRY-RUN] Summary: ${#hashes_to_delete[@]} torrents would be deleted"
  else
    Echo "📊 Summary: ${#hashes_to_delete[@]} torrents targeted, ${torrents_deleted} deleted"
  fi
}

#==============================================================================
# DoRecheck - Rechecks errored torrents
#------------------------------------------------------------------------------
# Description:
#   Identifies torrents in an "error" state for a specified instance and issues
#   recheck commands via the qBittorrent API.
#
# Globals:
#   QBT_INSTANCES    - Mapping of instance names to ports (RO)
#   DRY_RUN          - Flag to simulate actions (RO)
#   VERBOSE          - Flag for detailed output (RO)
#
# Arguments:
#   $1 - Instance name (e.g., 'tv' or 'all')
#
# Outputs:
#   Writes informational messages to stdout via `Echo`.
#   Writes error messages to stdout via `EchoE` for API failures.
#   Writes summary of torrents found and rechecked.
#
# Exit Codes:
#   0 - Success
#
# Side Effects:
#   Makes curl requests to the qBittorrent API.
#   Initiates recheck operations in qBittorrent if DRY_RUN=0.
#==============================================================================
DoRecheck() {
local -r SELECTED_INSTANCE="${INSTANCE_NAME}"
  local -a instances=()
  local -i total_rechecked=0
  local instance=""
  local port=""
  local api_url=""
  local torrent_info=""
  local -a errored_hashes=()
  local hash=""
  local torrent_name=""
    
  Echo "🔄 Rechecking errored torrents for instance ${SELECTED_INSTANCE}..."
  if [[ "${SELECTED_INSTANCE}" == "all" ]]; then
    instances=("${!QBT_INSTANCES[@]}")
  else
    instances=("${SELECTED_INSTANCE}")
  fi

  for instance in "${instances[@]}"; do
    port=${QBT_INSTANCES["${instance}"]}
    api_url="http://localhost:${port}/api/v2/torrents/info"
    Echo "🔎 Checking instance ${instance} at ${api_url}..."

    torrent_info=$(curl --silent "${api_url}")
    if [[ -z "${torrent_info}" ]]; then
      EchoE "Failed to fetch torrent info for instance ${instance}."
      continue
    fi

    errored_hashes=()
    while IFS= read -r hash; do
      [[ -n "${hash}" ]] && errored_hashes+=("${hash}")
    done < <(echo "${torrent_info}" | jq -r '.[] | select(.state == "error") | .hash')
    if [[ $? -ne 0 ]]; then
      EchoE "Failed to identify errored torrents for instance ${instance}."
      continue
    fi

    if [[ ${#errored_hashes[@]} -eq 0 ]]; then
      Echo "✅ No errored torrents found in instance ${instance}."
      continue
    fi

    Echo "🔄 Found ${#errored_hashes[@]} errored torrents in instance ${instance}..."
    for hash in "${errored_hashes[@]}"; do
      torrent_name=$(echo "${torrent_info}" | jq -r --arg h "${hash}" '.[] | select(.hash == $h) | .name')
      if [[ $? -ne 0 ]]; then
        EchoE "Failed to retrieve name for torrent ${hash}."
        continue
      fi
      Echo "[RECHECK] ${hash} [${torrent_name}]"

      if [[ ${DRY_RUN} -eq 0 ]]; then
        curl --silent --fail -X POST -H "Referer: http://localhost:${port}" --data "hashes=${hash}" "http://localhost:${port}/api/v2/torrents/recheck"
        if [[ $? -eq 0 ]]; then
          ((total_rechecked++))
        fi
      fi
    done
  done

  # Output summary
  if [[ ${DRY_RUN} -eq 1 ]]; then
    Echo "📊 [DRY-RUN] Summary: ${#errored_hashes[@]} errored torrents would be rechecked"
  else
    Echo "📊 Summary: ${#errored_hashes[@]} errored torrents found, ${total_rechecked} rechecked"
  fi
}

#==============================================================================
# MoveCompletedTorrents - Moves completed torrents to final directory
#------------------------------------------------------------------------------
# Description:
#   Identifies completed torrents not in their final directory and moves them
#   using the qBittorrent API setLocation endpoint.
#
# Globals:
#   QBT_INSTANCES    - Mapping of instance names to ports (RO)
#   FINAL_DIRS       - Mapping of instance names to final directories (RO)
#   DRY_RUN          - Flag to simulate actions (RO)
#   DEBUG            - Flag for debug logging (RO)
#   VERBOSE          - Flag for detailed output (RO)
#
# Arguments:
#   None (uses global SELECTED_INSTANCE)
#
# Outputs:
#   Writes informational messages to stdout via `Echo`.
#   Writes error messages to stdout via `EchoE` for API failures.
#   Writes warning messages to stdout via `EchoW` for move failures.
#   Writes debug messages to stdout via `EchoD` if DEBUG=1.
#   Writes summary of torrents found and moved.
#
# Exit Codes:
#   0 - Success
#
# Side Effects:
#   Makes curl requests to the qBittorrent API.
#   Modifies torrent save paths in qBittorrent if DRY_RUN=0.
#   Checks directory existence using `cygpath`.
#==============================================================================
MoveCompletedTorrents() {
local -a instances=()
  local -i grand_total_moved=0
  local instance=""
  local port=""
  local final_dir=""
  local api_url=""
  local torrent_info=""
  local -a completed_hashes=()
  local hash=""
  local name=""
  local save_path=""
  local win_final_dir=""
  local -i moved=0
  local hash_list=""
  local move_response=""
  declare -A hash_to_name=()
  declare -A hash_to_save_path=()
  
  Echo "🚚 Moving completed torrents for instance ${SELECTED_INSTANCE}..."
  if [[ "${SELECTED_INSTANCE}" == "all" ]]; then
    instances=("${!QBT_INSTANCES[@]}")
  else
    instances=("${SELECTED_INSTANCE}")
  fi

  for instance in "${instances[@]}"; do
    port=${QBT_INSTANCES["${instance}"]}
    final_dir=${FINAL_DIRS["${instance}"]}
    api_url="http://localhost:${port}/api/v2/torrents/info"
    Echo "🔎 Checking completed torrents in instance '${instance}' at ${api_url}..."

    torrent_info=$(curl --silent "${api_url}")
    if [[ -z "${torrent_info}" ]]; then
      EchoE "Failed to fetch torrent info for instance '${instance}'."
      continue
    fi

    completed_hashes=()
    hash_to_name=()
    hash_to_save_path=()

    while IFS=$'\t' read -r hash name save_path; do
      [[ -n "${hash}" && -n "${name}" ]] && {
        completed_hashes+=("${hash}")
        hash_to_name["${hash}"]="${name}"
        hash_to_save_path["${hash}"]="${save_path}"
      }
    done < <(RunJq "-r --arg dest \"${final_dir}\" '.[] | select(.progress == 1 and (.save_path | test(\$dest) | not) and (.state != \"moving\")) | [.hash, .name, .save_path] | @tsv'" "${torrent_info}")
    if [[ $? -ne 0 ]]; then
      EchoE "Failed to identify completed torrents for instance '${instance}'."
      continue
    fi

    if [[ ${#completed_hashes[@]} -eq 0 ]]; then
      Echo "✅ No completed torrents to move in instance '${instance}'."
      continue
    fi

    Echo "🚚 Found ${#completed_hashes[@]} completed torrents to move in instance '${instance}'..."
    moved=0
    hash_list=""

    for hash in "${completed_hashes[@]}"; do
      name="${hash_to_name[${hash}]}"
      save_path="${hash_to_save_path[${hash}]}"
      win_final_dir=$(cygpath -w "${final_dir}" | sed 's/\\/\\\\/g')

      # Check if source directory exists
      if [[ ! -d "${save_path}" ]]; then
        EchoW "Torrent ${hash} [${name}] is in a non-existent directory: ${save_path}"
        continue
      fi

      (( DEBUG )) && EchoD "Torrent ${hash} [${name}] current save_path: ${save_path}"
      Echo "[MOVE] ${hash} [${name}] ${save_path} → ${win_final_dir}"

      if [[ -z "${hash_list}" ]]; then
        hash_list="${hash}"
      else
        hash_list="${hash_list}|${hash}"
      fi
    done
      
    if [[ ${DRY_RUN} -eq 0 && -n "${hash_list}" ]]; then
      if (( DEBUG )); then
        EchoD "curl -X POST --data-urlencode \"hashes=${hash_list}\" --data-urlencode \"location=${final_dir}\" http://localhost:${port}/api/v2/torrents/setLocation"
        move_response=$(curl --verbose -X POST -H "Referer: http://localhost:${port}" \
          --data-urlencode "hashes=${hash_list}" \
          --data-urlencode "location=${final_dir}" \
          "http://localhost:${port}/api/v2/torrents/setLocation")
      else
        move_response=$(curl --silent -X POST -H "Referer: http://localhost:${port}" \
          --data-urlencode "hashes=${hash_list}" \
          --data-urlencode "location=${final_dir}" \
          "http://localhost:${port}/api/v2/torrents/setLocation")
      fi
      if [[ $? -eq 0 ]]; then
        moved=${#completed_hashes[@]}
      else
        EchoW "Failed to move torrents to ${win_final_dir}. Response: ${move_response}"
        moved=0
      fi
    fi

    if [[ ${DRY_RUN} -eq 1 ]]; then
      Echo "📊 [DRY-RUN] Instance '${instance}': ${#completed_hashes[@]} completed torrents would be moved"
    else
      Echo "📊 Instance '${instance}': ${#completed_hashes[@]} found, ${moved} moved"
    fi

    (( grand_total_moved += moved ))
  done

  if [[ ${DRY_RUN} -eq 0 ]]; then
    Echo "📦 Total moved across all instances: ${grand_total_moved}"
  fi
}

#==============================================================================
# RemoveTorrentsWithMalformedPath - Deletes torrents with invalid save paths
#------------------------------------------------------------------------------
# Description:
#   Identifies torrents with non-existent save paths and deletes them without
#   removing associated files.
#
# Globals:
#   QBT_INSTANCES    - Mapping of instance names to ports (RO)
#   DRY_RUN          - Flag to simulate actions (RO)
#   VERBOSE          - Flag for detailed output (RO)
#
# Arguments:
#   None (uses global SELECTED_INSTANCE)
#
# Outputs:
#   Writes informational messages to stdout via `Echo`.
#   Writes warning messages to stdout via `EchoW` for invalid paths.
#
# Exit Codes:
#   0 - Success
#
# Side Effects:
#   Makes curl requests to the qBittorrent API.
#   Deletes torrents in qBittorrent if DRY_RUN=0.
#   Converts Windows paths to Cygwin paths for validation.
#==============================================================================
RemoveTorrentsWithMalformedPath() {
local -a instances=()
  local instance=""
  local port=""
  local api_url=""
  local torrent_info=""
  local -i removed=0
  local hash=""
  local name=""
  local save_path=""
  local cyg_path=""
  
  Echo "🧹 Removing torrents with malformed or missing paths for instance '${SELECTED_INSTANCE}'..."
  if [[ "${SELECTED_INSTANCE}" == "all" ]]; then
    instances=("${!QBT_INSTANCES[@]}")
  else
    instances=("${SELECTED_INSTANCE}")
  fi

  for instance in "${instances[@]}"; do
    port="${QBT_INSTANCES[${instance}]}"
    api_url="http://localhost:${port}/api/v2/torrents/info"
    torrent_info=$(curl --silent "${api_url}")

    if [[ -z "${torrent_info}" ]]; then
      EchoE "Failed to fetch torrent info for instance ${instance}."
      continue
    fi

    removed=0
    while IFS=$'\t' read -r hash name save_path; do
      [[ -z "${hash}" || -z "${save_path}" ]] && continue
      if [[ -z "${save_path}" || ! "${save_path}" =~ ^[A-Za-z]:\\ ]]; then
        EchoW "Invalid save_path for torrent ${hash} [${name}]: '${save_path}'"
        continue
      fi
      cyg_path="${save_path//\\//}"
      cyg_path="/cygdrive/${cyg_path:0:1}/${cyg_path:3}"
      if [[ ! -d "${cyg_path}" ]]; then
        EchoW "[DELETE] ${hash} [${name}] save_path '${save_path}' does not exist"
        if [[ ${DRY_RUN} -eq 0 ]]; then
          curl --silent --get --data-urlencode "hashes=${hash}" "http://localhost:${port}/api/v2/torrents/delete?deleteFiles=false"
          ((removed++))
        fi
      fi
    done < <(RunJq '-r ".[] | [.hash, .name, .save_path] | @tsv"' "${torrent_info}")
    if [[ $? -ne 0 ]]; then
      EchoE "Failed to process torrents for instance '${instance}'."
      continue
    fi

    Echo "🗑️ Instance '${instance}': ${removed} torrents removed with invalid paths"
  done
}

#==============================================================================
# SetCorrectCategory - Fixes torrent categories based on instance
#------------------------------------------------------------------------------
# Description:
#   Updates torrent categories to match the expected category for the instance
#   (e.g., 'TV' for tv instance).
#
# Globals:
#   QBT_INSTANCES    - Mapping of instance names to ports (RO)
#   DRY_RUN          - Flag to simulate actions (RO)
#   VERBOSE          - Flag for detailed output (RO)
#
# Arguments:
#   None (uses global SELECTED_INSTANCE)
#
# Outputs:
#   Writes informational messages to stdout via `Echo`.
#   Writes warning messages to stdout via `EchoW` for unknown instances.
#
# Exit Codes:
#   0 - Success
#
# Side Effects:
#   Makes curl requests to the qBittorrent API.
#   Modifies torrent categories in qBittorrent if DRY_RUN=0.
#==============================================================================
SetCorrectCategory() {
local -a instances=()
  local instance=""
  local port=""
  local expected_cat=""
  local torrent_info=""
  local -i changed=0
  local hash=""
  local name=""
  local category=""
  
  Echo "🛠️ Fixing torrent categories for instance '${SELECTED_INSTANCE}'..."
  if [[ "${SELECTED_INSTANCE}" == "all" ]]; then
    instances=("${!QBT_INSTANCES[@]}")
  else
    instances=("${SELECTED_INSTANCE}")
  fi

  for instance in "${instances[@]}"; do
    port="${QBT_INSTANCES[${instance}]}"
    expected_cat=""
    case "${instance}" in
      anime) expected_cat="Anime" ;;
      xxx) expected_cat="XXX" ;;
      tv) expected_cat="TV" ;;
      movie) expected_cat="Movie" ;;
      *) EchoW "Unknown instance '${instance}', skipping." ; continue ;;
    esac

    torrent_info=$(curl --silent "http://localhost:${port}/api/v2/torrents/info")
    if [[ -z "${torrent_info}" ]]; then
      EchoE "Failed to fetch torrents for instance ${instance}."
      continue
    fi

    changed=0
    while IFS=$'\t' read -r hash name category; do
      if [[ "${category}" != "${expected_cat}" ]]; then
        ((VERBOSE)) && Echo "[FIX] ${hash} [${name}] category '${category}' → '${expected_cat}'"
        ((DRY_RUN == 0)) && curl --silent --get --data-urlencode "hashes=${hash}" --data-urlencode "category=${expected_cat}" "http://localhost:${port}/api/v2/torrents/setCategory"
        ((changed++))
        if ((VERBOSE == 0 && changed % 50 == 0)); then
          Echo "🔁 Applied ${changed} category fixes so far..."
        fi
      fi
    done < <(RunJq '-r ".[] | [.hash, .name, .category] | @tsv"' "${torrent_info}")
    if [[ $? -ne 0 ]]; then
      EchoE "Failed to process torrents for instance '${instance}'."
      continue
    fi

    Echo "✅ Instance '${instance}': ${changed} torrent categories fixed"
  done
}

#==============================================================================
# MoveIncompleteTorrentsToDownloadFolder - Moves incomplete torrents to temp folder
#------------------------------------------------------------------------------
# Description:
#   Ensures incomplete torrents are in the correct temporary download directory
#   for the instance using the qBittorrent API setLocation endpoint.
#
# Globals:
#   QBT_INSTANCES    - Mapping of instance names to ports (RO)
#   TEMP_DIRS        - Mapping of instance names to temp directories (RO)
#   DRY_RUN          - Flag to simulate actions (RO)
#   DEBUG            - Flag for debug logging (RO)
#
# Arguments:
#   None (uses global SELECTED_INSTANCE)
#
# Outputs:
#   Writes informational messages to stdout via `Echo`.
#   Writes error messages to stdout via `EchoE` for API failures.
#   Writes warning messages to stdout via `EchoW` for move failures.
#   Writes debug messages to stdout via `EchoD` if DEBUG=1.
#
# Exit Codes:
#   0 - Success
#
# Side Effects:
#   Makes curl requests to the qBittorrent API.
#   Modifies torrent save paths in qBittorrent if DRY_RUN=0.
#   Checks directory mappings using `cygpath`.
#==============================================================================
MoveIncompleteTorrentsToDownloadFolder() {
local -a instances=()
  local -i total_moved=0
  local instance=""
  local port=""
  local api_url=""
  local expected_dir=""
  local torrent_info=""
  local -i moved=0
  local hash=""
  local name=""
  local save_path=""
  local state=""
  local win_expected_dir=""
  local hash_list=""
  local move_response=""
  
  Echo "📦 Ensuring incomplete torrents are in the correct temp folder for instance ${SELECTED_INSTANCE}..."
  if [[ "${SELECTED_INSTANCE}" == "all" ]]; then
    instances=("${!QBT_INSTANCES[@]}")
  else
    instances=("${SELECTED_INSTANCE}")
  fi

  for instance in "${instances[@]}"; do
    port=${QBT_INSTANCES["${instance}"]}
    api_url="http://localhost:${port}/api/v2/torrents/info"
    expected_dir="${TEMP_DIRS[${instance}]}"
    [[ -z "${expected_dir}" ]] && { EchoW "No temp download dir defined for instance '${instance}'"; continue; }

    Echo "🔍 Scanning incomplete torrents in instance '${instance}' at ${api_url}..."

    torrent_info=$(curl --silent "${api_url}")
    if [[ -z "${torrent_info}" ]]; then
      EchoE "Failed to fetch torrent info for instance '${instance}'."
      continue
    fi

    moved=0
    hash_list=""
    while IFS=$'\t' read -r hash name save_path state; do
      win_expected_dir=$(cygpath -w "${expected_dir}" | sed 's/\\/\\\\/g')

      Echo "[MOVE] ${hash} [${name}] ${save_path} → ${win_expected_dir}"

      if [[ -z "${hash_list}" ]]; then
        hash_list="${hash}"
      else
        hash_list="${hash_list}|${hash}"
      fi
    done < <(RunJq "-r --arg temp \"${expected_dir}\" '.[] | select(.progress < 1 and .state != \"moving\" and .state != \"checkingUP\" and .state != \"checkingDL\" and (.save_path | test(\$temp) | not)) | [.hash, .name, .save_path, .state] | @tsv'" "${torrent_info}")
    if [[ $? -ne 0 ]]; then
      EchoE "Failed to process incomplete torrents for instance '${instance}'."
      continue
    fi

    if [[ ${DRY_RUN} -eq 0 && -n "${hash_list}" ]]; then
      if (( DEBUG )); then
        EchoD "curl -X POST --data-urlencode \"hashes=${hash_list}\" --data-urlencode \"location=${expected_dir}\" http://localhost:${port}/api/v2/torrents/setLocation"
        move_response=$(curl --verbose -X POST -H "Referer: http://localhost:${port}" \
          --data-urlencode "hashes=${hash_list}" \
          --data-urlencode "location=${expected_dir}" \
          "http://localhost:${port}/api/v2/torrents/setLocation")
      else
        move_response=$(curl --silent -X POST -H "Referer: http://localhost:${port}" \
          --data-urlencode "hashes=${hash_list}" \
          --data-urlencode "location=${expected_dir}" \
          "http://localhost:${port}/api/v2/torrents/setLocation")
      fi

      if [[ $? -eq 0 ]]; then
        moved=$(echo "${hash_list}" | tr '|' '\n' | wc -l)
      else
        EchoW "Failed to move torrents to ${win_expected_dir}. Response: ${move_response}"
        moved=0
      fi
    fi

    if [[ ${DRY_RUN} -eq 1 ]]; then
      Echo "[DRY-RUN] Instance '${instance}': ${moved} incomplete torrents would be moved to ${expected_dir}"
    else
      Echo "Instance '${instance}': ${moved} incomplete torrents moved to ${expected_dir}"
    fi

    (( total_moved += moved ))
  done

  if [[ ${DRY_RUN} -eq 0 ]]; then
    Echo "📦 Total incomplete torrents relocated: ${total_moved}"
  fi
}

#==============================================================================
# RunSanitizationSuite - Orchestrates full torrent sanitization
#------------------------------------------------------------------------------
# Description:
#   Runs a sequence of sanitization tasks: removes malformed torrents, fixes
#   categories, moves completed torrents, and relocates incomplete torrents.
#
# Globals:
#   None (relies on functions called)
#
# Arguments:
#   None (uses global SELECTED_INSTANCE)
#
# Outputs:
#   Writes informational messages to stdout via `Echo`.
#
# Exit Codes:
#   0 - Success
#
# Side Effects:
#   Calls `RemoveTorrentsWithMalformedPath`, `SetCorrectCategory`,
#   `MoveCompletedTorrents`, and `MoveIncompleteTorrentsToDownloadFolder`.
#   Inherits side effects of called functions (API calls, file modifications).
#==============================================================================
RunSanitizationSuite() {
  local -r SELECTED_INSTANCE="${INSTANCE_NAME}"

  Echo "🧽 Running full torrent sanitization on instance '${SELECTED_INSTANCE}'..."
  RemoveTorrentsWithMalformedPath
  SetCorrectCategory
  MoveCompletedTorrents
  MoveIncompleteTorrentsToDownloadFolder
  Echo "✅ Sanitization complete."
}

#==============================================================================
# Main - Orchestrates script execution
#------------------------------------------------------------------------------
# Description:
#   Parses arguments, checks dependencies, validates qBittorrent reachability,
#   and routes execution to the appropriate mode-specific function.
#
# Globals:
#   EXECUTION_MODE   - Selected execution mode (RO)
#   INSTANCE_NAME    - Selected qBittorrent instance (RO)
#
# Arguments:
#   $@ - Command-line arguments
#
# Outputs:
#   Writes error messages to stdout via `EchoE` for invalid modes or instances.
#
# Exit Codes:
#   0 - Success
#   1 - Failure (invalid mode, missing instance, dependency issues)
#
# Side Effects:
#   Calls `ParseArguments`, `DoDependency`, `CheckQbtReachability`, and
#   mode-specific functions (`DoDelete`, `DoRecheck`, etc.).
#   Inherits side effects of called functions (API calls, file modifications).
#==============================================================================
Main() {
  if ParseArguments "$@"; then
    # Validate execution mode
    if [[ ${EXECUTION_MODE} -eq ${MODE_NONE} ]]; then
      EchoE "No execution mode specified. Use --delete, --recheck-error, --set-priority, or --manage-downloads."
      ShowHelp
      return 1
    fi

    # Check dependencies
    DoDependency

    if [[ -z "${INSTANCE_NAME}" ]]; then
      EchoE "--instance is required for --recheck-error mode."
      ShowHelp
      return 1
    fi

    # Check qBittorrent reachability (except for dependency check mode)
    if [[ ${EXECUTION_MODE} -ne ${MODE_NONE} ]]; then
      CheckQbtReachability
    fi

    # Execute based on mode
    case ${EXECUTION_MODE} in
      ${MODE_DELETE})           DoDelete "${INSTANCE_NAME}";;
      ${MODE_RECHECK})          DoRecheck  "${INSTANCE_NAME}";;
      ${MODE_PRIORITY})         DoSetPriority "${INSTANCE_NAME}";;
      ${MODE_REMOVE_MALFORMED}) RemoveTorrentsWithMalformedPath "${INSTANCE_NAME}";;
      ${MODE_FIX_CATEGORY})     SetCorrectCategory "${INSTANCE_NAME}";;
      ${MODE_MOVE_COMPLETED})   MoveCompletedTorrents "${INSTANCE_NAME}";;
      ${MODE_MOVE_INCOMPLETE})  MoveIncompleteTorrentsToDownloadFolder "${INSTANCE_NAME}";;
      ${MODE_SANITISE})         RemoveTorrentsWithMalformedPath "${INSTANCE_NAME}";;
    esac
  fi
}

Main "$@" 
exit 0

# -----------------------------------------------------------------------------
# __CHATGPT_INSTRUCTIONS_BEGIN__
#
# All Bash functions in this script must include a header block with the following format:
#
#==============================================================================
# <function-name> - <short description of what the function does>
# Globals:
#   <VARIABLE_NAME> - Description of its use (RO for read-only, RW for read-write)
#   ...
# Arguments:
#   $1 - Description of the first positional argument
#   $2 - Description of the second positional argument (if applicable)
#   ...
# Outputs:
#   <Clearly state what is written to stdout or stderr>
#   <Any global variables modified (e.g., sets ACTOR_FILMOGRAPHY)>
#==============================================================================
#
# Guidelines:
# - Always use `local` for function-local variables.
# - Use `${...}` consistently for all variable references.
# - Output user-facing messages using the Echo, EchoE, EchoW, EchoD functions.
# - When writing a function that returns data via stdout, prefer using a dedicated file descriptor (e.g., `>&5`) to avoid collisions with logging.
# - Each function should be self-contained and document every global variable it accesses or modifies.
# - Always wrap long `jq` or `curl` commands in clean, readable syntax with fallbacks or clear error handling.
#
# Logging functions are predefined and must be used instead of raw `echo`.
# Each automatically includes a timestamp, an icon, and a log-level prefix.
#
# Use the following functions consistently:
#
# - `Echo`   → for normal informational output (✅ or 📌)
# - `EchoW`  → for warnings (⚠️ WARNING:)
# - `EchoE`  → for errors (❌ ERROR:)
# - `EchoD`  → for debug output (🛠 DEBUG:)
#
# Important:
# - Do **NOT** prepend messages manually with "ERROR:", "WARNING:", or emojis — these are automatically included by the functions.
# - Do **NOT** call `echo` directly for user-facing output unless it's data being piped, stored, or exported.
# - All log messages should be clear, human-readable, and aligned with script actions.
#
# Examples (✅ correct):
#   Echo    "✅ Submitted successfully."       # icon can be supplied
#   EchoW   "Actor already exists in the database."
#   EchoE   "Could not resolve TMDb ID for IMDb ID '${id}'."
#   EchoD   "Parsed movie title: ${title}"
#
# Examples (❌ incorrect):
#   EchoW   "WARNING: Actor exists."                # redundant prefix
#   EchoE   "❌ ERROR: Failed to post."            # message is duplicated
#   EchoD   "2025-04-22 00:51:35| Parsed movie title: ${title}"  # message havs two time stamps
#
# 1. **Purpose and Modes**:
#    - The script supports four modes: --delete, --recheck-error, --set-priority, and --manage-downloads.
#    - --delete: Deletes torrents matching patterns in TV_Organize_Patterns.txt.
#    - --recheck-error: Rechecks torrents in an "error" state for a specific qBittorrent instance.
#    - --set-priority: Sets high priority and force-starts TV torrents matching specific prefixes.
#    - --manage-downloads: Moves completed torrents to a final directory and ensures incomplete ones are in a temp directory.
#
# 2. **Key Variables**:
#    - PREFIXES: Array of TV show prefixes used for matching in --set-priority mode.
#    - QBT_INSTANCES: Associative array mapping instance names (tv, anime, etc.) to qBittorrent ports.
#    - TEMP_DIRS/FINAL_DIRS: Directory mappings for --manage-downloads mode.
#    - DRY_RUN: Flag to simulate actions (default: 1, enabled).
#
# 3. **Dependencies**:
#    - Requires curl, jq, date, and urlencode (with fallbacks for urlencode).
#    - Dependency checks are configurable via --check-deps, --check-deps-local, --check-deps-install.
#
# 4. **Logging and Debugging**:
#    - Use --debug (-D) to enable verbose output, including curl commands and intermediate data.
#    - Logs are written to /cygdrive/f/p_qBittorrent/.logs for reachability and failures.
#
# 5. **Error Handling**:
#    - The script includes reachability checks for qBittorrent with retries.
#    - Temporary files are cleaned up on exit (via trap).
#    - API responses are checked for failures.
#
# 6. **Potential Improvements**:
#    - Add support for more qBittorrent API endpoints (e.g., pausing/resuming torrents).
#    - Enhance pattern matching with regex options in --delete mode.
#    - Add a --force option to skip user prompts in --delete mode.
#
# 7. **Known Issues**:
#    - jq 1.7.1 has a bug causing assertion failures with complex expressions. Workarounds include simplifying jq usage or upgrading to a newer version.
#    - Large torrent lists may cause performance issues; consider batching API calls.
#
# __CHATGPT_INSTRUCTIONS_END__
# -----------------------------------------------------------------------------


Comments

Popular posts from this blog

Movies - Deadpool & Wolverine (2024)

 

TV Mini-serie - Lady in the Lake (2024)

 

Movie - The Gorge (2025)

  My views For 80 years everything was ok ... until they sent a woman For sure is DTV ... really bad Inside the gorge is clearly designed and written by a gamer Plot Two elite  snipers  receive identical missions: travel to an undisclosed location and guard the West and East sides of a deep gorge for one year without any contact with the outside world nor their counterpart on the opposite side. Levi Kane, a former  U.S. Marine  and current  private contractor  accepts the offer to guard the West tower. Drasa, a  Lithuanian  covert operative frequently employed by the  Kremlin , agrees to guard the East side. Upon arriving, Levi relieves his predecessor, J.D., a  British   Royal Marine  of duty and asks for specifics about the mission. J.D. explains that in addition to the towers on the East and West, there are automated turret defenses to the North and South, a powerful signal ‘ cloak ,’ and  explosives on the walls ...