Codebase-wide audit pass: fixed real bugs (SSH hangs missing BatchMode, local-outside-function no-ops, variable name collisions, a truncated ratio calc, wrong state-dir path, DARK vs NO_INTERNET drift, and more), then pulled logic that was duplicated across multiple scripts — arr cleanup safety gates, docker restart ordering, container maintenance stop/restart, watchdog state-file helpers, partnership role resolution, cert expiry checks, remote node discovery, and TMDB discovery scoring — into common.sh so each now has a single implementation.
128 lines
4.9 KiB
Bash
Executable File
128 lines
4.9 KiB
Bash
Executable File
#!/bin/bash
|
|
# ==============================================================================================
|
|
# ================================= Decision Engine ============================================
|
|
# ==============================================================================================
|
|
#
|
|
# PURPOSE
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Behavior-driven scoring kernel for media automation. Sourced by consumer
|
|
# scripts — not run directly. Evaluates candidates, applies weighted scoring,
|
|
# temporal decay, and deduplication, then returns a verdict.
|
|
#
|
|
# Does NOT download media, search indexers, or manage applications. Has no
|
|
# side effects — no file writes, no API calls, no deletions.
|
|
#
|
|
# Currently paired with: Arrs_Stack/playback_aware_lidarr_discovery.sh
|
|
#
|
|
# ==============================================================================================
|
|
# DESIGN PRINCIPLES
|
|
# ==============================================================================================
|
|
#
|
|
# Domain-Agnostic Core
|
|
# The engine itself has no knowledge of music vs TV vs movies. Consumers
|
|
# define thresholds, weights, and strictness profiles — the engine just
|
|
# scores and decides. This prevents cross-domain bias pollution: music
|
|
# strictness cannot contaminate TV intake logic.
|
|
#
|
|
# Lidarr consumers → highly selective, quality-first discovery
|
|
# Sonarr consumers → balanced, family-aware episodic intake
|
|
# Radarr consumers → broader flexibility with intelligent filtering
|
|
#
|
|
# Temporal Decay
|
|
# Old behavioral signals lose influence over time (one decay unit per 30
|
|
# days). Prevents permanent genre lock-in, historical bias accumulation,
|
|
# and dead-user score dominance.
|
|
#
|
|
# Consumer Owns the Decision
|
|
# The engine returns ACCEPT/REJECT/SCORE. What happens next is entirely
|
|
# the consumer's concern — the engine never acts on its own verdict.
|
|
#
|
|
# ==============================================================================================
|
|
# FUNCTIONS
|
|
# ==============================================================================================
|
|
#
|
|
# score_candidate user_score popularity_score recency_score quality_score
|
|
# Returns TOTAL_SCORE (integer sum). Consumer defines actual weight values.
|
|
#
|
|
# evaluate_threshold score minimum
|
|
# Returns 0 (pass) or 1 (fail). Used as: if evaluate_threshold ...
|
|
#
|
|
# apply_temporal_decay score age_days
|
|
# Returns adjusted score. Subtracts (age_days / 30), floor at 0.
|
|
#
|
|
# is_duplicate_candidate candidate history_file
|
|
# Returns 0 (duplicate found in history_file) or 1 (not found).
|
|
#
|
|
# make_decision score threshold
|
|
# Returns "ACCEPT" or "REJECT". Calls evaluate_threshold internally.
|
|
#
|
|
# ==============================================================================================
|
|
|
|
# ── score_candidate ───────────────────────────────────────────────────────────
|
|
score_candidate() {
|
|
|
|
local user_score="${1:-0}"
|
|
local popularity_score="${2:-0}"
|
|
local recency_score="${3:-0}"
|
|
local quality_score="${4:-0}"
|
|
local TOTAL_SCORE
|
|
|
|
TOTAL_SCORE=$(( \
|
|
user_score + \
|
|
popularity_score + \
|
|
recency_score + \
|
|
quality_score \
|
|
))
|
|
|
|
echo "$TOTAL_SCORE"
|
|
}
|
|
|
|
# ── evaluate_threshold ───────────────────────────────────────────────────────
|
|
evaluate_threshold() {
|
|
|
|
local score="$1"
|
|
local minimum="$2"
|
|
|
|
if (( score >= minimum )); then
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# ── apply_temporal_decay ─────────────────────────────────────────────────────
|
|
apply_temporal_decay() {
|
|
|
|
local score="$1"
|
|
local age_days="$2"
|
|
|
|
local decay=$(( age_days / 30 ))
|
|
|
|
local adjusted=$(( score - decay ))
|
|
|
|
(( adjusted < 0 )) && adjusted=0
|
|
|
|
echo "$adjusted"
|
|
}
|
|
|
|
# ── is_duplicate_candidate ───────────────────────────────────────────────────
|
|
is_duplicate_candidate() {
|
|
|
|
local candidate="$1"
|
|
local history_file="$2"
|
|
|
|
grep -qi "^${candidate}$" "$history_file" 2>/dev/null
|
|
}
|
|
|
|
# ── make_decision ─────────────────────────────────────────────────────────────
|
|
make_decision() {
|
|
|
|
local score="$1"
|
|
local threshold="$2"
|
|
|
|
if evaluate_threshold "$score" "$threshold"; then
|
|
echo "ACCEPT"
|
|
else
|
|
echo "REJECT"
|
|
fi
|
|
} |