I wrote a script to automatically block and purge fake releases (exe/scr/lnk) from Transmission and Sonarr
Like many of you, I got tired of Sonarr occasionally grabbing fake, malicious releases packed with .exe, .scr, or .lnk files instead of actual video files. Cleaning them out manually, removing them from Transmission, and blocklisting them in Sonarr gets old fast.
I didn't want to switch to a different torrent client just for this, so I wrote a Bash script that acts as a Transmission event hook (script-torrent-added and script-torrent-done).
What it does:
- Checks the Label: It verifies if the added torrent matches your Sonarr label so it doesn't mess with your personal, non-Sonarr downloads.
- Waits for Metadata: If it’s a magnet link, it safely waits for the file list to populate.
- Scans for Fakes: It checks the file extensions against a forbidden list (
exe|scr|lnk|bat|vbs). - Triggers Sonarr API: If a fake is detected, it hits the Sonarr API to fail the download, add it to Sonarr's blocklist (so it searches for a new release), and instructs Sonarr to remove it from the client.
- Fail-safe Cleanup: If Sonarr doesn't catch it for some reason, the script forces Transmission to remove the torrent and wipe the local data anyway.
The Script
#!/usr/bin/env bash
# sonarr-block-fakes.sh — purge fake releases from Transmission/Sonarr.
# Invoked by Transmission as script-torrent-added / script-torrent-done.
set -euo pipefail
SONARR_URL="http://localhost:8989/" # change it to your sonnar endpoint.
SONARR_API_KEY="" # get it from your sonarr settings.
TRANSMISSION_HOST="127.0.0.1:9091" # change it to your transmission endpoint.
TRANSMISSION_AUTH="" # e.g. "-n user:pass" if rpc-authentication-required is true
SONARR_LABEL="sonarr-downloads" # the label defined in your sonarr instance for transmission.
FORBIDDEN_EXT="exe|scr|lnk|bat|vbs"
LOG_FILE="${LOG_FILE:-/PATH/TO/LOGS/clean_sonarr_fakes.log}"
mkdir -p "$(dirname "$LOG_FILE")"
log() { printf '%s [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "${TR_TORRENT_NAME:-?}" "$*" >> "$LOG_FILE"; }
: "${TR_TORRENT_ID:?TR_TORRENT_ID not set — run me from Transmission}"
: "${TR_TORRENT_HASH:?TR_TORRENT_HASH not set}"
TR_TORRENT_DIR="${TR_TORRENT_DIR:-}"
TR_TORRENT_NAME="${TR_TORRENT_NAME:-id-$TR_TORRENT_ID}"
log "Triggered: id=$TR_TORRENT_ID hash=$TR_TORRENT_HASH dir=$TR_TORRENT_DIR labels=${TR_TORRENT_LABELS:-}"
tr_remote() {
# shellcheck disable=SC2086
transmission-remote "$TRANSMISSION_HOST" $TRANSMISSION_AUTH "$@"
}
labels="${TR_TORRENT_LABELS:-}"
if [[ -z "$labels" ]]; then
labels="$(tr_remote -t "$TR_TORRENT_ID" -i 2>/dev/null | awk -F': ' '/^[[:space:]]*Labels:/ {print $2; exit}')"
fi
label_matched=0
IFS=',' read -ra _labels <<< "$labels"
for l in "${_labels[@]}"; do
# trim whitespace
l="${l#"${l%%[![:space:]]*}"}"
l="${l%"${l##*[![:space:]]}"}"
if [[ "${l,,}" == "${SONARR_LABEL,,}" ]]; then
label_matched=1
break
fi
done
if (( label_matched == 0 )); then
echo "Not a Sonarr download, skipping check."
log "Skip: torrent labels ('$labels') do not include SONARR_LABEL ($SONARR_LABEL)"
exit 0
fi
# Magnet metadata can take 10-60s to arrive after script-torrent-added.
META_WAIT_TRIES=30
META_WAIT_SLEEP=2
files=""
metadata_ready=0
for _ in $(seq 1 "$META_WAIT_TRIES"); do
files="$(tr_remote -t "$TR_TORRENT_ID" -f 2>/dev/null || true)"
if [[ "$(printf '%s\n' "$files" | wc -l)" -gt 2 ]]; then
metadata_ready=1
break
fi
sleep "$META_WAIT_SLEEP"
done
if [[ -z "$files" ]]; then
log "ERROR: could not list files via transmission-remote"
exit 1
fi
if (( metadata_ready == 0 )); then
log "WARN: file metadata not available after $((META_WAIT_TRIES * META_WAIT_SLEEP))s — script-torrent-done hook will retry on completion"
exit 0
fi
offending="$(printf '%s\n' "$files" | grep -Ei "\.($FORBIDDEN_EXT)([[:space:]]|$)" || true)"
if [[ -z "$offending" ]]; then
log "Clean — no forbidden extensions found"
exit 0
fi
log "FAKE DETECTED. Offending entries:"
log "$offending"
hash_upper="${TR_TORRENT_HASH^^}"
queue_json="$(curl -fsS -m 15 -H "X-Api-Key: $SONARR_API_KEY" "$SONARR_URL/api/v3/queue?pageSize=1000&includeUnknownSeriesItems=true" || true)"
queue_id=""
if [[ -n "$queue_json" ]]; then
queue_id="$(printf '%s' "$queue_json" | jq -r --arg h "$hash_upper" '.records[]? | select((.downloadId // "" | ascii_upcase) == $h) | .id' | head -n1)"
fi
if [[ -n "$queue_id" ]]; then
log "Sonarr queue id=$queue_id — failing + blocklisting"
http_code="$(curl -sS -m 15 -o /dev/null -w '%{http_code}' -X DELETE -H "X-Api-Key: $SONARR_API_KEY" "$SONARR_URL/api/v3/queue/${queue_id}?removeFromClient=true&blocklist=true&skipRedownload=false" || echo "000")"
log "Sonarr DELETE /queue/$queue_id -> HTTP $http_code"
else
log "No matching Sonarr queue entry for hash $hash_upper — purging Transmission only"
fi
if tr_remote -t "$TR_TORRENT_ID" -rad >>"$LOG_FILE" 2>&1; then
log "Transmission: removed torrent + deleted local data"
else
log "Transmission: remove returned non-zero (may already be gone via Sonarr)"
fi
exit 0
Setup Notes:
- You'll need
jq,curl, andtransmission-remoteinstalled on the host running the script. - Update the configuration variables at the top (
SONARR_URL,SONARR_API_KEY,LOG_FILE, etc.). - Make the script executable (
chmod +x) and add the path to your Transmission settings file (settings.json) underscript-torrent-added-filenameandscript-torrent-done-filename. Make surescript-torrent-added-enabledis set totrue.
Hope this helps anyone else trying to keep their Transmission setup clean without babysitting the queue. Let me know if you have any questions or improvements.