#!/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
Post a Comment