#!/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__
# -----------------------------------------------------------------------------
My views Plot In an unnamed city overcome with violent crime and corruption, disillusioned police Detective Lieutenant William Somerset is one week from retirement. He is partnered with David Mills, a young, short-tempered, idealistic detective who recently relocated to the city with his wife, Tracy. On Monday, Somerset and Mills investigate an obese man who was forced to eat until his stomach burst, killing him. The detectives find the word " gluttony " written on a wall. Somerset, considering the case too extreme for his last investigation, asks to be reassigned, but his request is denied. The following day, another victim, who had been forced to cut one pound (0.45 kg) of flesh from his body, is found; the crime scene is marked " greed ." Clues at the scene lead Somerset and Mills to the sloth victim, a drug-dealing pederast whom they find emaciated and restrained to a bed. Photographs reveal the victim was restrained for precisely one year. Somers...
Comments
Post a Comment