#!/bin/bash # update-icons.sh — Replace Unraid Docker/VM icons with selfh.st CDN icons # Part of the unraid-glass theme project set -euo pipefail # --- Configuration --- CDN_BASE="https://cdn.jsdelivr.net/gh/selfhst/icons@main/png" ICON_RAM="/usr/local/emhttp/state/plugins/dynamix.docker.manager/images" ICON_PERSIST="/var/lib/docker/unraid/images" VM_ICON_DIR="/usr/local/emhttp/plugins/dynamix.vm.manager/templates/images" CONFIG_DIR="/boot/config/plugins/custom.css/assets" VM_CONFIG="${CONFIG_DIR}/vm-icons.conf" TMP_DIR="/tmp/glass-icons" LABEL_NAME="glass.icon" FORCE=false QUIET=false DRY_RUN=false DOCKER_ONLY=false VM_ONLY=false updated=0 skipped=0 failed=0 # --- Helpers --- log() { $QUIET || echo "$@"; } warn() { echo "WARN: $@" >&2; } usage() { cat <<'EOF' Usage: update-icons.sh [OPTIONS] Replace Unraid Docker/VM icons with selfh.st CDN icons. Options: --force Re-download even if icon file already matches --quiet Suppress output (for cron) --dry-run Show what would be done without doing it --docker-only Skip VM icon processing --vm-only Skip Docker icon processing --help Show this help EOF exit 0 } # --- Argument parsing --- while [[ $# -gt 0 ]]; do case "$1" in --force) FORCE=true ;; --quiet) QUIET=true ;; --dry-run) DRY_RUN=true ;; --docker-only) DOCKER_ONLY=true ;; --vm-only) VM_ONLY=true ;; --help) usage ;; *) warn "Unknown option: $1"; usage ;; esac shift done # --- Auto-detection: try to match container name to selfh.st icon --- cdn_exists() { curl -fsSL --head --max-time 5 "${CDN_BASE}/${1}.png" >/dev/null 2>&1 } auto_detect_icon() { local container="$1" local name # Known mappings for containers that won't auto-match case "$(echo "$container" | tr '[:upper:]' '[:lower:]')" in autokuma) echo "uptime-kuma"; return 0 ;; uptimekuma) echo "uptime-kuma"; return 0 ;; uptime-kuma-api) echo "uptime-kuma"; return 0 ;; homeassistant_inabox) echo "home-assistant"; return 0 ;; homeassistant*) echo "home-assistant"; return 0 ;; dockersocket) echo "docker"; return 0 ;; pgadmin*) echo "pgadmin"; return 0 ;; postgresql*) echo "postgresql"; return 0 ;; woodpecker-agent) echo "woodpecker-ci"; return 0 ;; woodpecker-server) echo "woodpecker-ci"; return 0 ;; rustdesk-hbbr) echo "rustdesk"; return 0 ;; rustdesk-hbbs) echo "rustdesk"; return 0 ;; rustfs) echo "rustdesk"; return 0 ;; netbox-redis-cache) echo "redis"; return 0 ;; netbox-worker) echo "netbox"; return 0 ;; netdisco-backend) echo "networking-toolbox"; return 0 ;; netdisco-web) echo "networking-toolbox"; return 0 ;; libation) echo "audible"; return 0 ;; diode-*) echo "netbox"; return 0 ;; authentik-worker) echo "authentik"; return 0 ;; adguardhome-sync) echo "adguard-home"; return 0 ;; adguardhome) echo "adguard-home"; return 0 ;; actual-budget) echo "actual-budget"; return 0 ;; speedtest-tracker) echo "speedtest-tracker"; return 0 ;; timemachine) echo "apple"; return 0 ;; open-webui) echo "open-webui"; return 0 ;; urbackup) echo "networking-toolbox"; return 0 ;; netalertx) echo "netalertx"; return 0 ;; seekandwatch) echo "databasement"; return 0 ;; unmarr) echo "homarr"; return 0 ;; dockhand) echo "docker"; return 0 ;; esac # Strategy 1: lowercase name directly name=$(echo "$container" | tr '[:upper:]' '[:lower:]') if cdn_exists "$name"; then echo "$name"; return 0; fi # Strategy 2: underscores to hyphens name=$(echo "$container" | tr '[:upper:]' '[:lower:]' | tr '_' '-') if cdn_exists "$name"; then echo "$name"; return 0; fi # Strategy 3: strip common suffixes name=$(echo "$container" | tr '[:upper:]' '[:lower:]' | sed -E 's/-(worker|agent|server|web|backend|api|cache|migrate|bootstrap|sync|ingress|ingester|reconciler)$//') if cdn_exists "$name"; then echo "$name"; return 0; fi return 1 } # Resolve label value to a full URL resolve_url() { local value="$1" if [[ "$value" == http* ]]; then echo "$value" else echo "${CDN_BASE}/${value}.png" fi } # Download icon to temp, validate, and copy to target paths download_and_replace() { local name="$1" local url="$2" local target_ram="$3" local target_persist="$4" local tmp_file="${TMP_DIR}/${name}.png" if $DRY_RUN; then log " [DRY-RUN] Would download: $url" log " [DRY-RUN] Would replace: $target_ram" log " [DRY-RUN] Would replace: $target_persist" ((updated++)) || true return 0 fi # Download if ! curl -fsSL --max-time 15 -o "$tmp_file" "$url" 2>/dev/null; then warn "Download failed for $name: $url" ((failed++)) || true return 1 fi # Validate non-empty if [ ! -s "$tmp_file" ]; then warn "Empty download for $name: $url" rm -f "$tmp_file" ((failed++)) || true return 1 fi # Skip if identical (unless --force) if ! $FORCE && [ -f "$target_ram" ] && cmp -s "$tmp_file" "$target_ram"; then log " [SKIP] $name — icon unchanged" rm -f "$tmp_file" ((skipped++)) || true return 0 fi # Replace in both paths cp "$tmp_file" "$target_ram" 2>/dev/null || true if [ -n "$target_persist" ] && [ -d "$(dirname "$target_persist")" ]; then cp "$tmp_file" "$target_persist" 2>/dev/null || true fi rm -f "$tmp_file" log " [OK] $name — icon updated" ((updated++)) || true return 0 } # --- Main --- mkdir -p "$TMP_DIR" trap 'rm -rf "$TMP_DIR"' EXIT # === Docker containers === if ! $VM_ONLY; then log "=== Docker Containers ===" if ! docker info >/dev/null 2>&1; then warn "Docker is not running, skipping containers" else while IFS= read -r container; do [ -z "$container" ] && continue log "Processing: $container" # Read glass.icon label label=$(docker inspect --format "{{index .Config.Labels \"${LABEL_NAME}\"}}" "$container" 2>/dev/null || echo "") label="${label#}" # docker inspect returns "" if missing if [ -n "$label" ]; then icon_url=$(resolve_url "$label") log " Label: $label → $icon_url" else # Auto-detect if icon_name=$(auto_detect_icon "$container"); then icon_url=$(resolve_url "$icon_name") log " Auto-detect: $icon_name → $icon_url" else log " [SKIP] No label, no auto-match" ((skipped++)) || true continue fi fi download_and_replace \ "$container" \ "$icon_url" \ "${ICON_RAM}/${container}-icon.png" \ "${ICON_PERSIST}/${container}-icon.png" || true done < <(docker ps -a --format '{{.Names}}') fi fi # === Virtual Machines === if ! $DOCKER_ONLY; then log "" log "=== Virtual Machines ===" if [ ! -f "$VM_CONFIG" ]; then log "No VM config found at $VM_CONFIG, skipping VMs" elif ! command -v virsh >/dev/null 2>&1; then warn "virsh not found, skipping VMs" else while IFS='=' read -r vm_name icon_value; do # Skip comments and empty lines [[ "$vm_name" =~ ^[[:space:]]*# ]] && continue [ -z "$vm_name" ] && continue vm_name=$(echo "$vm_name" | xargs) # trim whitespace icon_value=$(echo "$icon_value" | xargs) [ -z "$icon_value" ] && continue log "Processing VM: $vm_name" # Get current icon filename from VM XML icon_file=$(virsh dumpxml "$vm_name" 2>/dev/null | grep -oP 'icon="\K[^"]+' || echo "") if [ -z "$icon_file" ]; then warn "Could not read icon for VM '$vm_name'" ((failed++)) || true continue fi icon_url=$(resolve_url "$icon_value") log " Config: $icon_value → $icon_url" download_and_replace \ "vm-${vm_name// /-}" \ "$icon_url" \ "${VM_ICON_DIR}/${icon_file}" \ "" || true done < "$VM_CONFIG" fi fi # === Summary === log "" log "=== Done ===" log "Updated: $updated | Skipped: $skipped | Failed: $failed"