#!/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__
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