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:
Executable
+240
@@ -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
|
||||
Executable
+224
@@ -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
|
||||
Reference in New Issue
Block a user