Maintenance and monitoring scripts for a Plex media server stack running on a Raspberry Pi (or any Linux box). Handles health checks, backups, library reporting, media analysis, and scheduled maintenance.
| Service | What it does | Schedule | Links |
|---|---|---|---|
| Plex | Media streaming server | Always running | Support |
| Kometa | Metadata, collections, and overlay management for Plex | Daily at 05:00 (internal scheduler) | Wiki |
| UMTK | Upcoming movies/TV shows + TV show status overlays for Kometa | Daily at 02:00 (Docker internal cron) | Docs |
| PlexTraktSync | Syncs Plex watch history and ratings with Trakt | Always running (systemd) | Docs |
| ImageMaid | Plex metadata image cleanup and DB optimization | Weekly Sundays at 07:00 (Docker internal) | GitHub |
| Radarr | Movie management and downloads | Always running (systemd) | |
| Sonarr | TV show management and downloads | Always running (systemd) |
The scripts monitor, maintain, and report on this stack. They don't replace any of these tools — they wrap around them to keep everything healthy and give you visibility into your library.
# 1. Clone the repo
git clone git@github.com:itsFelixH/media-server-scripts.git ~/kometa/scripts
cd ~/kometa/scripts
# 2. Create your config from the template
cp config.yml.template config.yml
# 3. Fill in your values (see Configuration section below)
nano config.yml
# 4. Test a script
bash healthcheck.sh- OS: Linux (tested on Raspberry Pi OS / Debian)
- Shell: Bash
- Core tools:
jq,curl - Media analysis:
ffprobe(from ffmpeg) - Metadata audit:
python3,python3-yaml - Docker: For container management (Kometa, UMTK, ImageMaid, etc.)
- systemd: For service monitoring (Plex, Radarr, Sonarr)
| Script | Purpose | Schedule |
|---|---|---|
healthcheck.sh |
Monitor services, disk, memory, temperature, APIs | Every 30 min |
piboard-data.sh |
PiBoard data (system stats, services, last runs, network) | Every 1 min |
maintenance.sh |
System updates, Docker updates, log rotation, diagnostics | Mondays 03:00 |
backup.sh |
Archive all configs to media drive | Sundays 01:00 |
archive-reports.sh |
Copy changed reports to archive with date stamps | Daily 05:30 |
library-catalog.sh |
Snapshot library contents with diff tracking | Sundays 01:30 |
metadata-audit.sh |
Validate metadata files against library | Sundays 02:00 |
encode-queue.sh |
Find re-encoding candidates | 1st of month |
storage-report.sh |
Disk usage breakdown by folder/codec/resolution (both libraries) | 28th of month |
plex-vs-arrs.sh |
Compare Plex library against Radarr/Sonarr | Sundays 02:30 |
media-analyzer.sh |
Filter/analyze video files by codec, resolution, size | Manual |
runkometa.sh |
Interactive Kometa runner with library/mode selection | Manual |
All scripts support -h/--help and --no-discord.
Scripts with terminal output support -q/--quiet for cron use.
All scripts load settings from config.yml via the shared config.sh loader. The config file is gitignored (contains secrets).
Copy config.yml.template to config.yml and fill in your values. The template is fully commented with what each key does.
The config is organized into four sections:
- Credentials —
plex(url, token),discord(alerts, notifications webhooks),api_keys(radarr, sonarr) - Paths —
media(movies, tv),tools(kometa, umtk, imagemaid, compose_files list),output(logs, reports),backup(configs, reports) - Services —
server(hostname),services(plex service name, arr list, docker_containers list) - Tuning —
retention_days(single value applied to all logs/backups),notifications(on_success, on_failure)
Thresholds (disk %, memory %, temperature, stale task minutes) are hardcoded in config.sh with sensible defaults — no config needed.
Using a single script? You only need to fill in the keys that script uses (plus
server.hostname). Leave everything else empty or remove it —config.shwon't fail on missing optional keys, it just skips those checks.
healthcheck.sh — services, containers, disk, memory, Plex API
Runs silently. Only alerts Discord when something is wrong. Auto-restarts failed Docker containers.
plex.*, api_keys.*, services.*, tools.kometa, tools.umtk
- systemd services (Plex, Radarr, Sonarr, Bazarr)
- Docker containers running + healthy (Kometa, UMTK, ImageMaid)
- Root disk and media drive usage
- RAM and swap usage
- CPU temperature
- Plex API responding + token valid
- Radarr/Sonarr API responding (skipped if keys empty)
- Internet + TMDb reachability
- UMTK, Kometa, PlexTraktSync last run times
- All healthy → silent exit, one-line heartbeat in log
- Issues found → consolidated Discord alert to
#server-alerts - Same issues as last run → no repeat alert (deduplication)
- Issues resolved → recovery notification with strikethrough list
maintenance.sh — system updates, Docker, log rotation, diagnostics
Interactive menu with 11 maintenance tasks. Also runs unattended via --scheduled.
services.*, tools.*, retention_days
1: System Maintenance (apt update/upgrade/autoremove)
2: Update Media Tools (PlexTraktSync self-update)
3: Update Docker Containers (pull latest images, restart)
4: Restart Services (Plex, arr services, Docker containers)
5: Disk Maintenance (clean old logs, show usage)
6: Health Check (services, disk, memory, load, connectivity)
7: Temperature Check (thermal zones)
8: Network Check (TMDb, Trakt, DNS)
9: Process Monitor (zombies, top CPU)
10: Config Validation (YAML syntax for all configs)
11: Token Consistency Check (compare Plex token across configs)
bash maintenance.sh --scheduledRuns tasks 1, 2, 3, 5, 10, 11 unattended. Sends summary to Discord.
backup.sh — config archival with retention
Creates a zip archive of all critical configs and scripts. Output filename: <hostname>-backup-YYYYMMDD.zip. Manages retention automatically.
backup.configs, tools.*, retention_days, server.hostname
- All
*.ymlfrom the Kometa config directory - Kometa metadata directory (if it contains yml files)
- All
*.ymlfrom the UMTK config directory - ImageMaid config (
.env) - Docker compose files (from
tools.compose_fileslist) - Scripts config (
config.yml) - Crontab
archive-reports.sh — daily report archiver
Copies new or changed reports (JSON and markdown) to the archive location with date-stamped filenames. Only archives when content has actually changed.
output.reports, backup.reports
- Reads reports from
output.reportsdirectory - Archives to
backup.reportsin per-report subdirectories - Filenames:
<report-name>-YYYY-MM-DD.json - Extracts report date from JSON
generatedfield via jq - Skips unchanged reports (diff comparison against latest archived copy)
- Checks that the archive mount is available before writing
library-catalog.sh — library snapshot with diff tracking
Generates a structured JSON catalog of all movies and TV shows. Compares against the previous run to highlight additions and removals.
media.*, plex.*
- Report:
reports/library-catalog.json(overwritten each run) - Previous:
reports/library-catalog.prev.json(for diffing) - Discord: library totals + changes since last run
- Structured movie/show objects with year and size
- Decade distribution
- Season detail per show
- Added date tracking
- Recently added section
metadata-audit.sh — validate metadata against library
Checks Kometa metadata YAML files against actual library content. Finds orphaned entries, missing metadata, duplicates, and season/episode gaps.
tools.kometa (metadata dir derived), media.*
- Report:
reports/metadata-audit.json(overwritten each run)
- Orphaned metadata (entries for content not in library)
- Missing metadata (library items without custom entries)
- Duplicate entries across files
- Season/episode coverage gaps
- Structured entries with TMDb IDs and links
- Severity levels and action suggestions
- Coverage percentages
- Per-source breakdown
- Last-clean date tracking
python3, python3-yaml, jq, curl
encode-queue.sh — find re-encoding candidates
Scans for non-HEVC/non-AV1 files and generates a prioritized re-encode list sorted by size. Estimates space savings. Does NOT perform any encoding.
media.*
- Report:
reports/encode-queue.json(overwritten each run)
- HDR detection
- Batch grouping
- Savings by codec breakdown
- Exclude list support (
encode-exclude.txt)
./encode-queue.sh # Both libraries, files >1GB
./encode-queue.sh "/mnt/Media/Movies" # Movies only
./encode-queue.sh --min-size=2 --limit=20 # Only >2GB, top 20ffprobe (ffmpeg), jq, curl
storage-report.sh — disk usage by folder, codec, resolution
Scans a media directory and generates a detailed storage report. Auto-detects TV (show/season) vs Movies (flat) structure. Compares against previous run. When no directory is specified, scans both TV Shows and Movies and produces a combined report.
media.tv (default scan directory)
- Report:
reports/storage-report.json(overwritten each run)
- Per-library percentage breakdown
- Resolution buckets per item
- Top 10 by size
- Human-readable sizes
./storage-report.sh # Both libraries (combined report)
./storage-report.sh "/mnt/Media/Movies" # Movies only
./storage-report.sh "/mnt/Media/TV Shows" # TV Shows onlyffprobe (ffmpeg), jq, curl
plex-vs-arrs.sh — compare Plex library against Radarr/Sonarr
Compares Plex library content against Radarr (movies) and Sonarr (TV shows) via their APIs. Finds items that exist in one system but not the other, detects duplicates, and performs fuzzy title matching for items with mismatched IDs.
plex.*, api_keys.radarr, api_keys.sonarr
- Report:
reports/plex-vs-arrs.json(overwritten each run)
- Movies in Plex but not in Radarr (and vice versa)
- TV Shows in Plex but not in Sonarr (and vice versa)
- Fuzzy title matching for different IDs pointing to the same content
- Duplicate entries (same TMDb/TVDb ID appearing multiple times in Plex)
- Items without usable IDs
jq, curl
media-analyzer.sh — filter/analyze video files
Scans video files, probes each for codec and resolution, filters by mode. Supports scanning multiple directories.
media.*
| Mode | Description |
|---|---|
all |
Full analysis (default) |
non-hevc |
Files NOT encoded in HEVC/x265 |
hevc |
HEVC/x265 files only |
av1 |
AV1 files only |
h264 |
H.264/x264 files only |
non-hd |
Below 720p |
4k |
2160p+ files |
large |
Files exceeding size threshold |
./media-analyzer.sh non-hevc "/mnt/Media/Movies"
./media-analyzer.sh av1
./media-analyzer.sh all "/mnt/Media/TV Shows" "/mnt/Media/Movies"
./media-analyzer.sh --quiet large "/mnt/Media/TV Shows"ffprobe (ffmpeg), jq, curl
runkometa.sh — interactive Kometa runner
Menu-driven interface for running Kometa inside its Docker container with different options.
- Full run (all libraries)
- Ignore schedules
- Per-library (Movies / TV Shows)
- Per-mode (metadata, collections, overlays)
- Delete all collections
# Health check (every 30 minutes)
*/30 * * * * bash ~/kometa/scripts/healthcheck.sh
# Weekly config backup (Sundays 01:00)
0 1 * * 0 bash ~/kometa/scripts/backup.sh
# Library catalog (Sundays 01:30)
30 1 * * 0 bash ~/kometa/scripts/library-catalog.sh --quiet
# Metadata audit (Sundays 02:00)
0 2 * * 0 bash ~/kometa/scripts/metadata-audit.sh --quiet
# Plex vs ARRs comparison (Sundays 02:30)
30 2 * * 0 bash ~/kometa/scripts/plex-vs-arrs.sh --quiet
# Scheduled maintenance (Mondays 03:00)
0 3 * * 1 bash ~/kometa/scripts/maintenance.sh --scheduled
# Encode queue (1st of month 04:00)
0 4 1 * * bash ~/kometa/scripts/encode-queue.sh --quiet
# Storage report (28th of month 04:30)
30 4 28 * * bash ~/kometa/scripts/storage-report.sh --quiet
# Archive reports (daily 05:30)
30 5 * * * bash ~/kometa/scripts/archive-reports.sh --quietThe Docker-based services have their own internal schedules:
| Service | Schedule | Managed by |
|---|---|---|
| Kometa | Daily at 05:00 | KOMETA_TIMES env var in compose |
| UMTK | Daily at 02:00 | Internal cron in container |
| ImageMaid | Weekly Sundays at 07:00 | SCHEDULE in .env |
All scripts share a single discord_notify function defined in config.sh. No per-script notification code needed.
| Level | Webhook | Use case |
|---|---|---|
success |
discord.notifications |
Successful runs, completions |
warning |
discord.alerts |
Non-critical issues |
error |
discord.alerts |
Failures, things that need attention |
The footer on every notification automatically includes the hostname, script name, and run duration.
Control which messages are sent via notifications.on_success and notifications.on_failure in config. Use --no-discord per-run to suppress entirely.
~/kometa/scripts/
├── config.yml # Your config (gitignored, contains secrets)
├── config.yml.template # Template with full documentation
├── config.sh # Config loader + discord_notify function
├── healthcheck.sh
├── maintenance.sh
├── backup.sh
├── archive-reports.sh
├── runkometa.sh
├── library-catalog.sh
├── metadata-audit.sh
├── media-analyzer.sh
├── storage-report.sh
├── encode-queue.sh
├── logs/ # Per-script log subdirectories (gitignored)
│ ├── archive-reports/
│ ├── backup/
│ ├── healthcheck/
│ ├── maintenance/
│ └── ...
├── encode-exclude.txt # Encode queue exclusion patterns
└── reports/ # Generated JSON reports (gitignored)
- Kometa — Metadata, collections, and overlays for Plex
- UMTK — Upcoming movies/TV shows + status overlays
- PlexTraktSync — Syncs Plex watch history and ratings with Trakt
- ImageMaid — Plex image cleanup and DB optimization