#!/bin/bash # 🧠 ChatGPT-aware developer instructions at bottom of script (__CHATGPT_INSTRUCTIONS_BEGIN__) # Published: https://afberendsen.blogspot.com/2025/04/it-programming-bash-script.html ################################################################################ # Script Name: QbittorrentStatusMonitor.sh # Description : Monitors multiple qBittorrent instances running in parallel, # aggregating torrent statistics (Active, Checking, Moving, Completed) # across isolated categories: Anime, Movie, TV, and XXX. # # Output : Prints a visually aligned, column-based table every 30 minutes # showing per-category and total counts. Automatically reprints # headers every 20 rows. Logs can be redirected if needed. # # Requirements: - qBittorrent Web UI enabled on each profile with API access # - curl, jq installed # - Cygwin-compatible (Unix-like Bash shell on Windows) # # Usage : Run in the background or via systemd/task scheduler: # $ nohup ./QbittorrentStatusMonitor.sh & # # Version History: # v1.0 - Initial working prototype with fixed ports and hardcoded URLs # v1.1 - Added structured header printing and improved output alignment # v1.2 - Introduced category-based aggregation logic (Anime, Movie, TV, XXX) # v1.3 - Switched to dynamic category/port maps using CATEGORIES[] and QBT_PORTS[] # v1.4 - Improved JSON fetch safety by using temporary variables # v1.5 - Added jq-based filtering for consistent state detection per category # v1.6 - Included 'Total' columns for Active, Checking, Moving, and Completed # v1.7 - Gracefully handled empty or invalid API responses with fallbacks # v1.8 - Refactored state-counting into modular helper functions # v1.9 - Centralized category/port configuration for easier scalability # v1.10 - Added command-line options: --help (-h), --version (-V) # v1.11 - Added --interval (-i) to set polling rate, and --count (-c) for header frequency # v1.12 - Implemented retry logic (default 5 tries with 5s delay) for transient API failures # v1.13 - Added --try and --delay switches for user-defined retry attempts and delay # v1.14 - Introduced '--' as a command-line switch terminator to stop further option parsing # # Author : afberendsen + ChatGPT # Date : 2025-04-17 ################################################################################ # Load shared library [[ ! -e ~/MY_LIBRARY.sh ]] && { echo "***ERROR: Cannot find library" ; exit 1; } [[ ! -r ~/MY_LIBRARY.sh ]] && { echo "***ERROR: Library is not readable" ; exit 1; } source ~/MY_LIBRARY.sh || { echo "***ERROR: Cannot source library" ; exit 1; } [[ ! -e ~/MY_SECRETS.sh ]] && { echo "***ERROR: Cannot find ~/MY_SECRETS.sh" ; exit 1; } [[ ! -r ~/MY_SECRETS.sh ]] && { echo "***ERROR: ~/MY_SECRETS.sh is not readable" ; exit 1; } source ~/MY_SECRETS.sh || { echo "***ERROR: Cannot source ~/MY_SECRETS.sh" ; exit 1; } trap 'Echo "💥 Caught interrupt signal, exiting."; exit 1' SIGINT #============================================================================== # Constants #============================================================================== readonly SCRIPT_VERSION="1.14" declare -r LOG_FILE="./qbittorrent_status.log" # CONFIGURATION declare -ar CATEGORIES=("Anime" "Movie" "TV" "XXX") declare -Ar QBT_PORTS=(["Anime"]=8079 ["Movie"]=8076 ["TV"]=8077 ["XXX"]=8078) declare -A QBT_URLS=() declare -i INTERVAL_MINUTES=30 declare -i INTERVAL_HEADER=20 declare -i TIMEOUT_TRY=10 declare -i TIMEOUT_DELAY=5 # Initialize API URLs for cat in "${CATEGORIES[@]}"; do QBT_URLS["${cat}"]="http://localhost:${QBT_PORTS[$cat]}/api/v2/torrents/info" done #============================================================================== # Show help #============================================================================== show_help() { cat <<EOF Usage: ${0##*/} [OPTIONS] [--] Monitors multiple qBittorrent instances and prints torrent statistics. Options: -i, --interval <minutes> Set polling interval (default: 30) -c, --count <lines> Rows between header lines (default: 20) -t, --try <number> Retry attempts for failed API calls (default: 5) -d, --delay <seconds> Delay between retries in seconds (default: 5) -h, --help Show this help message and exit -V, --version Show script version and exit -- Stop processing further switches Each qBittorrent instance must listen on: Anime : http://localhost:8079 Movie : http://localhost:8076 TV : http://localhost:8077 XXX : http://localhost:8078 EOF } #============================================================================== #============================================================================== OLD_print_header() { printf "+---------------------+--------+---------------------------------------+---------------------------------------+----------------------------------------------------------------------+\n" printf "| | _____________Active__________________ | ______________Checking_______________ | _______________Moving________________ | ______________Completed______________ |\n" printf "| Timestamp | Anime | Movie | TV | XXX | Total | Anime | Movie | TV | XXX | Total | Anime | Movie | TV | XXX | Total | Anime | Movie | TV | XXX | Total |\n" printf "+---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+\n" nRowCounter=0 } #============================================================================== # Dynamic top/bottom border line #============================================================================== print_separator_line() { printf "+---------------------+" for _ in "${states[@]}"; do for _ in "${CATEGORIES[@]}"; do printf '%s' "-------+"; done printf '%s' "-------+" done printf "\n" } #============================================================================== #============================================================================== print_header() { local -ir col_width=5 local -ir col_total_width=$((col_width + 3)) # One space + number + +one space + pipe = 8 local -i block_width local -r states=("Active" "Checking" "Moving" "Completed") print_separator_line # STATE ROW (Active | Checking | ...) printf "| %-19s |" "" for state in "${states[@]}"; do block_width=$(( (${#CATEGORIES[@]} + 1) * col_total_width - 1 )) # -1 is for the last '|' label_len=${#state} padding=$((block_width - label_len)) left_padding=$((padding / 2)) right_padding=$((padding - left_padding)) # Left padding: 1 space + underscores printf ' %*s' "$((left_padding - 1))" "$(printf '%*s' "$((left_padding - 1))" | tr ' ' '_')" printf '%s' "${state}" printf '%*s |' "$((right_padding - 1))" "$(printf '%*s' "$((right_padding - 1))" | tr ' ' '_')" done echo # COLUMN NAMES (Anime | Movie | ... | Total) printf "| %-19s |" "Timestamp" for _ in "${states[@]}"; do for cat in "${CATEGORIES[@]}"; do printf " %5s |" "${cat}" done printf " %5s |" "Total" done echo print_separator_line nRowCounter=0 } #============================================================================== #============================================================================== print_row() { local -r timestamp="${1}" shift local -a values=( "$@" ) [[ ${nRowCounter} -ge ${INTERVAL_HEADER} ]] && print_header printf "| %-19s |" "${timestamp}" for i in {0..4}; do printf " %5s |" "${values[$i]}" ; done # Total for i in {5..9}; do printf " %5s |" "${values[$i]}" ; done # Checking for i in {10..14}; do printf " %5s |" "${values[$i]}" ; done # Moving for i in {15..19}; do printf " %5s |" "${values[$i]}" ; done # Completed echo (( nRowCounter++ )) } #============================================================================== #============================================================================== get_state_count() { local json="${1}" category="${2}" state="${3}" echo "${json}" | jq --arg cat "${category}" --arg state "${state}" \ '[.[] | select(.category == $cat and .state == $state)] | length' } #============================================================================== #============================================================================== get_completed_count() { local json="${1}" category="${2}" echo "${json}" | jq --arg cat "${category}" \ '[.[] | select(.category == $cat and (.state == "uploading" or .state == "stalledUP" or .state == "pausedUP" or .state == "queuedUP"))] | length' } #============================================================================== #============================================================================== ParseArguments() { while [[ $# -gt 0 ]]; do case "$1" in -i|--interval) shift [[ "$1" =~ ^[0-9]+$ ]] || { echo "ERROR: --interval must be a number"; exit 1; } INTERVAL_MINUTES="$1" ;; -c|--count) shift [[ "$1" =~ ^[0-9]+$ ]] || { echo "ERROR: --count must be a number"; exit 1; } INTERVAL_HEADER="$1" ;; -t|--try) shift [[ "$1" =~ ^[0-9]+$ ]] || { echo "ERROR: --try must be a number"; exit 1; } TIMEOUT_TRY="$1" ;; -d|--delay) shift [[ "$1" =~ ^[0-9]+$ ]] || { echo "ERROR: --delay must be a number"; exit 1; } TIMEOUT_DELAY="$1" ;; -h|--help) show_help exit 0 ;; -V|--version) echo "${0##*/} version ${SCRIPT_VERSION}" exit 0 ;; --) shift; break ;; # Stop parsing further options -*) echo "Unknown option: $1" show_help exit 1 ;; *) break ;; # Stop if positional argument encountered esac shift done } #============================================================================== #============================================================================== Main() { ParseArguments "$@" declare -ir INTERVAL_SECONDS=$((INTERVAL_MINUTES * 60)) declare -i nRowCounter="${INTERVAL_HEADER}" # Force first header while true; do TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') declare -A JSONS declare -Ai TOTALS CHECKINGS MOVINGS COMPLETEDS declare -i TOTAL=0 CHECKING=0 MOVING=0 COMPLETED=0 for cat in "${CATEGORIES[@]}"; do temp_json=$(mktemp) declare attempt=1 success=0 while (( attempt <= TIMEOUT_TRY )); do curl --silent --max-time 10 "${QBT_URLS[$cat]}" > "${temp_json}" if jq -e . "${temp_json}" &>/dev/null; then success=1 break fi (( attempt++ )) sleep "${TIMEOUT_DELAY}" done if (( success == 0 )); then EchoW "⚠️ Invalid JSON from ${QBT_URLS[$cat]} after ${TIMEOUT_TRY} tries – using empty list" JSONS["$cat"]="[]" else JSONS["$cat"]="$(< "${temp_json}")" fi rm -f "${temp_json}" TOTALS["$cat"]="$(echo "${JSONS[$cat]}" | jq length 2>/dev/null || echo 0)" CHECKINGS["$cat"]="$(get_state_count "${JSONS[$cat]}" "${cat}" "checkingResumeData" 2>/dev/null || echo 0)" MOVINGS["$cat"]="$(get_state_count "${JSONS[$cat]}" "${cat}" "moving" 2>/dev/null || echo 0)" COMPLETEDS["$cat"]="$(get_completed_count "${JSONS[$cat]}" "${cat}" 2>/dev/null || echo 0)" (( TOTAL += TOTALS[$cat] )) (( CHECKING += CHECKINGS[$cat] )) (( MOVING += MOVINGS[$cat] )) (( COMPLETED += COMPLETEDS[$cat] )) done print_row "${TIMESTAMP}" \ "${TOTALS[Anime]}" "${TOTALS[Movie]}" "${TOTALS[TV]}" "${TOTALS[XXX]}" "${TOTAL}" \ "${CHECKINGS[Anime]}" "${CHECKINGS[Movie]}" "${CHECKINGS[TV]}" "${CHECKINGS[XXX]}" "${CHECKING}" \ "${MOVINGS[Anime]}" "${MOVINGS[Movie]}" "${MOVINGS[TV]}" "${MOVINGS[XXX]}" "${MOVING}" \ "${COMPLETEDS[Anime]}" "${COMPLETEDS[Movie]}" "${COMPLETEDS[TV]}" "${COMPLETEDS[XXX]}" "${COMPLETED}" sleep "${INTERVAL_SECONDS}" done } Main "$@" exit 0 # __CHATGPT_INSTRUCTIONS_BEGIN__ # ➤ Versioning Policy # - Always increment versions sequentially (e.g., 1.9 → 1.10 → 1.11). # - DO NOT bump major version (1.x → 2.0) unless explicitly requested by the user. # ➤ Platform Check # - Every script must include this as the FIRST EXECUTABLE line: # [[ "$(uname -r)" != CYGWIN_NT* ]] && echo "This script can only be executed under Cygwin" && exit # ➤ Secrets and Tokens # - NEVER embed credentials or API keys directly in the script. # - Place all such constants inside ~/MY_SECRETS.sh and load them with: # source ~/MY_SECRETS.sh # ➤ Function Naming Convention # - Functions must be in PascalCase (FirstLetterUppercase, no underscores). # e.g., ParseArguments, DoRecheckErrored, SubmitPostToBlogger # ➤ Script Header Formatting # - Use a clear comment block with: # - Script name # - Version history (incremental) # - Purpose summary # - Author or ChatGPT note if applicable # ➤ Output Conventions # - Use Echo / EchoD / EchoW / EchoE from ~/MY_LIBRARY.sh # - Echo messages must include timestamps and emojis if relevant # ➤ Argument Parsing # - Use getopt-like flags (e.g., -v, -d, --execute) # - Mutually exclusive flags (e.g., --delete, --set-priority, --recheck-error) must be enforced # ➤ Return Values from Functions # - Use a dedicated FD (e.g., >&5) when a function returns structured data # ➤ Looping Style # - Prefer while ... done < <(...) over piping for clarity and control # ➤ Default Flag Expectations # - Always support: -h|--help, -V|--version, -n|--dry-run, -v|--verbose, -D|--debug # ➤ Caching and Metadata # - Store all cache, mappings, or temporary state under ~/.MyBlogCache or ~/.MyScriptCache # ➤ Blogger and OMDb/TMDb Integration # - Respect post format rules (titles, actor pages, movie formatting) # - Use both IMDb ID and actor name when available # ➤ Blog Post Templates # - Actor posts must include: photo, role table, source list, biography # - Movie posts must support: collapsible cast, poster, runtime, metadata, labels # ➤ Reserved Directories # - Use /cygdrive/f/p_qBittorrent/.cache/ for qBittorrent logs/backups # - Use /cygdrive/p/MovieLibrary or /cygdrive/f/p_BlogCache/ for media/blog scripts # __CHATGPT_INSTRUCTIONS_END__
Comments
Post a Comment