Add --move mode to sonarr_classification_scan.sh, matching Radarr
Acts on forward misplacements and reverse-kids-leak (adult content in the kids root — not a judgment call, same reasoning as the Radarr side), moving to the new SONARR_GENERAL_ROOT config var. Reverse-anime -leak stays report-only — deliberate style placements like Castlevania are common and valid there. episodeFileCount is Sonarr's hasFile equivalent: series with 0 files get relocated + an immediate SeriesSearch instead of a file move. One series at a time, verified after each — matches the race condition found doing this by hand for the Fate/Zero and Fate/Stay Night moves earlier in the session.
This commit is contained in:
@@ -15,9 +15,10 @@
|
|||||||
# FORWARD — a series classified as anime/kids is sitting outside its dedicated root
|
# 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
|
# 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
|
# Report-only by default. Every rule below was validated against this library's real data
|
||||||
# validated against this library's real data before being adopted — see the companion
|
# before being adopted — see the companion comment block in master.conf above the curated
|
||||||
# comment block in master.conf above the curated lists.
|
# lists. Pass --move to actually act (see MOVE MODE below) — nothing writes to Sonarr unless
|
||||||
|
# that flag is given.
|
||||||
#
|
#
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# CLASSIFICATION RULES — DIFFERENT FIELD MODEL THAN RADARR, NOT A COPY-PASTE
|
# CLASSIFICATION RULES — DIFFERENT FIELD MODEL THAN RADARR, NOT A COPY-PASTE
|
||||||
@@ -56,16 +57,37 @@
|
|||||||
# sparser than TMDb's even for completely legitimate shows (confirmed live: "The Pussycat
|
# 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
|
# 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
|
# and imdbId=null) — the vote-count heuristic that works for Radarr would flag real content
|
||||||
# for removal here, so it's deliberately not reused.
|
# for removal here, so it's deliberately not reused. --remove-junk from the Radarr script has
|
||||||
|
# no Sonarr equivalent for the same reason.
|
||||||
|
#
|
||||||
|
# ==============================================================================================
|
||||||
|
# MOVE MODE (--move)
|
||||||
|
# ==============================================================================================
|
||||||
|
#
|
||||||
|
# Acts on FORWARD misplacements (classified anime/kids, sitting in the wrong root) and on
|
||||||
|
# REVERSE-KIDS leaks (adult certification sitting in the kids root — moved back to
|
||||||
|
# SONARR_GENERAL_ROOT). Does NOT act on REVERSE-ANIME leaks — those are genuine judgment
|
||||||
|
# calls, since deliberate style placements (Castlevania-type Western/Chinese animation
|
||||||
|
# grouped with anime by choice) legitimately live in the anime root without matching the
|
||||||
|
# anime signal.
|
||||||
|
#
|
||||||
|
# episodeFileCount is Sonarr's equivalent of Radarr's hasFile — a series can have 0 files
|
||||||
|
# (fully monitored, nothing downloaded) even while correctly classified. Those get their
|
||||||
|
# rootFolderPath/path corrected and an immediate SeriesSearch triggered rather than a file
|
||||||
|
# move (mirrors radarr_classification_scan.sh's handling of hasFile=false movies).
|
||||||
|
#
|
||||||
|
# One series at a time, verified after each — a rapid-fire batch of Sonarr moves is exactly
|
||||||
|
# what raced Sonarr's own file-move worker earlier this session doing this by hand (2 series
|
||||||
|
# reported "success" while their files sat at an intermediate path). A short sleep plus a
|
||||||
|
# real re-fetch-and-check after every single move catches that here before it can compound.
|
||||||
#
|
#
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# DESIGN PRINCIPLES
|
# 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.
|
# 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
|
# Cache-First — arr_get_tracked_data() same as sonarr_cleanup.sh, single call regardless
|
||||||
# of library size.
|
# of library size. Refreshed after --move writes so no other script reads stale data.
|
||||||
#
|
#
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# CONFIGURATION
|
# CONFIGURATION
|
||||||
@@ -73,9 +95,10 @@
|
|||||||
#
|
#
|
||||||
# host*.conf
|
# host*.conf
|
||||||
# SONARR_URL / SONARR_API_KEY / SONARR_TV_ROOT — existing, aliased by detect_hosts()
|
# 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
|
# SONARR_GENERAL_ROOT / SONARR_KIDS_ROOT / SONARR_ANIME_ROOT — rootFolderPath literals as
|
||||||
# (e.g. "/kids tv", "/ext-anime-shows") — leave blank on a host with no dedicated root
|
# reported by the API (e.g. "/tv", "/kids tv", "/ext-anime-shows") — leave blank on a
|
||||||
# for that category; the corresponding checks are skipped, not treated as an error.
|
# host with no dedicated root for that category; the corresponding checks are skipped,
|
||||||
|
# not treated as an error.
|
||||||
#
|
#
|
||||||
# master.conf
|
# master.conf
|
||||||
# SONARR_ANIME_NETWORKS / SONARR_KIDS_NETWORKS — curated network allowlists
|
# SONARR_ANIME_NETWORKS / SONARR_KIDS_NETWORKS — curated network allowlists
|
||||||
@@ -88,6 +111,7 @@
|
|||||||
# sonarr_classification_scan.sh — normal run, prints report
|
# sonarr_classification_scan.sh — normal run, prints report
|
||||||
# sonarr_classification_scan.sh --log — verbose (per-series list)
|
# sonarr_classification_scan.sh --log — verbose (per-series list)
|
||||||
# sonarr_classification_scan.sh --status — show config and exit
|
# sonarr_classification_scan.sh --status — show config and exit
|
||||||
|
# sonarr_classification_scan.sh --move — act on forward misplacements + reverse-kids-leak
|
||||||
#
|
#
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|
||||||
@@ -95,6 +119,14 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
|
|
||||||
source "$SCRIPT_DIR/../load_config.sh"
|
source "$SCRIPT_DIR/../load_config.sh"
|
||||||
|
|
||||||
|
# --move is a script-local flag, not one parse_args recognizes — check the raw args before
|
||||||
|
# they get filtered into PARSED_ARGS.
|
||||||
|
MOVE_MODE=false
|
||||||
|
for _arg in "$@"; do
|
||||||
|
[[ "$_arg" == "--move" ]] && MOVE_MODE=true
|
||||||
|
done
|
||||||
|
unset _arg
|
||||||
|
|
||||||
parse_args "$@"
|
parse_args "$@"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
@@ -126,10 +158,12 @@ if [[ "$SHOW_STATUS" == true ]]; then
|
|||||||
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||||
echo "$ICON_GEAR Sonarr URL: $SONARR_URL"
|
echo "$ICON_GEAR Sonarr URL: $SONARR_URL"
|
||||||
echo "$ICON_GEAR TV root: $SONARR_TV_ROOT"
|
echo "$ICON_GEAR TV root: $SONARR_TV_ROOT"
|
||||||
|
echo "$ICON_GEAR General root: ${SONARR_GENERAL_ROOT:-<not configured>}"
|
||||||
echo "$ICON_GEAR Kids root: ${SONARR_KIDS_ROOT:-<not configured>}"
|
echo "$ICON_GEAR Kids root: ${SONARR_KIDS_ROOT:-<not configured>}"
|
||||||
echo "$ICON_GEAR Anime root: ${SONARR_ANIME_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 Anime networks: ${#SONARR_ANIME_NETWORKS[@]} curated"
|
||||||
echo "$ICON_GEAR Kids networks: ${#SONARR_KIDS_NETWORKS[@]} curated"
|
echo "$ICON_GEAR Kids networks: ${#SONARR_KIDS_NETWORKS[@]} curated"
|
||||||
|
echo "$ICON_GEAR Move mode: $MOVE_MODE"
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -188,6 +222,7 @@ RESULTS=$(echo "$SERIES_RESPONSE" | jq \
|
|||||||
map(
|
map(
|
||||||
{
|
{
|
||||||
title, id, network, certification, rootFolderPath,
|
title, id, network, certification, rootFolderPath,
|
||||||
|
episodeFileCount: (.statistics.episodeFileCount // 0),
|
||||||
is_anime: is_anime,
|
is_anime: is_anime,
|
||||||
is_kids: is_kids
|
is_kids: is_kids
|
||||||
} |
|
} |
|
||||||
@@ -225,4 +260,156 @@ echo "$ICON_WARN Reverse — kids leak: $REVERSE_KIDS_COUNT (in ${SONARR_K
|
|||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
[[ "$ENABLE_LOGGING" != true ]] && echo " (run with --log for the per-title list)"
|
[[ "$ENABLE_LOGGING" != true ]] && echo " (run with --log for the per-title list)"
|
||||||
|
|
||||||
|
# ==============================================================================================
|
||||||
|
# ━━━ Move Mode ━━━
|
||||||
|
# ==============================================================================================
|
||||||
|
# See MOVE MODE in the header for scope (forward + reverse-kids-leak, not reverse-anime-leak).
|
||||||
|
if [[ "$MOVE_MODE" == true ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "━━━ $ICON_SYNC Move Mode ━━━"
|
||||||
|
|
||||||
|
acquire_lock "wait"
|
||||||
|
trap "_release_all_locks" EXIT
|
||||||
|
|
||||||
|
build_arr_path_map "SONARR"
|
||||||
|
|
||||||
|
MOVE_TARGETS=$(echo "$RESULTS" | jq -c '[.[] | select(.forward_anime_miss or .forward_kids_miss or .reverse_kids_leak)]')
|
||||||
|
MOVE_COUNT=$(echo "$MOVE_TARGETS" | jq 'length')
|
||||||
|
|
||||||
|
if [[ "$MOVE_COUNT" -eq 0 ]]; then
|
||||||
|
info "Nothing to move"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn "About to process $MOVE_COUNT series — one at a time, verifying after each"
|
||||||
|
|
||||||
|
MOVED=0
|
||||||
|
RELOCATED_SEARCH=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
while IFS= read -r item; do
|
||||||
|
id=$(echo "$item" | jq -r '.id')
|
||||||
|
title=$(echo "$item" | jq -r '.title')
|
||||||
|
is_anime_flag=$(echo "$item" | jq -r '.is_anime')
|
||||||
|
is_forward_kids=$(echo "$item" | jq -r '.forward_kids_miss')
|
||||||
|
had_files_count=$(echo "$item" | jq -r '.episodeFileCount')
|
||||||
|
|
||||||
|
if [[ "$is_anime_flag" == "true" ]]; then
|
||||||
|
target_root="$SONARR_ANIME_ROOT"
|
||||||
|
elif [[ "$is_forward_kids" == "true" ]]; then
|
||||||
|
target_root="$SONARR_KIDS_ROOT"
|
||||||
|
else
|
||||||
|
target_root="$SONARR_GENERAL_ROOT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$target_root" ]]; then
|
||||||
|
error " ✗ $title — target root not configured (SONARR_GENERAL_ROOT blank), skipping"
|
||||||
|
(( FAILED++ ))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# RESULTS only carries the reduced report fields — Sonarr's PUT expects the complete
|
||||||
|
# resource representation, so fetch a fresh full series record to modify and send back.
|
||||||
|
full_series=$(arr_api "$SONARR_URL" "$SONARR_API_KEY" "v3" "series/$id" "Sonarr")
|
||||||
|
if [[ -z "$full_series" ]]; then
|
||||||
|
error " ✗ $title — could not fetch full series record, skipping"
|
||||||
|
(( FAILED++ ))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
old_path=$(echo "$full_series" | jq -r '.path')
|
||||||
|
folder_name="${old_path##*/}"
|
||||||
|
|
||||||
|
# A literal "/" in the folder name would build a broken nested directory instead of
|
||||||
|
# moving to one clean folder — this is exactly the self-inflicted bug hit doing the
|
||||||
|
# Fate/Zero and Fate/Stay Night moves by hand earlier this session.
|
||||||
|
if [[ "$folder_name" == *"/"* ]]; then
|
||||||
|
error " ✗ $title — folder name contains '/', skipping (needs manual handling)"
|
||||||
|
(( FAILED++ ))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
new_path="${target_root}/${folder_name}"
|
||||||
|
|
||||||
|
if [[ "$had_files_count" -gt 0 ]]; then
|
||||||
|
info " → $title: $old_path → $new_path (moving $had_files_count episode file(s))"
|
||||||
|
move_qs="?moveFiles=true"
|
||||||
|
else
|
||||||
|
info " → $title: $old_path → $new_path (no files — relocating + search)"
|
||||||
|
move_qs=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
updated_series=$(echo "$full_series" | jq --arg root "$target_root" --arg path "$new_path" \
|
||||||
|
'.rootFolderPath = $root | .path = $path')
|
||||||
|
|
||||||
|
http_code=$(curl -sf -o /dev/null -w "%{http_code}" -X PUT \
|
||||||
|
--max-time 30 \
|
||||||
|
-H "X-Api-Key: $SONARR_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$updated_series" \
|
||||||
|
"${SONARR_URL}/api/v3/series/${id}${move_qs}" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" && "$http_code" != "202" ]]; then
|
||||||
|
error " ✗ $title — API returned HTTP $http_code — stopping (review before re-running)"
|
||||||
|
(( FAILED++ ))
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Never trust the PUT response alone — re-fetch and confirm the change actually landed.
|
||||||
|
# This exact check is what caught the earlier race condition doing this by hand: two
|
||||||
|
# series reported "success" while episodeFileCount had silently dropped to 0.
|
||||||
|
verify_series=$(arr_api "$SONARR_URL" "$SONARR_API_KEY" "v3" "series/$id" "Sonarr")
|
||||||
|
verify_root=$(echo "$verify_series" | jq -r '.rootFolderPath')
|
||||||
|
verify_filecount=$(echo "$verify_series" | jq -r '.statistics.episodeFileCount // 0')
|
||||||
|
|
||||||
|
if [[ "$verify_root" != "$target_root" ]]; then
|
||||||
|
error " ✗ $title — verification failed (root: $verify_root) — stopping"
|
||||||
|
(( FAILED++ ))
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$had_files_count" -gt 0 ]]; then
|
||||||
|
if [[ "$verify_filecount" -eq "$had_files_count" ]]; then
|
||||||
|
echo " $ICON_SUCCESS $title — moved and verified ($verify_filecount files)"
|
||||||
|
(( MOVED++ ))
|
||||||
|
else
|
||||||
|
error " ✗ $title — verification failed (root updated but episode count $verify_filecount != expected $had_files_count) — stopping"
|
||||||
|
(( FAILED++ ))
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
search_code=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
|
||||||
|
--max-time 30 \
|
||||||
|
-H "X-Api-Key: $SONARR_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"name\":\"SeriesSearch\",\"seriesId\":${id}}" \
|
||||||
|
"${SONARR_URL}/api/v3/command" 2>/dev/null)
|
||||||
|
if [[ "$search_code" == "200" || "$search_code" == "201" ]]; then
|
||||||
|
echo " $ICON_SUCCESS $title — relocated, search triggered"
|
||||||
|
else
|
||||||
|
warn " $title — relocated but search trigger returned HTTP $search_code (will pick up on next scheduled search)"
|
||||||
|
fi
|
||||||
|
(( RELOCATED_SEARCH++ ))
|
||||||
|
fi
|
||||||
|
done < <(echo "$MOVE_TARGETS" | jq -c '.[]')
|
||||||
|
|
||||||
|
# arr_get_tracked_data() is cache-first — every write above changed rootFolderPath, so the
|
||||||
|
# shared cache is now stale until the next scheduled arr_cache_prefill run. Refresh it now
|
||||||
|
# rather than leave that window open for every other script reading this cache.
|
||||||
|
if [[ "$(( MOVED + RELOCATED_SEARCH ))" -gt 0 ]]; then
|
||||||
|
info "Refreshing shared tracked-data cache..."
|
||||||
|
fresh_series=$(arr_api "$SONARR_URL" "$SONARR_API_KEY" "v3" "series" "Sonarr")
|
||||||
|
[[ -n "$fresh_series" ]] && arr_cache_write "sonarr" "$fresh_series"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━ $ICON_SUMMARY MOVE SUMMARY ━━━━━"
|
||||||
|
echo "$ICON_SUCCESS Moved (files relocated): $MOVED"
|
||||||
|
echo "$ICON_SUCCESS Relocated + search triggered: $RELOCATED_SEARCH"
|
||||||
|
echo "$ICON_ERROR Failed: $FAILED"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
fi
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -436,6 +436,7 @@
|
|||||||
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_GENERAL_ROOT="" # rootFolderPath literal for the general root (e.g. "/tv") — target for reverse-kids-leak moves; leave blank to disable
|
||||||
HOSTN_SONARR_KIDS_ROOT="" # rootFolderPath literal, as reported by Sonarr API — leave blank if no dedicated kids root
|
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
|
HOSTN_SONARR_ANIME_ROOT="" # rootFolderPath literal, as reported by Sonarr API — leave blank if no dedicated anime root
|
||||||
|
|
||||||
|
|||||||
@@ -586,6 +586,7 @@ 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_GENERAL_ROOT_VAR="${MY_ID}_SONARR_GENERAL_ROOT"; SONARR_GENERAL_ROOT="${!SONARR_GENERAL_ROOT_VAR:-}"
|
||||||
SONARR_KIDS_ROOT_VAR="${MY_ID}_SONARR_KIDS_ROOT"; SONARR_KIDS_ROOT="${!SONARR_KIDS_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_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}"
|
||||||
|
|||||||
Reference in New Issue
Block a user