Skip to main content

IT - Programming - BASH scripting - qBittorrent_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

TV Series - The Brokenwood Mysteries [NZ] (2014) - Season 10

 

Movie - Sin City: A Dame to Kill For (2014)

 

Movies - Deadpool & Wolverine (2024)