Add anime/kids content misclassification detection for Radarr and Sonarr

Overseerr lets users request content into the wrong root folder; these
new report-only scans classify every tracked movie/series (anime, kids,
regular) from metadata alone and flag mismatches against the actual
root folder, in both directions. Rules were validated against real
library data before being adopted — see the header comments in each
script and the master.conf notes above the curated lists.
This commit is contained in:
Gmer4Lfe
2026-07-17 11:49:23 -04:00
parent 6dc5c91f60
commit 9dbcdc0bd7
5 changed files with 521 additions and 0 deletions
+240
View File
@@ -0,0 +1,240 @@
#!/bin/bash
# ==============================================================================================
# ============================ Radarr Content Classification Scan ==============================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Overseerr lets any user request a movie into the wrong root folder (kids content added to
# the general Movies share, anime added to Kids_Movies, etc.) and most users never notice or
# correct it. This script reads Radarr's tracked movie list and classifies every movie as
# anime / kids-only / regular using metadata signals alone (genre, certification, studio,
# original language) — then reports where a movie's computed classification disagrees with
# the root folder it's actually sitting in, in both directions:
#
# FORWARD — a movie classified as anime/kids is sitting outside its dedicated root
# REVERSE — a movie sitting inside the kids/anime root doesn't match that classification
#
# Report-only. No files are moved and no Radarr API writes happen — this is a detection
# tool. Every rule below was validated against this library's real data before being
# adopted (see master.conf comments above the curated lists) — this is not a generic
# genre-matcher, it's tuned specifically against the false-positive traps that showed up
# when testing looser rules (documented per-rule below).
#
# ==============================================================================================
# CLASSIFICATION RULES
# ==============================================================================================
#
# is_anime:
# (genre Animation AND originalLanguage Japanese) OR studio in RADARR_ANIME_STUDIOS
# Always wins over kids when both could apply — explicit priority, not a tiebreak.
#
# is_kids ("kids will end up watching this alone" — NOT "family movie night"):
# not is_anime AND certification not in (R, NC-17) AND (
# (genre Animation AND certification != PG-13)
# OR studio in RADARR_KIDS_STUDIOS
# )
# Deliberately excludes bare "Family" genre and bare "G" certification — both genuinely
# traced back to live-action films the whole household watches together (Mrs. Doubtfire,
# Doctor Dolittle, National Treasure-style adventures, classic Westerns), not kids-only
# content. Family movie night stays in the general Movies root by design.
#
# The PG-13 exclusion on the Animation branch is load-bearing — without it this rule
# catches South Park movies, Sausage Party, "9", Resident Evil: Death Island, and (via
# the curated studio list) Warner Bros. Animation's R-rated Watchmen films, since that
# studio makes both kids content and adult content under the same name.
#
# is_junk (bad/thin TMDb match, not a real classification problem):
# hasFile == false AND imdbId == null AND tmdb votes < RADARR_JUNK_MIN_VOTES
# Caught live: two fake "X-Men"/"Wolverine" entries, two fake "Silent Hill" entries, one
# fake "The Purge" spinoff — all monitored placeholders with nothing behind them. The
# fix for these is removal from Radarr, not blocklist+redownload — there's no release to
# blocklist and likely nothing legitimate to redownload under that exact TMDb match.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Report, Don't Act
# This script never calls Radarr's write API and never touches a file. Every finding is
# a candidate for a human decision — moving media and re-pointing Radarr's tracking is a
# separate, deliberate follow-up action, not something this scan does automatically.
#
# Curated Lists, Not Bare Genre/Cert Matching
# Every signal used here failed at least once as a bare/standalone check during rule
# development (Family genre, G certification, blanket Animation genre, bare Anime genre
# tag, Disney+/general-platform networks) — see master.conf comments for what each
# curated list deliberately excludes and why.
#
# Cache-First, Never a Per-Movie Call
# Uses arr_get_tracked_data() same as radarr_cleanup.sh — Radarr's movie list already
# embeds everything this script needs per movie, so this is a single API call (or zero,
# if the shared cache is warm) regardless of library size.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf
# RADARR_URL / RADARR_API_KEY / RADARR_MOVIES_ROOT — existing, aliased by detect_hosts()
# RADARR_KIDS_ROOT / RADARR_ANIME_ROOT — rootFolderPath literals as reported by the API
# (e.g. "/kids movies", "/ext-anime-movies") — leave blank on a host with no dedicated
# root for that category; the corresponding checks are skipped, not treated as an error.
#
# master.conf
# RADARR_ANIME_STUDIOS / RADARR_KIDS_STUDIOS — curated studio allowlists
# RADARR_JUNK_MIN_VOTES — TMDb vote threshold for the bad-metadata check
# RADARR_VERSION_MAJOR — expected API major version (reused from radarr_cleanup.sh)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# radarr_classification_scan.sh — normal run, prints report
# radarr_classification_scan.sh --log — verbose (per-movie TRACKED-style logging)
# radarr_classification_scan.sh --status — show config and exit
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../load_config.sh"
parse_args "$@"
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
error "jq not found — required for JSON parsing"
exit 1
fi
detect_hosts
if [[ -z "${RADARR_URL:-}" ]] || [[ -z "${RADARR_API_KEY:-}" ]]; then
info "Radarr not configured on $MY_ID ($LOCAL_SERVER_NAME) — skipping"
exit 0
fi
require_var RADARR_URL
require_var RADARR_API_KEY
if [[ "$SHOW_STATUS" == true ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_GEAR Radarr URL: $RADARR_URL"
echo "$ICON_GEAR Movies root: $RADARR_MOVIES_ROOT"
echo "$ICON_GEAR Kids root: ${RADARR_KIDS_ROOT:-<not configured>}"
echo "$ICON_GEAR Anime root: ${RADARR_ANIME_ROOT:-<not configured>}"
echo "$ICON_GEAR Anime studios: ${#RADARR_ANIME_STUDIOS[@]} curated"
echo "$ICON_GEAR Kids studios: ${#RADARR_KIDS_STUDIOS[@]} curated"
echo "$ICON_GEAR Junk min votes: ${RADARR_JUNK_MIN_VOTES:-15}"
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
echo ""
echo "━━━ $ICON_SYNC Fetching Radarr Library ━━━"
if ! check_api "$RADARR_URL" "Radarr" 10; then
exit 1
fi
check_arr_version "$RADARR_URL" "$RADARR_API_KEY" "v3" "$RADARR_VERSION_MAJOR" "Radarr" || exit 1
MOVIES_RESPONSE=$(arr_get_tracked_data "radarr" "$RADARR_URL" "$RADARR_API_KEY" "v3") || {
error "Failed to fetch movies from Radarr"
exit 1
}
MOVIE_COUNT=$(echo "$MOVIES_RESPONSE" | jq -r 'length' 2>/dev/null)
if [[ -z "$MOVIE_COUNT" ]] || [[ "$MOVIE_COUNT" -eq 0 ]]; then
error "API returned 0 movies — aborting"
exit 1
fi
info "$MOVIE_COUNT movies loaded"
# ==============================================================================================
# ━━━ Classify ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_CLEAN Classifying ━━━"
ANIME_STUDIOS_JSON=$(printf '%s\n' "${RADARR_ANIME_STUDIOS[@]}" | jq -R . | jq -s .)
KIDS_STUDIOS_JSON=$(printf '%s\n' "${RADARR_KIDS_STUDIOS[@]}" | jq -R . | jq -s .)
JUNK_MIN_VOTES="${RADARR_JUNK_MIN_VOTES:-15}"
RESULTS=$(echo "$MOVIES_RESPONSE" | jq \
--argjson animeStudios "$ANIME_STUDIOS_JSON" \
--argjson kidsStudios "$KIDS_STUDIOS_JSON" \
--arg animeRoot "${RADARR_ANIME_ROOT:-}" \
--arg kidsRoot "${RADARR_KIDS_ROOT:-}" \
--argjson junkMinVotes "$JUNK_MIN_VOTES" '
def is_anime:
(any(.genres[]?; . == "Animation") and .originalLanguage.name == "Japanese")
or (.studio as $s | $animeStudios | index($s) != null);
def not_adult: (.certification != "R") and (.certification != "NC-17");
def is_kids:
(is_anime | not) and not_adult and (
(any(.genres[]?; . == "Animation") and .certification != "PG-13")
or (.studio as $s | $kidsStudios | index($s) != null)
);
def is_junk:
(.hasFile == false) and (.imdbId == null)
and ((.ratings.tmdb.votes // 999999) < $junkMinVotes);
map(
{
title, id, studio, certification, rootFolderPath,
is_anime: is_anime,
is_kids: is_kids,
is_junk: is_junk
} |
. + {
forward_anime_miss: (.is_anime and $animeRoot != "" and .rootFolderPath != $animeRoot),
forward_kids_miss: (.is_kids and $kidsRoot != "" and .rootFolderPath != $kidsRoot),
reverse_anime_leak: ((.is_anime | not) and $animeRoot != "" and .rootFolderPath == $animeRoot and (.is_junk | not)),
reverse_kids_leak: ((.is_anime | not) and (.is_kids | not) and $kidsRoot != "" and .rootFolderPath == $kidsRoot
and (.is_junk | not)
and (.certification == "R" or .certification == "NC-17"
or (.certification == "PG-13"
and (any(.genres[]?; . == "Family") | not)
and (any(.genres[]?; . == "Animation") | not))))
}
)
')
FORWARD_ANIME_COUNT=$(echo "$RESULTS" | jq '[.[] | select(.forward_anime_miss)] | length')
FORWARD_KIDS_COUNT=$(echo "$RESULTS" | jq '[.[] | select(.forward_kids_miss)] | length')
REVERSE_ANIME_COUNT=$(echo "$RESULTS" | jq '[.[] | select(.reverse_anime_leak)] | length')
REVERSE_KIDS_COUNT=$(echo "$RESULTS" | jq '[.[] | select(.reverse_kids_leak)] | length')
JUNK_COUNT=$(echo "$RESULTS" | jq '[.[] | select(.is_junk)] | length')
if [[ "$ENABLE_LOGGING" == true ]]; then
echo "$RESULTS" | jq -r '.[] | select(.forward_anime_miss or .forward_kids_miss or .reverse_anime_leak or .reverse_kids_leak or .is_junk) |
" [\(if .is_junk then "JUNK" elif .forward_anime_miss then "FORWARD-ANIME" elif .forward_kids_miss then "FORWARD-KIDS" elif .reverse_anime_leak then "REVERSE-ANIME" elif .reverse_kids_leak then "REVERSE-KIDS" else "?" end)] \(.title) (root: \(.rootFolderPath), studio: \(.studio // "n/a"), cert: \(.certification // "n/a"))"'
fi
# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY RADARR CLASSIFICATION SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_SYNC Movies scanned: $MOVIE_COUNT"
echo "$ICON_TRASH Forward — anime miss: $FORWARD_ANIME_COUNT (classified anime, outside ${RADARR_ANIME_ROOT:-<unconfigured>})"
echo "$ICON_TRASH Forward — kids miss: $FORWARD_KIDS_COUNT (classified kids, outside ${RADARR_KIDS_ROOT:-<unconfigured>})"
echo "$ICON_WARN Reverse — anime leak: $REVERSE_ANIME_COUNT (in ${RADARR_ANIME_ROOT:-<unconfigured>}, no anime signal — review, may be deliberate style placement)"
echo "$ICON_WARN Reverse — kids leak: $REVERSE_KIDS_COUNT (in ${RADARR_KIDS_ROOT:-<unconfigured>}, adult-rated content)"
echo "$ICON_PROTECTED Bad metadata (junk): $JUNK_COUNT (hasFile=false, no imdbId, thin TMDb match — candidates for removal, not redownload)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
[[ "$ENABLE_LOGGING" != true ]] && echo " (run with --log for the per-title list)"
exit 0
+224
View File
@@ -0,0 +1,224 @@
#!/bin/bash
# ==============================================================================================
# ============================ Sonarr Content Classification Scan ==============================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Same problem as radarr_classification_scan.sh, TV side: Overseerr lets any user request a
# show into the wrong root folder (kids shows added to the general TV share, anime added to
# Kids_Tv_Shows, etc.). This script reads Sonarr's tracked series list and classifies every
# series as anime / kids-only / regular using metadata signals alone (genre, certification,
# network, original language) — then reports where a series' computed classification
# disagrees with the root folder it's actually sitting in, in both directions:
#
# FORWARD — a series classified as anime/kids is sitting outside its dedicated root
# REVERSE — a series sitting inside the kids/anime root doesn't match that classification
#
# Report-only. No files are moved and no Sonarr API writes happen. Every rule below was
# validated against this library's real data before being adopted — see the companion
# comment block in master.conf above the curated lists.
#
# ==============================================================================================
# CLASSIFICATION RULES — DIFFERENT FIELD MODEL THAN RADARR, NOT A COPY-PASTE
# ==============================================================================================
#
# TV metadata (TheTVDB, via Sonarr) shapes these signals differently than movie metadata
# (TMDb, via Radarr) — every difference below was confirmed live, not assumed:
# - Sonarr has an explicit "Anime" genre tag; Radarr does not.
# - Sonarr uses a single "network" field (TheTVDB's broadcaster), not "studio".
# - Certification is on the US TV Parental Guidelines scale (TV-Y/TV-Y7/TV-G/TV-PG/
# TV-14/TV-MA), not the MPAA scale — the tiers do not mean the same thing at the same
# position (TV-G is "general audience", not "for children", unlike movie G).
#
# is_anime:
# genre "Anime" (corroborated by Japanese language OR a Japan network — the bare tag alone
# produced a real false positive: "Craig of the Creek", an all-American Cartoon Network
# show, carries an "Anime" genre tag on TheTVDB for no discernible reason)
# OR (genre Animation AND originalLanguage Japanese)
# OR network in SONARR_ANIME_NETWORKS
# Always wins over kids when both could apply — explicit priority, not a tiebreak.
#
# is_kids ("kids will end up watching this alone" — NOT "family show night"):
# not is_anime AND (
# genre "Children" (NOT "Family" — see below)
# OR certification in (TV-Y, TV-Y7) (NOT TV-G — see below)
# OR network in SONARR_KIDS_NETWORKS
# )
# "Family" genre and "TV-G" certification were both tested standalone and rejected —
# both catch general-audience live-action content the whole household watches together
# (I Love Lucy, The Brady Bunch, Full House, Homestead Rescue), not kids-only content.
# Blanket "Animation" genre was also tested and rejected — it's dominated on TV by adult
# animated sitcoms (Rick and Morty, BoJack Horseman, Family Guy, South Park), unlike the
# movie side where it's a usable (gated) signal.
#
# No junk-detection tier here (unlike Radarr) — TheTVDB's ratings/imdbId data is far
# sparser than TMDb's even for completely legitimate shows (confirmed live: "The Pussycat
# Dolls Present: The Search for the Next Doll", a real 2007 MTV show, has ratings.votes=0
# and imdbId=null) — the vote-count heuristic that works for Radarr would flag real content
# for removal here, so it's deliberately not reused.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Report, Don't Act — same as the Radarr scan; moving/re-pointing is a deliberate follow-up.
# Curated Lists, Not Bare Genre/Cert Matching — see master.conf comments for exclusions.
# Cache-First — arr_get_tracked_data() same as sonarr_cleanup.sh, single call regardless
# of library size.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf
# SONARR_URL / SONARR_API_KEY / SONARR_TV_ROOT — existing, aliased by detect_hosts()
# SONARR_KIDS_ROOT / SONARR_ANIME_ROOT — rootFolderPath literals as reported by the API
# (e.g. "/kids tv", "/ext-anime-shows") — leave blank on a host with no dedicated root
# for that category; the corresponding checks are skipped, not treated as an error.
#
# master.conf
# SONARR_ANIME_NETWORKS / SONARR_KIDS_NETWORKS — curated network allowlists
# SONARR_VERSION_MAJOR — expected API major version (reused from sonarr_cleanup.sh)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# sonarr_classification_scan.sh — normal run, prints report
# sonarr_classification_scan.sh --log — verbose (per-series list)
# sonarr_classification_scan.sh --status — show config and exit
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../load_config.sh"
parse_args "$@"
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
error "jq not found — required for JSON parsing"
exit 1
fi
detect_hosts
if [[ -z "${SONARR_URL:-}" ]] || [[ -z "${SONARR_API_KEY:-}" ]]; then
info "Sonarr not configured on $MY_ID ($LOCAL_SERVER_NAME) — skipping"
exit 0
fi
require_var SONARR_URL
require_var SONARR_API_KEY
if [[ "$SHOW_STATUS" == true ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_GEAR Sonarr URL: $SONARR_URL"
echo "$ICON_GEAR TV root: $SONARR_TV_ROOT"
echo "$ICON_GEAR Kids root: ${SONARR_KIDS_ROOT:-<not configured>}"
echo "$ICON_GEAR Anime root: ${SONARR_ANIME_ROOT:-<not configured>}"
echo "$ICON_GEAR Anime networks: ${#SONARR_ANIME_NETWORKS[@]} curated"
echo "$ICON_GEAR Kids networks: ${#SONARR_KIDS_NETWORKS[@]} curated"
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
echo ""
echo "━━━ $ICON_SYNC Fetching Sonarr Library ━━━"
if ! check_api "$SONARR_URL" "Sonarr" 10; then
exit 1
fi
check_arr_version "$SONARR_URL" "$SONARR_API_KEY" "v3" "$SONARR_VERSION_MAJOR" "Sonarr" || exit 1
SERIES_RESPONSE=$(arr_get_tracked_data "sonarr" "$SONARR_URL" "$SONARR_API_KEY" "v3") || {
error "Failed to fetch series from Sonarr"
exit 1
}
SERIES_COUNT=$(echo "$SERIES_RESPONSE" | jq -r 'length' 2>/dev/null)
if [[ -z "$SERIES_COUNT" ]] || [[ "$SERIES_COUNT" -eq 0 ]]; then
error "API returned 0 series — aborting"
exit 1
fi
info "$SERIES_COUNT series loaded"
# ==============================================================================================
# ━━━ Classify ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_CLEAN Classifying ━━━"
ANIME_NETWORKS_JSON=$(printf '%s\n' "${SONARR_ANIME_NETWORKS[@]}" | jq -R . | jq -s .)
KIDS_NETWORKS_JSON=$(printf '%s\n' "${SONARR_KIDS_NETWORKS[@]}" | jq -R . | jq -s .)
RESULTS=$(echo "$SERIES_RESPONSE" | jq \
--argjson animeNetworks "$ANIME_NETWORKS_JSON" \
--argjson kidsNetworks "$KIDS_NETWORKS_JSON" \
--arg animeRoot "${SONARR_ANIME_ROOT:-}" \
--arg kidsRoot "${SONARR_KIDS_ROOT:-}" '
def is_anime:
(any(.genres[]?; . == "Anime")
and (.originalLanguage.name == "Japanese" or (.network as $n | $animeNetworks | index($n) != null)))
or (any(.genres[]?; . == "Animation") and .originalLanguage.name == "Japanese")
or (.network as $n | $animeNetworks | index($n) != null);
def is_kids:
(is_anime | not) and (
any(.genres[]?; . == "Children")
or (.certification as $c | ["TV-Y","TV-Y7"] | index($c) != null)
or (.network as $n | $kidsNetworks | index($n) != null)
);
map(
{
title, id, network, certification, rootFolderPath,
is_anime: is_anime,
is_kids: is_kids
} |
. + {
forward_anime_miss: (.is_anime and $animeRoot != "" and .rootFolderPath != $animeRoot),
forward_kids_miss: (.is_kids and $kidsRoot != "" and .rootFolderPath != $kidsRoot),
reverse_anime_leak: ((.is_anime | not) and $animeRoot != "" and .rootFolderPath == $animeRoot),
reverse_kids_leak: ((.is_anime | not) and (.is_kids | not) and $kidsRoot != "" and .rootFolderPath == $kidsRoot
and (.certification == "TV-MA" or .certification == "TV-14"))
}
)
')
FORWARD_ANIME_COUNT=$(echo "$RESULTS" | jq '[.[] | select(.forward_anime_miss)] | length')
FORWARD_KIDS_COUNT=$(echo "$RESULTS" | jq '[.[] | select(.forward_kids_miss)] | length')
REVERSE_ANIME_COUNT=$(echo "$RESULTS" | jq '[.[] | select(.reverse_anime_leak)] | length')
REVERSE_KIDS_COUNT=$(echo "$RESULTS" | jq '[.[] | select(.reverse_kids_leak)] | length')
if [[ "$ENABLE_LOGGING" == true ]]; then
echo "$RESULTS" | jq -r '.[] | select(.forward_anime_miss or .forward_kids_miss or .reverse_anime_leak or .reverse_kids_leak) |
" [\(if .forward_anime_miss then "FORWARD-ANIME" elif .forward_kids_miss then "FORWARD-KIDS" elif .reverse_anime_leak then "REVERSE-ANIME" elif .reverse_kids_leak then "REVERSE-KIDS" else "?" end)] \(.title) (root: \(.rootFolderPath), network: \(.network // "n/a"), cert: \(.certification // "n/a"))"'
fi
# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY SONARR CLASSIFICATION SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_SYNC Series scanned: $SERIES_COUNT"
echo "$ICON_TRASH Forward — anime miss: $FORWARD_ANIME_COUNT (classified anime, outside ${SONARR_ANIME_ROOT:-<unconfigured>})"
echo "$ICON_TRASH Forward — kids miss: $FORWARD_KIDS_COUNT (classified kids, outside ${SONARR_KIDS_ROOT:-<unconfigured>})"
echo "$ICON_WARN Reverse — anime leak: $REVERSE_ANIME_COUNT (in ${SONARR_ANIME_ROOT:-<unconfigured>}, no anime signal — review, may be deliberate style placement)"
echo "$ICON_WARN Reverse — kids leak: $REVERSE_KIDS_COUNT (in ${SONARR_KIDS_ROOT:-<unconfigured>}, adult-rated content)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
[[ "$ENABLE_LOGGING" != true ]] && echo " (run with --log for the per-title list)"
exit 0
+4
View File
@@ -436,6 +436,8 @@
HOSTN_SONARR_URL="http://localhost:8989" HOSTN_SONARR_URL="http://localhost:8989"
HOSTN_SONARR_API_KEY="" HOSTN_SONARR_API_KEY=""
HOSTN_SONARR_TV_ROOT="/mnt/user/Tv_Shows" HOSTN_SONARR_TV_ROOT="/mnt/user/Tv_Shows"
HOSTN_SONARR_KIDS_ROOT="" # rootFolderPath literal, as reported by Sonarr API — leave blank if no dedicated kids root
HOSTN_SONARR_ANIME_ROOT="" # rootFolderPath literal, as reported by Sonarr API — leave blank if no dedicated anime root
declare -A HOSTN_SONARR_PATH_MAP=( declare -A HOSTN_SONARR_PATH_MAP=(
# ["/tv"]="/mnt/user/Tv_Shows" # ["/tv"]="/mnt/user/Tv_Shows"
@@ -446,6 +448,8 @@
HOSTN_RADARR_API_KEY="" HOSTN_RADARR_API_KEY=""
HOSTN_TMDB_API_KEY="" HOSTN_TMDB_API_KEY=""
HOSTN_RADARR_MOVIES_ROOT="/mnt/user/Movies" HOSTN_RADARR_MOVIES_ROOT="/mnt/user/Movies"
HOSTN_RADARR_KIDS_ROOT="" # rootFolderPath literal, as reported by Radarr API — leave blank if no dedicated kids root
HOSTN_RADARR_ANIME_ROOT="" # rootFolderPath literal, as reported by Radarr API — leave blank if no dedicated anime root
declare -A HOSTN_RADARR_PATH_MAP=( declare -A HOSTN_RADARR_PATH_MAP=(
# ["/movies"]="/mnt/user/Movies" # ["/movies"]="/mnt/user/Movies"
+49
View File
@@ -1216,6 +1216,55 @@
RADARR_DROPPED_ADD_EXCLUSION=true # add removed movies to import exclusion list RADARR_DROPPED_ADD_EXCLUSION=true # add removed movies to import exclusion list
SONARR_DROPPED_ADD_EXCLUSION=true # add removed series to import exclusion list SONARR_DROPPED_ADD_EXCLUSION=true # add removed series to import exclusion list
# ━━━ Arr Content Classification (radarr/sonarr_classification_scan.sh) ━━━
#
# Curated lists validated against real library data 2026-07-17 — every entry here was
# checked against the actual signal ratio (kids%/anime% of titles under that studio/network)
# before being added, specifically to avoid the false-positive traps found this session:
# general umbrella studios (Walt Disney Pictures, Paramount Pictures), general streaming
# platforms (Netflix, Disney+, Hulu), and networks that carry mixed content under one name
# (The WB, UPN) are deliberately excluded even though they produce plenty of kids/anime
# content — they also produce just as much non-kids/non-anime content under the same name.
#
# Anime always takes priority over kids when both signals could apply (explicit user rule).
#
RADARR_ANIME_STUDIOS=(
"Studio Ghibli" "ufotable" "Toei Company" "Toei Animation" "MAPPA" "Madhouse"
"Production I.G" "Wit Studio" "Bones" "Trigger" "Kyoto Animation" "CloverWorks"
"A-1 Pictures" "Sunrise" "J.C.Staff" "Shaft" "Studio Pierrot" "P.A. Works"
"David Production" "OLM Inc."
)
RADARR_KIDS_STUDIOS=(
"DreamWorks Animation" "Pixar" "Disney Television Animation"
"Walt Disney Animation Studios" "DisneyToon Studios" "Walt Disney Productions"
"Walt Disney Feature Animation" "Warner Bros. Animation" "Illumination"
"Sony Pictures Animation" "Blue Sky Studios" "Mattel" "Rankin/Bass Productions"
"Mainframe Entertainment" "DreamWorks Animation Television" "ZAG Entertainment"
"Paramount Animation" "Pacific Data Images" "Cinesite Animation"
"Cartoon Network Studios" "Nickelodeon Animation Studio" "Nelvana"
"Aardman Animations"
)
# Radarr's TMDb-vote/imdbId combo reliably flags bad metadata matches (proven live —
# caught 5 genuine junk entries: 2 fake X-Men "Wolverine" titles, Silent Hill: No Escape,
# Silent Hill 4: The Room, The Purge: Behind the Series — all hasFile=false, no imdbId,
# under this vote count). Do NOT reuse this threshold/approach for Sonarr — TheTVDB's
# ratings data is far sparser than TMDb's even for completely legitimate shows (confirmed
# live: "The Pussycat Dolls Present", a real 2007 MTV show, has ratings.votes=0), so the
# same heuristic there would flag real content for removal.
RADARR_JUNK_MIN_VOTES=15
SONARR_ANIME_NETWORKS=(
"Tokyo MX" "AT-X" "TV Tokyo" "Fuji TV" "Nippon TV" "TBS (JP)" "MBS" "TV Asahi"
"WOWOW" "NHK" "Animax" "tvk" "Teletama" "Kansai TV" "ABC (JP)"
)
SONARR_KIDS_NETWORKS=(
"Cartoon Network" "Cartoon Network (CA)" "Cartoon Network (Canada)" "Nickelodeon"
"Nick Jr." "Nicktoons" "Disney Channel" "Disney Jr." "Disney XD" "Toon Disney"
"PBS" "PBS KIDS" "CBeebies" "CBBC" "YTV" "TVOkids" "Discovery Kids"
"Discovery Family" "Boomerang" "ABC Kids" "Teletoon" "Télétoon" "Milkshake!"
"Tiny Pop" "Sprout" "Universal Kids" "BabyTV"
)
# ━━━ Arr Failed/Stalled Recovery ━━━ # ━━━ Arr Failed/Stalled Recovery ━━━
# Auto blocklist + re-search failed imports and stalled downloads. # Auto blocklist + re-search failed imports and stalled downloads.
# Runs every 6 hours — schedule: 0 */6 * * * # Runs every 6 hours — schedule: 0 */6 * * *
+4
View File
@@ -586,11 +586,15 @@ detect_hosts() {
SONARR_URL_VAR="${MY_ID}_SONARR_URL"; SONARR_URL="${!SONARR_URL_VAR:-}" SONARR_URL_VAR="${MY_ID}_SONARR_URL"; SONARR_URL="${!SONARR_URL_VAR:-}"
SONARR_API_KEY_VAR="${MY_ID}_SONARR_API_KEY"; SONARR_API_KEY="${!SONARR_API_KEY_VAR:-}" SONARR_API_KEY_VAR="${MY_ID}_SONARR_API_KEY"; SONARR_API_KEY="${!SONARR_API_KEY_VAR:-}"
SONARR_TV_ROOT_VAR="${MY_ID}_SONARR_TV_ROOT"; SONARR_TV_ROOT="${!SONARR_TV_ROOT_VAR:-}" SONARR_TV_ROOT_VAR="${MY_ID}_SONARR_TV_ROOT"; SONARR_TV_ROOT="${!SONARR_TV_ROOT_VAR:-}"
SONARR_KIDS_ROOT_VAR="${MY_ID}_SONARR_KIDS_ROOT"; SONARR_KIDS_ROOT="${!SONARR_KIDS_ROOT_VAR:-}"
SONARR_ANIME_ROOT_VAR="${MY_ID}_SONARR_ANIME_ROOT"; SONARR_ANIME_ROOT="${!SONARR_ANIME_ROOT_VAR:-}"
SONARR_RECOVERY_VAR="${MY_ID}_SONARR_RECOVERY"; SONARR_RECOVERY="${!SONARR_RECOVERY_VAR:-true}" SONARR_RECOVERY_VAR="${MY_ID}_SONARR_RECOVERY"; SONARR_RECOVERY="${!SONARR_RECOVERY_VAR:-true}"
RADARR_URL_VAR="${MY_ID}_RADARR_URL"; RADARR_URL="${!RADARR_URL_VAR:-}" RADARR_URL_VAR="${MY_ID}_RADARR_URL"; RADARR_URL="${!RADARR_URL_VAR:-}"
RADARR_API_KEY_VAR="${MY_ID}_RADARR_API_KEY"; RADARR_API_KEY="${!RADARR_API_KEY_VAR:-}" RADARR_API_KEY_VAR="${MY_ID}_RADARR_API_KEY"; RADARR_API_KEY="${!RADARR_API_KEY_VAR:-}"
RADARR_MOVIES_ROOT_VAR="${MY_ID}_RADARR_MOVIES_ROOT"; RADARR_MOVIES_ROOT="${!RADARR_MOVIES_ROOT_VAR:-}" RADARR_MOVIES_ROOT_VAR="${MY_ID}_RADARR_MOVIES_ROOT"; RADARR_MOVIES_ROOT="${!RADARR_MOVIES_ROOT_VAR:-}"
RADARR_KIDS_ROOT_VAR="${MY_ID}_RADARR_KIDS_ROOT"; RADARR_KIDS_ROOT="${!RADARR_KIDS_ROOT_VAR:-}"
RADARR_ANIME_ROOT_VAR="${MY_ID}_RADARR_ANIME_ROOT"; RADARR_ANIME_ROOT="${!RADARR_ANIME_ROOT_VAR:-}"
RADARR_RECOVERY_VAR="${MY_ID}_RADARR_RECOVERY"; RADARR_RECOVERY="${!RADARR_RECOVERY_VAR:-true}" RADARR_RECOVERY_VAR="${MY_ID}_RADARR_RECOVERY"; RADARR_RECOVERY="${!RADARR_RECOVERY_VAR:-true}"
SLSKD_URL_VAR="${MY_ID}_SLSKD_URL"; SLSKD_URL="${!SLSKD_URL_VAR:-}" SLSKD_URL_VAR="${MY_ID}_SLSKD_URL"; SLSKD_URL="${!SLSKD_URL_VAR:-}"