A direct arr_cache_write() call mid-rescan wrote a partial snapshot that looked like real data loss to every consumer of the cache. The guard now lives in arr_cache_write() itself so every caller is protected, not just arr_get_tracked_data(). arr_rescan_monitor.sh closes the resulting gap for rescans triggered outside arr_full_rescan.sh's own trigger-and-wait path.
899 lines
28 KiB
Markdown
899 lines
28 KiB
Markdown
# ━━━━━ TOOLS — Manual ━━━━━
|
||
|
||
Configuration reference, usage procedures, and field guides for every script
|
||
in `Tools/`. Run any script with `--status` first — it shows current state before
|
||
making any changes.
|
||
|
||
---
|
||
|
||
## ━━━ CONTENTS ━━━
|
||
|
||
- [emby_to_lidarr_sync.sh](#emby_to_lidarr_syncsh)
|
||
- [emby_to_sonarr_sync.sh](#emby_to_sonarr_syncsh)
|
||
- [emby_to_radarr_sync.sh](#emby_to_radarr_syncsh)
|
||
- [fallback_state_reset.sh](#fallback_state_resetsh)
|
||
- [docker_prune_images.sh](#docker_prune_imagessh)
|
||
- [watchdog_skip_list_manager.sh](#watchdog_skip_list_managersh)
|
||
- [bulk_permissions_repair.sh](#bulk_permissions_repairsh)
|
||
- [container_data_export.sh](#container_data_exportsh)
|
||
- [emby_database_repair.sh](#emby_database_repairsh)
|
||
- [zfs_pool_scrub.sh](#zfs_pool_scrubsh)
|
||
- [smart_long_test.sh](#smart_long_testsh)
|
||
- [arr_profile_enforcer.sh](#arr_profile_enforcersh)
|
||
- [webhook_setup.sh](#webhook_setupsh)
|
||
- [ramdisk_stop.sh](#ramdisk_stopsh)
|
||
- [arr_rescan_monitor.sh](#arr_rescan_monitorsh)
|
||
- [Adding a New Tool](#adding-a-new-tool)
|
||
|
||
---
|
||
|
||
## Output Tiers
|
||
|
||
All tools have two output levels controlled by `--log`.
|
||
|
||
Without `--log`, each script processes and always concludes with a summary block
|
||
showing identity, duration, counts, and a status line. Warnings and errors are
|
||
always visible. State-display scripts (watchdog_skip_list_manager, zfs_pool_scrub
|
||
`--status`, fallback_state_reset current-state section) always show their state
|
||
output — `--log` adds configuration detail and per-item resolution within each
|
||
section.
|
||
|
||
With `--log`, per-item detail appears: individual items added/skipped, per-database
|
||
check results, per-pool scan lines, per-container stop/start state, per-directory
|
||
creation results. Use when debugging unexpected results or confirming a first run.
|
||
|
||
---
|
||
|
||
## emby_to_lidarr_sync.sh
|
||
|
||
One-shot bootstrap tool. Scans Emby play history, finds artists you've actually
|
||
listened to that are not yet tracked in Lidarr, and adds them. No scoring — if
|
||
it was played, Lidarr should monitor it. Not scheduled; run manually when you
|
||
want to close the gap between what's in your library and what Lidarr watches.
|
||
|
||
### When to Use
|
||
|
||
- After initial Lidarr setup — bring it in line with existing listening history
|
||
- After a Lidarr database wipe or migration
|
||
- Any time you suspect artists you listen to are slipping through unmonitored
|
||
|
||
### Usage
|
||
|
||
```bash
|
||
# See what would be added (no changes)
|
||
bash Tools/emby_to_lidarr_sync.sh --dry-run
|
||
|
||
# Limit to recent plays only
|
||
bash Tools/emby_to_lidarr_sync.sh --dry-run --days 30
|
||
|
||
# Run for real
|
||
bash Tools/emby_to_lidarr_sync.sh
|
||
```
|
||
|
||
### Notes
|
||
|
||
- Reads all MusicAlbum items from Emby and extracts AlbumArtist — primary album
|
||
artists only, not guest features or tag credits
|
||
- Filters out VA, Various Artists, and other metadata placeholders
|
||
- Triggers `ArtistSearch` immediately after each successful add — no manual search needed
|
||
- Activity log has a finite history; use `--days N` if the log has been pruned
|
||
|
||
---
|
||
|
||
## emby_to_sonarr_sync.sh
|
||
|
||
One-shot bootstrap tool. Finds TV series present in Emby that are not tracked in
|
||
Sonarr and adds them. Uses TVDB ID matching when available (more reliable than
|
||
title matching), falling back to case-insensitive title comparison.
|
||
|
||
### When to Use
|
||
|
||
- After initial Sonarr setup — bring it in line with your existing library
|
||
- After a Sonarr database wipe or migration
|
||
- Any time series you own are slipping through unmonitored
|
||
|
||
### Usage
|
||
|
||
```bash
|
||
# See what would be added (no changes)
|
||
bash Tools/emby_to_sonarr_sync.sh --dry-run
|
||
|
||
# Run for real
|
||
bash Tools/emby_to_sonarr_sync.sh
|
||
```
|
||
|
||
### Notes
|
||
|
||
- Triggers `SeriesSearch` immediately after each successful add — Sonarr begins
|
||
searching for missing episodes right away
|
||
- Adds to the first accessible root folder in Sonarr
|
||
- TVDB ID match preferred over title; title fallback handles edge cases
|
||
- Requires `SONARR_EMBY_LIBRARIES` configured in `master.conf`
|
||
|
||
---
|
||
|
||
## emby_to_radarr_sync.sh
|
||
|
||
One-shot bootstrap tool. Finds movies present in Emby that are not tracked in
|
||
Radarr and adds them. Uses TMDB ID matching when available, falling back to
|
||
case-insensitive title comparison.
|
||
|
||
### When to Use
|
||
|
||
- After initial Radarr setup — bring it in line with your existing library
|
||
- After a Radarr database wipe or migration
|
||
- Any time movies you own are slipping through unmonitored
|
||
|
||
### Usage
|
||
|
||
```bash
|
||
# See what would be added (no changes)
|
||
bash Tools/emby_to_radarr_sync.sh --dry-run
|
||
|
||
# Run for real
|
||
bash Tools/emby_to_radarr_sync.sh
|
||
```
|
||
|
||
### Notes
|
||
|
||
- Triggers `MoviesSearch` immediately after each successful add — Radarr begins
|
||
searching for the movie right away
|
||
- Adds to the first accessible root folder in Radarr
|
||
- TMDB ID match preferred over title; title fallback handles edge cases
|
||
|
||
---
|
||
|
||
## fallback_state_reset.sh
|
||
|
||
Resets the fallback state file to NORMAL and clears all tier flags. State file only —
|
||
does NOT start or stop any containers. After reset, fallback.sh resumes from NORMAL on
|
||
its next cycle.
|
||
|
||
**Only run after verifying the stack is actually in a normal state** — right containers
|
||
on the right server, DDNS correct, no active fallback in progress. Resetting state during
|
||
a real fallback causes fallback.sh to stop covering the remote until the next detection cycle.
|
||
|
||
### When to Use
|
||
|
||
```
|
||
After fallback_test.sh didn't complete cleanly
|
||
→ state left in FALLBACK but containers are actually back to normal
|
||
|
||
After a failed handback
|
||
→ state shows FALLBACK but remote is back up and containers are split
|
||
|
||
After killing fallback.sh directly (not gracefully via SIGTERM)
|
||
→ state is unknown, cycle was interrupted mid-operation
|
||
|
||
After a dev/debug session
|
||
→ state left in a non-NORMAL state from testing
|
||
```
|
||
|
||
### Usage
|
||
|
||
```bash
|
||
fallback_state_reset.sh # show current state, prompt for YES before resetting
|
||
fallback_state_reset.sh --status # show current state file contents only
|
||
fallback_state_reset.sh --dry-run # show what the new state file would contain, no write
|
||
fallback_state_reset.sh --force # reset without confirmation prompt (for scripted use)
|
||
```
|
||
|
||
### Verify Before Resetting
|
||
|
||
```bash
|
||
# Right containers on right server?
|
||
cat "$STATE_DIR/fallback_state" # shows fallback current state
|
||
|
||
# DDNS pointing correctly?
|
||
nslookup Gmer4Lfe.com # confirm it resolves to the right IP
|
||
|
||
# fallback.sh not running?
|
||
pgrep -f "fallback.sh" # empty output = not running
|
||
|
||
# Both servers Tailscale connected?
|
||
tailscale status # both hosts should show active
|
||
```
|
||
|
||
### What the State File Contains
|
||
|
||
```bash
|
||
state=NORMAL
|
||
fallback_start=0
|
||
handback_strikes=0
|
||
tier2_started=false
|
||
tier3_started=false
|
||
tier4_started=false
|
||
```
|
||
|
||
---
|
||
|
||
## watchdog_skip_list_manager.sh
|
||
|
||
View and manage the persistent container skip list used by `docker_watchdog.sh`.
|
||
|
||
### When to Use
|
||
|
||
```
|
||
docker_watchdog.sh restarts the same container N times within the rolling window
|
||
→ container added to skip list in $STATE_DIR
|
||
→ critical notification sent
|
||
→ watchdog stops touching it entirely
|
||
|
||
You fix the underlying problem (database, config, dependencies).
|
||
You need to clear the container from the skip list so monitoring resumes.
|
||
```
|
||
|
||
### Recovery Workflow
|
||
|
||
```bash
|
||
# 1. Understand the situation — always start here:
|
||
watchdog_skip_list_manager.sh --status
|
||
# Shows: skip list contents, which are running vs. stopped, restart history
|
||
|
||
# 2. Fix the underlying problem first
|
||
# Check logs: docker logs ContainerName --tail 100
|
||
# Check disk: df -h /mnt/user
|
||
# Check db: docker exec ContainerName sqlite3 /path/to.db ".tables"
|
||
|
||
# 3. Clear from skip list + restart history:
|
||
watchdog_skip_list_manager.sh --clear ContainerName
|
||
|
||
# 4. Start the container manually — confirm your fix worked:
|
||
docker start ContainerName
|
||
|
||
# 5. Watchdog resumes normal monitoring on next cycle — no further action needed
|
||
```
|
||
|
||
### State Files Managed
|
||
|
||
```bash
|
||
# Both live on /boot/config — survive reboots intentionally.
|
||
# A container that was skip-listed before a reboot is still broken after it.
|
||
|
||
$SYS_WATCHDOG_FAILED_FILE # persistent skip list
|
||
$WATCHDOG_CONTAINER_RESTART_LOG # restart loop tracking
|
||
```
|
||
|
||
### Configuration (master.conf)
|
||
|
||
```bash
|
||
WATCHDOG_CONTAINER_RESTART_LIMIT=3 # restarts before skip-listing
|
||
WATCHDOG_CONTAINER_RESTART_WINDOW=1 # rolling window in hours
|
||
```
|
||
|
||
### Usage
|
||
|
||
```bash
|
||
watchdog_skip_list_manager.sh # show status (default)
|
||
watchdog_skip_list_manager.sh --status # explicit status
|
||
watchdog_skip_list_manager.sh --clear ContainerName # clear specific + restart history
|
||
watchdog_skip_list_manager.sh --clear ContainerName --force # no confirmation prompt
|
||
watchdog_skip_list_manager.sh --clear-all # clear everything
|
||
watchdog_skip_list_manager.sh --clear-all --force # non-interactive
|
||
watchdog_skip_list_manager.sh --dry-run # preview any clear action
|
||
```
|
||
|
||
---
|
||
|
||
## bulk_permissions_repair.sh
|
||
|
||
Applies correct ownership and permissions to specific paths. Faster than running
|
||
`media_shares_permissions.sh` which processes every configured share — use this when
|
||
you know exactly what needs fixing and don't want to wait for a full library walk.
|
||
|
||
### When to Use
|
||
|
||
```
|
||
Admin copy left root:root files — scp, cp, direct file transfer
|
||
New share needs permissions now — can't wait for nightly run
|
||
Container wrote as root — before PUID/PGID was fixed
|
||
Specific directory has wrong perms — targeted fix, not a full library walk
|
||
```
|
||
|
||
Use the full `media_shares_permissions.sh` instead for:
|
||
- Regular nightly maintenance (already scheduled in daily_sync_maintenance.sh)
|
||
- After confirming a container's PUID/PGID is now correct
|
||
- Initial permissions setup on a new server
|
||
|
||
### Diagnosing High Wrong-Owner Counts
|
||
|
||
The script counts files with wrong ownership before applying the fix. A high count
|
||
on a share that was recently written means a container has wrong PUID/PGID.
|
||
|
||
```bash
|
||
# Fix: add to the container's Docker template:
|
||
PUID=99
|
||
PGID=100
|
||
|
||
# Common culprits writing as root:
|
||
# SABnzbd, qBittorrent, slskd — check each one's Docker env vars
|
||
```
|
||
|
||
### Configuration (master.conf)
|
||
|
||
```bash
|
||
PERMISSIONS_OWNER="nobody:users" # matches PUID=99 PGID=100
|
||
PERMISSIONS_DIR_MODE="755" # directories — enter, list, no world-write
|
||
PERMISSIONS_FILE_MODE="664" # files — owner+group rw, others read-only
|
||
```
|
||
|
||
### Usage
|
||
|
||
```bash
|
||
# Single path:
|
||
bulk_permissions_repair.sh /mnt/user/Movies
|
||
|
||
# Multiple paths — all corrected in one run:
|
||
bulk_permissions_repair.sh /mnt/user/Movies /mnt/user/Tv_Shows /mnt/user/Music
|
||
|
||
# Dry run first — shows count of files with wrong ownership per path:
|
||
bulk_permissions_repair.sh /mnt/user/Movies --dry-run
|
||
|
||
# Verbose — show each corrected file:
|
||
bulk_permissions_repair.sh /mnt/user/Movies --log
|
||
```
|
||
|
||
---
|
||
|
||
## container_data_export.sh
|
||
|
||
Exports a container's appdata directory to a compressed tar archive. Stops the
|
||
container first for a clean consistent backup, verifies the archive after creation,
|
||
then restarts the container.
|
||
|
||
### When to Use
|
||
|
||
```
|
||
Before major container updates — especially "database migration — no rollback" changelogs
|
||
Before pool migrations — clean backup before moving appdata to a new pool
|
||
Before removing a container from the stack — archive its data before deletion
|
||
Manual point-in-time backup before risky config changes
|
||
```
|
||
|
||
### Export Sequence
|
||
|
||
```
|
||
1. Space check
|
||
Estimates required space from appdata size × 1.1
|
||
Aborts if output directory doesn't have enough free space
|
||
Container is NOT stopped until the space check passes
|
||
|
||
2. Stop container cleanly
|
||
docker stop ContainerName — graceful shutdown
|
||
|
||
3. Create archive
|
||
tar -czf ContainerName_YYYY-MM-DD_HH-MM.tar.gz /path/to/appdata
|
||
|
||
4. Verify archive integrity
|
||
tar --test-file archive.tar.gz — confirms archive is valid and complete
|
||
If verification fails → restart container anyway, report error
|
||
|
||
5. Restart container
|
||
docker start ContainerName — always happens, even if archiving failed
|
||
```
|
||
|
||
### Usage
|
||
|
||
```bash
|
||
# Syntax: container_data_export.sh ContainerName AppDataPath OutputDir
|
||
|
||
# Emby backup:
|
||
container_data_export.sh \
|
||
Emby \
|
||
/mnt/media-servers/Media_Server/Emby \
|
||
/mnt/user/Backups/
|
||
|
||
# Dry run — verify space and paths without stopping anything:
|
||
container_data_export.sh \
|
||
Emby \
|
||
/mnt/media-servers/Media_Server/Emby \
|
||
/mnt/user/Backups/ \
|
||
--dry-run
|
||
|
||
# Output filename: Emby_2026-05-14_02-30.tar.gz
|
||
# Timestamped — safe to run multiple times, no overwrite
|
||
```
|
||
|
||
---
|
||
|
||
## emby_database_repair.sh
|
||
|
||
Stops Emby, runs SQLite `PRAGMA integrity_check` on every Emby database, and restarts.
|
||
Reports per-database — does NOT automatically repair. Recovery requires judgment.
|
||
|
||
### When to Use
|
||
|
||
```
|
||
Emby logs show database errors → run this first
|
||
Emby crashing repeatedly with no clear cause → likely database corruption
|
||
Playback history or user data behaving strangely → users.db or library.db issue
|
||
After a hard shutdown or power loss with Emby running → check for WAL corruption
|
||
```
|
||
|
||
### Recovery Guide by Database
|
||
|
||
```
|
||
library.db — media library metadata: titles, seasons, episodes, artwork
|
||
CORRUPT → safe to delete — Emby fully rebuilds from media files on next start
|
||
Rebuild takes time (hours on large libraries) but loses nothing permanent
|
||
|
||
users.db — user accounts, watch history, playback positions, settings
|
||
CORRUPT → deleting resets ALL user accounts and watch history
|
||
Check for a recent backup (weekly_sync_maintenance.sh mirrors Emby/)
|
||
before deleting — restore from remote if available
|
||
|
||
authentication.db — API keys, session tokens
|
||
CORRUPT → safe to delete — API keys regenerated on restart
|
||
Any connected clients will need to re-authenticate once
|
||
|
||
activity.db — activity/access log
|
||
CORRUPT → safe to delete — it's a log, losing it is acceptable
|
||
|
||
library.db-wal — write-ahead log (uncommitted transactions)
|
||
PRESENT + CORRUPT → check library.db first; WAL corruption usually means
|
||
the main library.db is also affected
|
||
```
|
||
|
||
### Configuration (host*.conf)
|
||
|
||
```bash
|
||
HOST1_EMBY_CONTAINER="Emby" # aliased by detect_hosts() → EMBY_CONTAINER
|
||
HOST2_EMBY_CONTAINER="Emby"
|
||
```
|
||
|
||
Emby's config path is detected automatically from Docker volume mounts — no manual
|
||
path configuration needed.
|
||
|
||
### Usage
|
||
|
||
```bash
|
||
emby_database_repair.sh # stop Emby, check all databases, restart
|
||
emby_database_repair.sh --dry-run # show what would be checked, no Emby stop
|
||
emby_database_repair.sh --log # verbose — show SQLite output per database
|
||
emby_database_repair.sh --status # show Emby config path and database locations
|
||
```
|
||
|
||
---
|
||
|
||
## zfs_pool_scrub.sh
|
||
|
||
Triggers ZFS scrub on all pools (or a specific named pool) and waits for completion.
|
||
Notifies when done with a summary of any errors found.
|
||
|
||
### Why Run ZFS Scrub
|
||
|
||
ZFS stores a checksum with every block of data. Scrub reads every block and verifies
|
||
the checksum matches the stored hash. Silent data corruption can sit on disk for months
|
||
without triggering any error — until you try to read that specific file. By then:
|
||
- It may already be mirrored to HOST2 in its corrupted state
|
||
- The original source may no longer exist
|
||
- ZFS can self-repair during scrub if redundancy exists (RAIDZ or mirrors)
|
||
|
||
Run monthly. Also run after any disk replacement or power event.
|
||
Safe to run while the system is in use — scrub runs at low I/O priority.
|
||
|
||
### Configuration (host*.conf)
|
||
|
||
```bash
|
||
HOST1_ZFS_REPORT_IGNORE_POOLS=(
|
||
"disk10" # JBOD member — no redundancy, skipped from default scrub
|
||
"disk9"
|
||
"disk8"
|
||
)
|
||
|
||
HOST2_ZFS_REPORT_IGNORE_POOLS=(
|
||
"cache" # example — single-disk pool excluded from default
|
||
)
|
||
```
|
||
|
||
To scrub a pool in the ignore list, specify it by name explicitly.
|
||
|
||
### Usage
|
||
|
||
```bash
|
||
# Scrub all pools except those in ZFS_REPORT_IGNORE_POOLS:
|
||
zfs_pool_scrub.sh
|
||
|
||
# Scrub a specific pool by name — bypasses the ignore list:
|
||
zfs_pool_scrub.sh gaming
|
||
|
||
# Check current scrub status without starting a new one:
|
||
zfs_pool_scrub.sh --status
|
||
|
||
# Dry run — show which pools would be scrubbed:
|
||
zfs_pool_scrub.sh --dry-run
|
||
|
||
# Verbose — show scrub progress every 60s poll:
|
||
zfs_pool_scrub.sh --log
|
||
```
|
||
|
||
---
|
||
|
||
## smart_long_test.sh
|
||
|
||
Runs a SMART extended (long) self-test on all drives sequentially and reports results.
|
||
Extended tests read every sector — they catch bad sectors and pre-failure reallocations
|
||
that the short test skips. Called monthly by `monthly_maintenance.sh`.
|
||
|
||
### When to Use
|
||
|
||
```
|
||
Monthly via monthly_maintenance.sh — automatic, no manual trigger needed
|
||
After a disk replacement or rebuild — verify the new drive before it enters production
|
||
After a power cut or hard shutdown — check for newly reallocated sectors
|
||
When a drive shows elevated reallocated sectors in ZFS scrub or system logs
|
||
```
|
||
|
||
### Configuration (host*.conf)
|
||
|
||
```bash
|
||
HOST1_SMART_IGNORE_DRIVES=("/dev/sdb") # boot USB — no useful SMART data
|
||
HOST2_SMART_IGNORE_DRIVES=()
|
||
```
|
||
|
||
Aliased by `detect_hosts()` → `SMART_IGNORE_DRIVES`.
|
||
|
||
### Usage
|
||
|
||
```bash
|
||
# Test all drives (skips SMART_IGNORE_DRIVES):
|
||
smart_long_test.sh
|
||
|
||
# Test a specific drive — bypasses the ignore list:
|
||
smart_long_test.sh /dev/sda
|
||
|
||
# Show last self-test result for all drives without starting a new test:
|
||
smart_long_test.sh --status
|
||
|
||
# Show which drives would be tested, no tests started:
|
||
smart_long_test.sh --dry-run
|
||
|
||
# Verbose — show poll progress every 60s per drive:
|
||
smart_long_test.sh --log
|
||
```
|
||
|
||
### What Gets Reported
|
||
|
||
Each drive reports one of: `completed without error`, `completed with errors`,
|
||
`interrupted`, `in progress`. Any result other than `completed without error`
|
||
triggers a notification. Drives are tested one at a time — total runtime depends
|
||
on drive count and size (a full 8TB HDD takes roughly 90–180 minutes).
|
||
|
||
---
|
||
|
||
## arr_profile_enforcer.sh
|
||
|
||
Ensures every series in Sonarr and every movie in Radarr is on the correct quality
|
||
profile based on where it lives on disk. Only touches items with the wrong profile —
|
||
idempotent, safe to re-run.
|
||
|
||
### Profile Rules
|
||
|
||
```
|
||
Root folder path contains "kids" or "anime" → ARR_KIDS_PROFILE_NAME
|
||
All other root folders → ARR_SONARR_DEFAULT_PROFILE (Sonarr)
|
||
ARR_RADARR_DEFAULT_PROFILE (Radarr)
|
||
```
|
||
|
||
Profile IDs are resolved from the API at runtime by name — no IDs need to be
|
||
hardcoded and the same script works across hosts.
|
||
|
||
### When to Use
|
||
|
||
```
|
||
After initial arr setup — library was imported with a generic profile
|
||
After adding a new root folder — new imports may use the wrong default
|
||
After a profile rename — profile names in master.conf must match arr exactly
|
||
Any time arr is reporting downloads to the wrong quality level
|
||
```
|
||
|
||
### Configuration (master.conf)
|
||
|
||
```bash
|
||
ARR_KIDS_PROFILE_NAME="Kids shows" # must exactly match the profile name in Sonarr/Radarr
|
||
ARR_SONARR_DEFAULT_PROFILE="Any"
|
||
ARR_RADARR_DEFAULT_PROFILE="Any (mine)"
|
||
```
|
||
|
||
### Usage
|
||
|
||
```bash
|
||
# Dry run first — shows what would change without touching anything:
|
||
arr_profile_enforcer.sh --dry-run
|
||
|
||
# Fix all Sonarr and Radarr libraries:
|
||
arr_profile_enforcer.sh
|
||
|
||
# Sonarr only:
|
||
arr_profile_enforcer.sh --sonarr-only
|
||
|
||
# Radarr only:
|
||
arr_profile_enforcer.sh --radarr-only
|
||
```
|
||
|
||
---
|
||
|
||
## webhook_setup.sh
|
||
|
||
Registers the Varaverk upgrade webhook notification in Sonarr, Radarr, and Lidarr.
|
||
Idempotent — skips any arr that already has the webhook registered. Runs on both
|
||
hosts unless `--local-only` is passed.
|
||
|
||
### When to Use
|
||
|
||
```
|
||
After initial Varaverk install
|
||
After adding a new arr to the stack
|
||
After adding a new host — both hosts need the webhook registered locally
|
||
After WEBHOOK_SECRET was regenerated — re-register to push the new secret
|
||
```
|
||
|
||
### What It Does
|
||
|
||
```
|
||
1. Generates WEBHOOK_SECRET in master.conf if empty
|
||
(openssl rand -hex 32 → written to master.conf in place)
|
||
|
||
2. Registers webhook in local Sonarr, Radarr, and Lidarr
|
||
POST to /api/v3/notification with the correct event triggers
|
||
|
||
3. SSHes to remote host and runs itself --local-only
|
||
Both hosts register using the same secret
|
||
```
|
||
|
||
The webhook URL registered is `http://<local-ip>:<WEBHOOK_PORT>/webhook?key=<secret>` —
|
||
uses the server's own LAN IP so arrs talk directly to the local listener.
|
||
|
||
### Configuration (master.conf)
|
||
|
||
```bash
|
||
WEBHOOK_PORT=7821 # port the listener runs on
|
||
WEBHOOK_SECRET="" # auto-generated on first run if empty
|
||
```
|
||
|
||
### Usage
|
||
|
||
```bash
|
||
# Register on both hosts (normal first-time setup):
|
||
webhook_setup.sh
|
||
|
||
# Local host only (used internally when SSH'd from the other host):
|
||
webhook_setup.sh --local-only
|
||
|
||
# Show what would be registered without making changes:
|
||
webhook_setup.sh --dry-run
|
||
```
|
||
|
||
---
|
||
|
||
|
||
## ramdisk_stop.sh
|
||
|
||
Safely stops the transcode ramdisk: redirects the transcode symlink to the SSD
|
||
fallback first (so Emby continues writing without interruption), then unmounts the
|
||
tmpfs and updates the state file. Primary use case is stopping the current ramdisk
|
||
before re-running `ramdisk_setup.sh` with new size or threshold values.
|
||
|
||
### When to Use
|
||
|
||
```
|
||
Bumping RAMDISK_SIZE — setup script is idempotent, skips remount if already mounted
|
||
→ stop first, then re-run ramdisk_setup.sh with new HOST*_RAMDISK_SIZE value
|
||
|
||
Adjusting RAMDISK_WARN_GB / RAMDISK_LOW_GB thresholds
|
||
→ no need to stop for threshold changes (transcode_manager reads vars live)
|
||
→ only needed if you're also changing the size
|
||
|
||
Temporarily freeing ramdisk RAM — reclaim tmpfs back to general memory pool
|
||
→ stop, restart later with ramdisk_setup.sh
|
||
```
|
||
|
||
### Stop Sequence
|
||
|
||
```
|
||
1. Redirect symlink: TRANSCODE_LINK → TRANSCODE_SSD
|
||
Emby immediately writes to SSD — no broken-path window during unmount
|
||
|
||
2. Check for active transcode files on ramdisk (warn, don't block)
|
||
Files in progress on the ramdisk are lost on unmount — expected for maintenance
|
||
|
||
3. Unmount ramdisk
|
||
Regular umount first; if busy (directory handles only, no active writes)
|
||
falls back to lazy unmount automatically
|
||
|
||
4. Update /tmp/transcode_state.db → current_target=TRANSCODE_SSD
|
||
transcode_manager.sh reads this on its next cycle
|
||
```
|
||
|
||
### transcode_manager Warning
|
||
|
||
If `transcode_manager.sh` is running, it may flip the symlink back to the ramdisk
|
||
on its next cycle (once the ramdisk is unmounted, that flip will fail). Stop
|
||
`transcode_manager.sh` first if you need the SSD redirect to hold before remounting.
|
||
|
||
### After Stopping
|
||
|
||
```bash
|
||
# Update host*.conf with new size values:
|
||
# HOST1_RAMDISK_SIZE="10G"
|
||
# HOST1_RAMDISK_WARN_GB=8.5
|
||
# HOST1_RAMDISK_LOW_GB=7
|
||
|
||
# Remount at new size:
|
||
bash Transcodes/ramdisk_setup.sh
|
||
```
|
||
|
||
### Usage
|
||
|
||
```bash
|
||
ramdisk_stop.sh --status # show mount state, symlink, active files — always check first
|
||
ramdisk_stop.sh --dry-run # show what would happen without making changes
|
||
ramdisk_stop.sh # stop the ramdisk
|
||
ramdisk_stop.sh --log # verbose — show each step
|
||
```
|
||
|
||
---
|
||
|
||
## arr_rescan_monitor.sh
|
||
|
||
Waits for an already-active rescan-type command (RescanFolders/RescanSeries/RescanMovie,
|
||
DownloadedXScan, RefreshX) on the given arr to finish, then refreshes the shared tracked-data
|
||
cache (`arr_cache_write()` in common.sh) with the real post-scan numbers. Triggers nothing
|
||
itself — for watching a rescan that was already started some other way (manual API call,
|
||
the arr's own UI, an ad-hoc debugging session).
|
||
|
||
### When to Use
|
||
|
||
```
|
||
You manually trigger a RescanFolders/RescanSeries/RescanMovie outside arr_full_rescan.sh
|
||
→ arr_cache_write() correctly refuses to write while it's active (2026-07-17 — a mid-scan
|
||
write once read as real data loss to every consumer of the cache)
|
||
→ nothing writes the real number through once the scan finishes unless something is
|
||
watching for completion — that's this tool
|
||
|
||
arr_full_rescan.sh's weekly run finds one arr already mid-rescan and skips it
|
||
→ that arr's cache stays stale until its own rescan finishes; run this to close the gap
|
||
without waiting for next week's scheduled run
|
||
```
|
||
|
||
### What It Does NOT Do
|
||
|
||
Does not trigger a rescan — if none is active, it exits immediately with nothing to wait
|
||
for. Use the arr's own UI/API, or `Arrs_Stack/arr_full_rescan.sh`, to start one.
|
||
|
||
### Usage
|
||
|
||
```bash
|
||
# Wait for lidarr's active rescan (if any) to finish, then refresh its cache:
|
||
arr_rescan_monitor.sh lidarr
|
||
|
||
# Override the default 2-hour wait ceiling (seconds):
|
||
arr_rescan_monitor.sh lidarr 3600
|
||
|
||
# Check whether a rescan is active and the cache's current age, no waiting:
|
||
arr_rescan_monitor.sh lidarr --status
|
||
|
||
# Show what would be waited on without waiting or writing the cache:
|
||
arr_rescan_monitor.sh lidarr --dry-run
|
||
```
|
||
|
||
### Notes
|
||
|
||
- Safe to run against sonarr/radarr the same way — arr_type is the only required arg
|
||
- If the wait times out (default 7200s), the cache is left untouched — re-run once the scan
|
||
actually finishes, or let it pick up naturally next time something calls
|
||
`arr_get_tracked_data()` and finds the cache stale with no active rescan
|
||
- Does not conflict with `arr_full_rescan.sh` — that script already refreshes its own cache
|
||
on completion for scans it triggers itself; this covers everything it didn't start
|
||
|
||
---
|
||
|
||
## docker_prune_images.sh
|
||
|
||
Removes orphaned Docker images that accumulate after container updates. Two modes:
|
||
|
||
**Default (dangling only)** — removes untagged images (no name, no container reference).
|
||
Safe — running containers are never affected. Use routinely after update cycles.
|
||
|
||
**`--all` (full orphan cleanup)** — first removes stopped/exited containers, then removes
|
||
all images not used by any running container. Use when you've removed apps and want to
|
||
recover the disk space. CAUTION: also removes intentionally stopped containers.
|
||
|
||
### Usage
|
||
|
||
```bash
|
||
docker_prune_images.sh # remove dangling (untagged) images only
|
||
docker_prune_images.sh --all # remove stopped containers, then all unused images
|
||
docker_prune_images.sh --dry-run # show what would be removed without making changes
|
||
docker_prune_images.sh --status # show dangling images and stopped containers
|
||
```
|
||
|
||
---
|
||
|
||
## Adding a New Tool
|
||
|
||
Write the tool when you solve a problem manually with bash commands. You'll face it again.
|
||
The cost of writing the tool is 30 minutes. The cost of reconstructing the commands at 2am
|
||
is much higher.
|
||
|
||
### Checklist
|
||
|
||
```
|
||
✓ Header explains the specific situation that requires this tool
|
||
✓ Root check — most tools need root
|
||
✓ --dry-run support — always
|
||
✓ --status support — show current state before acting
|
||
✓ Confirmation for destructive operations (interactive YES or --force flag)
|
||
✓ Notify on completion — success and failure
|
||
✓ Leave system in clean state on any exit — trap for cleanup
|
||
✓ Add to README-Tools.md scripts table and HOW THE SCRIPTS RELATE diagram
|
||
```
|
||
|
||
### Minimal Skeleton
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# ==============================================================================================
|
||
# ============================= Your Tool Name ================================================
|
||
# ==============================================================================================
|
||
#
|
||
# PURPOSE
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# One sentence: what situation this solves and when to use it.
|
||
#
|
||
# ==============================================================================================
|
||
# OPERATIONAL SAFEGUARDS
|
||
# ==============================================================================================
|
||
#
|
||
# Root Required
|
||
# chown / docker / etc. require root.
|
||
#
|
||
# Confirmation Required
|
||
# Interactive mode prompts for YES. Use --force to bypass in scripts.
|
||
#
|
||
# ==============================================================================================
|
||
# RUNTIME MODES
|
||
# ==============================================================================================
|
||
#
|
||
# your_tool.sh
|
||
# Normal run.
|
||
#
|
||
# your_tool.sh --dry-run
|
||
# Preview without making changes.
|
||
#
|
||
# your_tool.sh --status
|
||
# Show current state and exit.
|
||
#
|
||
# ==============================================================================================
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
source "$SCRIPT_DIR/../load_config.sh"
|
||
parse_args "$@"
|
||
|
||
if [[ "$EUID" -ne 0 ]]; then error "Must be run as root"; exit 1; fi
|
||
|
||
platform_require_cmd \
|
||
"/usr/local/emhttp/plugins/dynamix/scripts/notify" \
|
||
"" "" "unRAID notify script" || warn "notify not found — notifications disabled"
|
||
|
||
acquire_lock
|
||
detect_hosts
|
||
|
||
if [[ "$SHOW_STATUS" == true ]]; then
|
||
log "Current state: ..."
|
||
exit 0
|
||
fi
|
||
|
||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
||
|
||
if [[ "$FORCE" != true ]]; then
|
||
read -r -p "Type YES to proceed: " CONFIRM
|
||
[[ "$CONFIRM" != "YES" ]] && { warn "Aborted."; exit 0; }
|
||
fi
|
||
|
||
# Do the work
|
||
# ...
|
||
|
||
notify "Tool completed on $(hostname) ($MY_ID)" "Tool Name" "normal"
|
||
```
|