diff --git a/gaming/stackmon/stl-config.md b/gaming/stackmon/stl-config.md index 0c0513e..8c9d44e 100644 --- a/gaming/stackmon/stl-config.md +++ b/gaming/stackmon/stl-config.md @@ -6,7 +6,7 @@ | **Developer** | Riftpoint Entertainment | | **Engine** | Unknown (likely Unity) | | **Graphics API** | DX10/DX11 | -| **Status** | Playtest (Q2 2026 release) | +| **Status** | Playtest ended — archived for offline play | | **Genre** | Creature-collector / card-stacking | | **STL Profile** | `~/.config/steamtinkerlaunch/gamecfgs/id/3729550.conf` | | **Setup Date** | 2026-03-02 | @@ -26,6 +26,15 @@ - No customvars file needed - No ProtonDB reports available yet (playtest phase) +## Offline Archive + +Playtest ended 2026-03-08. Game files archived (no Steam DRM): + +- **Archive**: `/mnt/truenas/media/Games/Stackmon-Playtest-Archive.tar.zst` (1.1 GB) +- **Contains**: Game files + Wine/Proton prefix with save data (`SaveFile0.es3`) +- **To play**: Extract and run with `wine Stackmon.exe` +- **Details**: See `/mnt/truenas/media/Games/Stackmon-Playtest-README.md` + ## Proton Compatibility - Using Proton 9.0-4 (default) diff --git a/media-servers/troubleshooting.md b/media-servers/troubleshooting.md index 32d4970..f080d85 100644 --- a/media-servers/troubleshooting.md +++ b/media-servers/troubleshooting.md @@ -369,6 +369,35 @@ docker logs jellyfin | grep -i memory iostat -x 2 5 ``` +#### Roku/Apple TV Playback Timeout (TrueHD/DTS-HD MA Audio) +**Symptoms**: +- Playback hangs at "Loading" for 20-30 seconds then fails on Roku +- Jellyfin logs show forced transcoding with subtitle extraction delay +- Works fine on web browser or mobile clients + +**Root Cause**: File has incompatible default audio (TrueHD, DTS-HD MA, Opus) AND a default SRT subtitle. Jellyfin must transcode audio AND burn-in subtitles over HLS. The 27-second subtitle extraction delay causes Roku client timeout. + +**Incompatible Audio Codecs** (Roku/Apple TV): +| Codec | Status | +|-------|--------| +| AC3 (Dolby Digital) | Native playback | +| AAC | Native playback | +| EAC3 (Dolby Digital+) | Native playback | +| TrueHD | Requires transcode | +| DTS / DTS-HD MA | Requires transcode | +| Opus | Requires transcode | + +**Immediate Fix** (per-file with mkvpropedit): +```bash +# Clear subtitle default, set compatible audio as default +mkvprobedit "file.mkv" \ + --edit track:s1 --set flag-default=0 \ + --edit track:a1 --set flag-default=0 \ + --edit track:a3 --set flag-default=1 +``` + +**Systemic Fix**: Tdarr flow plugins `ensAC3str` (adds AC3 stereo fallback) and `clrSubDef` (clears non-forced subtitle defaults) — see `tdarr/CONTEXT.md` + #### Audio/Video Sync Issues **Symptoms**: - Audio and video out of sync during playback diff --git a/server-configs/caddy-migration/.env.example b/server-configs/caddy-migration/.env.example new file mode 100644 index 0000000..b17a1e6 --- /dev/null +++ b/server-configs/caddy-migration/.env.example @@ -0,0 +1,4 @@ +# Cloudflare API token with Zone:DNS:Edit permission for manticorum.com +# Create at: https://dash.cloudflare.com/profile/api-tokens +# Required permissions: Zone - DNS - Edit (scoped to manticorum.com zone) +CF_API_TOKEN=your_cloudflare_api_token_here diff --git a/server-configs/caddy-migration/Caddyfile b/server-configs/caddy-migration/Caddyfile new file mode 100644 index 0000000..0c26304 --- /dev/null +++ b/server-configs/caddy-migration/Caddyfile @@ -0,0 +1,221 @@ +# ============================================================ +# Caddy Reverse Proxy - manticorum.com homelab +# Replaces: Nginx Proxy Manager on 10.10.0.16 +# ============================================================ + +# Global options +{ + email admin@manticorum.com + + # DNS-01 challenge via Cloudflare for automatic wildcard cert + acme_dns cloudflare {env.CF_API_TOKEN} + + # Trust Cloudflare proxy IPs so {client_ip} reflects real visitor + servers { + trusted_proxies static \ + 173.245.48.0/20 \ + 103.21.244.0/22 \ + 103.22.200.0/22 \ + 103.31.4.0/22 \ + 141.101.64.0/18 \ + 108.162.192.0/18 \ + 190.93.240.0/20 \ + 188.114.96.0/20 \ + 197.234.240.0/22 \ + 198.41.128.0/17 \ + 162.158.0.0/15 \ + 104.16.0.0/13 \ + 104.24.0.0/14 \ + 172.64.0.0/13 \ + 131.0.72.0/22 \ + 10.0.0.0/8 \ + 172.16.0.0/12 \ + 192.168.0.0/16 + client_ip_headers CF-Connecting-IP + } +} + +# ============================================================ +# Reusable snippets +# ============================================================ + +# Internal-only access list +# Allows: local subnets + home public IP +# Equivalent to NPM "Internal Only" access list (id=1) +(internal_only) { + @blocked not remote_ip 10.0.0.0/23 10.10.0.0/24 73.36.102.55/32 + respond @blocked "Access denied" 403 +} + +# Standard proxy headers sent to backends +(proxy_headers) { + header_up X-Real-IP {client_ip} + header_up X-Forwarded-For {client_ip} + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-Host {host} +} + +# ============================================================ +# Public services (no IP restriction) +# ============================================================ + +# SBA Dev Website +sbadev.manticorum.com { + reverse_proxy 10.10.0.33:801 { + import proxy_headers + } + header Access-Control-Allow-Origin * +} + +# SBA News (Ghost blog) +sbanews.manticorum.com { + reverse_proxy 10.10.0.88:2368 { + import proxy_headers + } +} + +# Paper Dynasty Dev +pddev.manticorum.com { + reverse_proxy 10.10.0.42:813 { + import proxy_headers + } +} + +# Foundry VTT +foundry.manticorum.com { + reverse_proxy 10.10.0.223:30000 { + import proxy_headers + } +} + +# Paper Dynasty Staging +pds.manticorum.com { + reverse_proxy 10.10.0.42:810 { + import proxy_headers + } +} + +# n8n Automation (extended timeouts for long workflows) +n8n.manticorum.com { + reverse_proxy 10.10.0.210:5678 { + import proxy_headers + transport http { + read_timeout 300s + write_timeout 300s + dial_timeout 300s + } + } +} + +# Gameplay Demo Frontend +gameplay-demo.manticorum.com { + reverse_proxy 10.0.0.206:3000 { + import proxy_headers + } +} + +# Gameplay Demo API +gameplay-api-demo.manticorum.com { + reverse_proxy 10.0.0.206:8000 { + import proxy_headers + } +} + +# Memos +memos.manticorum.com { + reverse_proxy 10.10.0.222:5230 { + import proxy_headers + } +} + +# NoteDiscovery +notes.manticorum.com { + reverse_proxy 10.10.0.222:8000 { + import proxy_headers + } +} + +# Vagabond (Foundry VTT - alternate world) +vagabond.manticorum.com { + reverse_proxy 10.10.0.223:30000 { + import proxy_headers + } +} + +# Pocket +pocket.manticorum.com { + reverse_proxy 10.0.0.233:80 { + import proxy_headers + } +} + +# Gitea +git.manticorum.com { + reverse_proxy 10.10.0.225:3000 { + import proxy_headers + } +} + +# OmniTools +omnitools.manticorum.com { + reverse_proxy 10.10.0.210:8080 { + import proxy_headers + } +} + +# Termix +termix.manticorum.com { + reverse_proxy 10.10.0.210:8180 { + import proxy_headers + } +} + +# Uptime Kuma +status.manticorum.com { + reverse_proxy 10.10.0.227:3001 { + import proxy_headers + } +} + +# Jellyfin Media Server +jellyfin.manticorum.com { + reverse_proxy 10.10.0.226:8096 { + import proxy_headers + } +} + +# ============================================================ +# Internal-only services (restricted to local network) +# ============================================================ + +# Radarr +radarr.manticorum.com { + import internal_only + reverse_proxy 10.10.0.221:7878 { + import proxy_headers + } +} + +# Sonarr +sonarr.manticorum.com { + import internal_only + reverse_proxy 10.10.0.221:8989 { + import proxy_headers + } +} + +# Jellyseerr +jellyseer.manticorum.com { + import internal_only + reverse_proxy 10.10.0.221:5055 { + import proxy_headers + } +} + +# OpenClaw AI Assistant +openclaw.manticorum.com { + import internal_only + reverse_proxy 10.10.0.224:18789 { + import proxy_headers + } +} diff --git a/server-configs/caddy-migration/Dockerfile b/server-configs/caddy-migration/Dockerfile new file mode 100644 index 0000000..1bd61a0 --- /dev/null +++ b/server-configs/caddy-migration/Dockerfile @@ -0,0 +1,8 @@ +FROM caddy:builder AS builder + +RUN xcaddy build \ + --with github.com/caddy-dns/cloudflare + +FROM caddy:latest + +COPY --from=builder /usr/bin/caddy /usr/bin/caddy diff --git a/server-configs/caddy-migration/MIGRATION.md b/server-configs/caddy-migration/MIGRATION.md new file mode 100644 index 0000000..a56b48e --- /dev/null +++ b/server-configs/caddy-migration/MIGRATION.md @@ -0,0 +1,257 @@ +# NPM to Caddy Migration Plan + +Step-by-step guide to migrate from Nginx Proxy Manager to Caddy on `10.10.0.16`. + +## Prerequisites + +- [ ] Cloudflare API token with `Zone:DNS:Edit` for manticorum.com +- [ ] SSH access to `pihole` (10.10.0.16) and `ubuntu-manticore` (10.10.0.226) +- [ ] Docker and docker compose installed on 10.10.0.16 +- [ ] Familiarity with current NPM proxy hosts (see README.md) + +## Phase 1: Prepare (no downtime, no changes to production) + +### 1.1 Create Cloudflare API Token + +1. Go to https://dash.cloudflare.com/profile/api-tokens +2. Create token with permissions: + - **Zone - DNS - Edit** (scoped to manticorum.com) +3. Save the token securely + +### 1.2 Deploy Caddy Config to Host + +```bash +# From workstation +scp -r server-configs/caddy-migration/ pihole:/home/cal/caddy/ +ssh pihole "cp /home/cal/caddy/.env.example /home/cal/caddy/.env" +``` + +Edit `.env` on the host: +```bash +ssh pihole "nano /home/cal/caddy/.env" +# Set CF_API_TOKEN= +``` + +### 1.3 Build and Test Caddy (on alternate ports) + +Temporarily modify `docker-compose.yml` to use non-conflicting ports: +```yaml +ports: + - "8080:80" + - "8443:443" + - "8443:443/udp" +``` + +```bash +ssh pihole "cd /home/cal/caddy && docker compose up -d --build" +``` + +Verify the container starts and the Cloudflare module is loaded: +```bash +ssh pihole "docker logs caddy 2>&1 | head -30" +ssh pihole "docker exec caddy caddy list-modules | grep cloudflare" +``` + +Verify config is valid: +```bash +ssh pihole "docker exec caddy caddy validate --config /etc/caddy/Caddyfile" +``` + +Test a proxy host directly (bypass DNS): +```bash +curl -k --resolve sbadev.manticorum.com:8443:10.10.0.16 https://sbadev.manticorum.com:8443/ +``` + +### 1.4 Verify Cert Issuance + +Check that Caddy successfully obtains a wildcard cert: +```bash +ssh pihole "docker logs caddy 2>&1 | grep -i 'certificate\|tls\|acme'" +``` + +You should see successful ACME DNS-01 challenge completion. + +### 1.5 Stop Test Caddy + +```bash +ssh pihole "cd /home/cal/caddy && docker compose down" +``` + +Revert `docker-compose.yml` ports back to 80/443. + +## Phase 2: Cutover (brief downtime) + +Expected downtime: **< 2 minutes** (stop NPM, start Caddy, sync DNS). + +### 2.1 Backup NPM + +```bash +ssh pihole "cd ~/nginx-proxy-manager && tar czf ~/npm-backup-$(date +%Y%m%d).tar.gz data/ letsencrypt/" +``` + +### 2.2 Take a Snapshot + +If 10.10.0.16 is a VM/LXC on Proxmox, take a snapshot first: +```bash +# From proxmox host (adjust VMID) +pct snapshot pre-caddy-migration +``` + +### 2.3 Stop NPM + +```bash +ssh pihole "cd ~/nginx-proxy-manager && docker compose down" +``` + +Ports 80, 443, and 81 are now free. + +### 2.4 Start Caddy + +```bash +ssh pihole "cd /home/cal/caddy && docker compose up -d" +``` + +### 2.5 Verify Services + +Quick smoke test of key services: +```bash +# Test from workstation (DNS should already point to 10.10.0.16 via Pi-hole) +curl -sI https://git.manticorum.com | head -5 +curl -sI https://n8n.manticorum.com | head -5 +curl -sI https://jellyfin.manticorum.com | head -5 +curl -sI https://foundry.manticorum.com | head -5 +curl -sI https://status.manticorum.com | head -5 + +# Test internal-only access +curl -sI https://radarr.manticorum.com | head -5 # should work from local +curl -sI https://sonarr.manticorum.com | head -5 +``` + +### 2.6 Update Pi-hole Sync + +Deploy the new sync script: +```bash +ssh pihole "cp /home/cal/caddy/scripts/caddy-pihole-sync.sh /home/cal/scripts/" +ssh pihole "chmod +x /home/cal/scripts/caddy-pihole-sync.sh" +``` + +Test dry run: +```bash +ssh pihole "/home/cal/scripts/caddy-pihole-sync.sh --dry-run" +``` + +Run sync: +```bash +ssh pihole "/home/cal/scripts/caddy-pihole-sync.sh" +``` + +Update cron to use the new script: +```bash +ssh pihole "crontab -l | sed 's|npm-pihole-sync.sh|caddy-pihole-sync.sh|g' | crontab -" +``` + +Also update the `CADDYFILE` path variable in the script if the deployment path differs from `/home/cal/caddy/Caddyfile`. + +## Phase 3: Validate (next 24-48 hours) + +### 3.1 Monitor Caddy Logs + +```bash +ssh pihole "docker logs caddy -f" +``` + +Look for: +- Successful TLS handshakes +- No upstream connection errors +- Cert renewal events (if timing aligns) + +### 3.2 Check Uptime Kuma + +Verify all monitored services at https://status.manticorum.com show UP. + +### 3.3 Test WebSocket Services + +These services use WebSockets and should be tested interactively: +- **Foundry VTT** (foundry.manticorum.com) - open a game session +- **n8n** (n8n.manticorum.com) - open workflow editor +- **Memos** (memos.manticorum.com) - create/edit a memo +- **Termix** (termix.manticorum.com) - open a terminal session + +### 3.4 Test External Access + +If any services are accessed via Cloudflare from outside: +1. From a phone on cellular (not on home WiFi) +2. Access public services and verify they load +3. Access internal-only services and verify 403 response + +### 3.5 Verify Access Restrictions + +```bash +# From a machine NOT on 10.0.0.0/23 or 10.10.0.0/24: +curl -sI https://radarr.manticorum.com # Should return 403 +curl -sI https://sonarr.manticorum.com # Should return 403 +``` + +## Phase 4: Cleanup + +### 4.1 Remove NPM (after validation period) + +```bash +# Keep backup, remove containers and images +ssh pihole "cd ~/nginx-proxy-manager && docker compose rm -f" +ssh pihole "docker image rm jc21/nginx-proxy-manager:latest" +``` + +### 4.2 Update Documentation + +- Update `server-configs/networking/nginx-proxy-manager-pihole.md` to reference Caddy +- Update any Uptime Kuma monitors that check port 81 (NPM admin) +- Update `CONTEXT.md` networking section + +### 4.3 Free Port 81 + +Port 81 (NPM admin UI) is no longer needed. Caddy's admin API runs on localhost:2019 inside the container by default (not exposed). + +## Rollback Plan + +If something goes wrong, rollback takes < 1 minute: + +```bash +# Stop Caddy +ssh pihole "cd /home/cal/caddy && docker compose down" + +# Restart NPM +ssh pihole "cd ~/nginx-proxy-manager && docker compose up -d" + +# Revert cron to old sync script +ssh pihole "crontab -l | sed 's|caddy-pihole-sync.sh|npm-pihole-sync.sh|g' | crontab -" +``` + +Or restore from Proxmox snapshot: +```bash +pct rollback pre-caddy-migration +``` + +## Adding New Services After Migration + +Edit the Caddyfile and reload - no web UI needed: + +```bash +ssh pihole "nano /home/cal/caddy/Caddyfile" + +# Add a new block: +# newservice.manticorum.com { +# reverse_proxy 10.10.0.xxx:port { +# import proxy_headers +# } +# } + +# Validate +ssh pihole "docker exec caddy caddy validate --config /etc/caddy/Caddyfile" + +# Apply (zero downtime) +ssh pihole "docker exec caddy caddy reload --config /etc/caddy/Caddyfile" + +# Sync DNS to Pi-holes +ssh pihole "/home/cal/scripts/caddy-pihole-sync.sh" +``` diff --git a/server-configs/caddy-migration/README.md b/server-configs/caddy-migration/README.md new file mode 100644 index 0000000..1d46d50 --- /dev/null +++ b/server-configs/caddy-migration/README.md @@ -0,0 +1,101 @@ +# Caddy Reverse Proxy - NPM Replacement + +Caddy configuration to replace Nginx Proxy Manager (NPM) on `10.10.0.16` for the manticorum.com homelab. + +## Why Caddy + +- **Single config file** instead of a web UI + SQLite database +- **Automatic HTTPS** with built-in Let's Encrypt and zero-downtime renewal +- **Wildcard cert via DNS-01** replaces 22 individual HTTP-01 certs +- **HTTP/3 (QUIC)** support out of the box +- **WebSocket proxying** works automatically without per-host toggles +- **Git-friendly** - the entire proxy config is a single version-controlled file + +## Architecture + +``` +Clients -> Pi-hole DNS (10.10.0.16) -> Caddy (10.10.0.16:443) -> Backend services +``` + +Caddy replaces NPM in the same position. Pi-hole sync script updated to parse the Caddyfile instead of NPM's SQLite database. + +## Files + +| File | Purpose | +|------|---------| +| `Caddyfile` | Main reverse proxy configuration (all 22 proxy hosts) | +| `docker-compose.yml` | Container deployment | +| `Dockerfile` | Custom Caddy build with Cloudflare DNS plugin | +| `.env.example` | Required environment variables | +| `scripts/caddy-pihole-sync.sh` | Pi-hole DNS sync (replaces npm-pihole-sync.sh) | + +## Proxied Services + +### Public (no IP restriction) + +| Domain | Backend | Notes | +|--------|---------|-------| +| sbadev.manticorum.com | 10.10.0.33:801 | CORS header added | +| sbanews.manticorum.com | 10.10.0.88:2368 | Ghost blog | +| pddev.manticorum.com | 10.10.0.42:813 | Paper Dynasty dev | +| foundry.manticorum.com | 10.10.0.223:30000 | Foundry VTT | +| pds.manticorum.com | 10.10.0.42:810 | PD staging | +| n8n.manticorum.com | 10.10.0.210:5678 | 300s timeouts | +| gameplay-demo.manticorum.com | 10.0.0.206:3000 | | +| gameplay-api-demo.manticorum.com | 10.0.0.206:8000 | | +| memos.manticorum.com | 10.10.0.222:5230 | | +| notes.manticorum.com | 10.10.0.222:8000 | NoteDiscovery | +| vagabond.manticorum.com | 10.10.0.223:30000 | Foundry VTT alt | +| pocket.manticorum.com | 10.0.0.233:80 | | +| git.manticorum.com | 10.10.0.225:3000 | Gitea | +| omnitools.manticorum.com | 10.10.0.210:8080 | | +| termix.manticorum.com | 10.10.0.210:8180 | | +| status.manticorum.com | 10.10.0.227:3001 | Uptime Kuma | +| jellyfin.manticorum.com | 10.10.0.226:8096 | | + +### Internal Only (10.0.0.0/23, 10.10.0.0/24, home IP) + +| Domain | Backend | +|--------|---------| +| radarr.manticorum.com | 10.10.0.221:7878 | +| sonarr.manticorum.com | 10.10.0.221:8989 | +| jellyseer.manticorum.com | 10.10.0.221:5055 | +| openclaw.manticorum.com | 10.10.0.224:18789 | + +## What Changed from NPM + +| Feature | NPM | Caddy | +|---------|-----|-------| +| SSL certs | 22 individual LE certs (HTTP-01) | 1 wildcard cert (DNS-01 via Cloudflare) | +| WebSocket | Per-host toggle | Automatic | +| HTTP/2 | Per-host toggle | Always on | +| HTTP/3 | Not supported | Built-in | +| HSTS | Per-host toggle | Automatic with HTTPS | +| Config format | SQLite DB + web UI | Caddyfile (text) | +| Admin panel | Port 81 web UI | SSH + text editor / Caddy API on :2019 | +| "Block exploits" | Built-in toggle | Rely on Cloudflare WAF | +| Real IP from CF | Manual nginx.conf edit | `trusted_proxies` in global block | +| Pi-hole sync | Scrapes SQLite DB | Parses Caddyfile | + +## Management + +```bash +# SSH to host +ssh pihole + +# Validate config +docker exec caddy caddy validate --config /etc/caddy/Caddyfile + +# Reload after editing (zero downtime) +docker exec caddy caddy reload --config /etc/caddy/Caddyfile + +# View logs +docker logs caddy -f + +# Check cert status +docker exec caddy caddy list-modules # verify cloudflare module loaded +curl -s localhost:2019/config/ | jq . # full runtime config via API + +# Rebuild container (after Dockerfile changes) +cd ~/caddy && docker compose up -d --build +``` diff --git a/server-configs/caddy-migration/docker-compose.yml b/server-configs/caddy-migration/docker-compose.yml new file mode 100644 index 0000000..c4c80fe --- /dev/null +++ b/server-configs/caddy-migration/docker-compose.yml @@ -0,0 +1,29 @@ +services: + caddy: + # Custom build with Cloudflare DNS plugin for DNS-01 challenges + build: + context: . + dockerfile: Dockerfile + container_name: caddy + restart: unless-stopped + ports: + - "80:80" # HTTP (redirect to HTTPS) + - "443:443" # HTTPS + - "443:443/udp" # HTTP/3 QUIC + environment: + CF_API_TOKEN: ${CF_API_TOKEN} + TZ: America/Chicago + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data # TLS certs and ACME state + - caddy_config:/config # Runtime config (auto-managed) + networks: + - caddy_network + +networks: + caddy_network: + driver: bridge + +volumes: + caddy_data: + caddy_config: diff --git a/server-configs/caddy-migration/scripts/caddy-pihole-sync.sh b/server-configs/caddy-migration/scripts/caddy-pihole-sync.sh new file mode 100755 index 0000000..6c77c3b --- /dev/null +++ b/server-configs/caddy-migration/scripts/caddy-pihole-sync.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# caddy-pihole-sync.sh +# Syncs all domain names from Caddyfile to Pi-hole local DNS entries. +# Replaces the NPM SQLite-based sync script. +# +# Usage: +# ./caddy-pihole-sync.sh [--dry-run] +# +# Reads the Caddyfile, extracts domain blocks, and writes local DNS +# entries to both Pi-holes pointing all domains to the Caddy host IP. + +set -euo pipefail + +CADDY_IP="10.10.0.16" +CADDYFILE="/home/cal/caddy/Caddyfile" +PRIMARY_PIHOLE_CONTAINER="pihole" +SECONDARY_PIHOLE_HOST="ubuntu-manticore" +SECONDARY_PIHOLE_CONTAINER="pihole" +CUSTOM_LIST_PATH="/etc/pihole/custom.list" +DRY_RUN=false + +if [[ "${1:-}" == "--dry-run" ]]; then + DRY_RUN=true +fi + +# Extract domain names from Caddyfile site blocks +# Matches lines like "subdomain.manticorum.com {" at the start of a block +extract_domains() { + grep -oP '^[a-zA-Z0-9._-]+\.manticorum\.com' "$CADDYFILE" | sort -u +} + +DOMAINS=$(extract_domains) + +if [[ -z "$DOMAINS" ]]; then + echo "ERROR: No domains found in $CADDYFILE" + exit 1 +fi + +echo "=== Caddy -> Pi-hole DNS Sync ===" +echo "Caddy IP: $CADDY_IP" +echo "Domains found: $(echo "$DOMAINS" | wc -l)" +echo "" + +# Build custom.list content (Pi-hole local DNS format: "IP domain") +CUSTOM_ENTRIES="" +while IFS= read -r domain; do + CUSTOM_ENTRIES+="${CADDY_IP} ${domain}"$'\n' + echo " ${CADDY_IP} -> ${domain}" +done <<< "$DOMAINS" + +if $DRY_RUN; then + echo "" + echo "[DRY RUN] Would write to both Pi-holes:" + echo "$CUSTOM_ENTRIES" + exit 0 +fi + +echo "" + +# Sync to primary Pi-hole (local container) +echo "Syncing to primary Pi-hole..." +echo "$CUSTOM_ENTRIES" | docker exec -i "$PRIMARY_PIHOLE_CONTAINER" tee "$CUSTOM_LIST_PATH" > /dev/null +docker exec "$PRIMARY_PIHOLE_CONTAINER" pihole restartdns reload +echo " Primary Pi-hole updated." + +# Sync to secondary Pi-hole (remote host) +echo "Syncing to secondary Pi-hole ($SECONDARY_PIHOLE_HOST)..." +echo "$CUSTOM_ENTRIES" | ssh "$SECONDARY_PIHOLE_HOST" "docker exec -i $SECONDARY_PIHOLE_CONTAINER tee $CUSTOM_LIST_PATH > /dev/null" +ssh "$SECONDARY_PIHOLE_HOST" "docker exec $SECONDARY_PIHOLE_CONTAINER pihole restartdns reload" +echo " Secondary Pi-hole updated." + +echo "" +echo "Sync complete. $(echo "$DOMAINS" | wc -l) domains pointed to $CADDY_IP on both Pi-holes." diff --git a/tdarr/archive/README.md b/tdarr/archive/README.md index 2ebb7ca..498d9ef 100644 --- a/tdarr/archive/README.md +++ b/tdarr/archive/README.md @@ -17,3 +17,7 @@ File completion monitor that watched the local Tdarr cache directory for finishe **Why it existed:** When the local workstation ran as an unmapped Tdarr node, completed transcodes landed in the local NVMe cache. This monitor detected completion (by tracking size stability) and kept the best copy. **Why it's archived:** Same reason as above - mapped node on manticore writes directly to the shared NFS media mount. No local cache to monitor. Archived February 2026. + +## tdarr-flow-backup-2026-03-06.json + +Pre-modification backup of flow `KeayMCz5Y` before adding Ensure AC3 Stereo (`ensAC3str`) and Clear Subtitle Default Flags (`clrSubDef`) plugins. Safe to delete after 2026-04-06 if the flow is running well. diff --git a/tdarr/archive/tdarr-flow-backup-2026-03-06.json b/tdarr/archive/tdarr-flow-backup-2026-03-06.json new file mode 100644 index 0000000..dafcc6c --- /dev/null +++ b/tdarr/archive/tdarr-flow-backup-2026-03-06.json @@ -0,0 +1,637 @@ +{ + "_id": "KeayMCz5Y", + "name": "CPU and GPU Workers with Classic Plugins", + "priority": 0, + "flowPlugins": [ + { + "name": "Remove Non-English Subtitles", + "sourceRepo": "Community", + "pluginName": "runClassicTranscodePlugin", + "version": "2.0.0", + "id": "Yzfcv6TiH", + "position": { + "x": 750.8906176935868, + "y": -187.64825008441858 + }, + "fpEnabled": true, + "inputsDB": { + "pluginSourceId": "Community:Tdarr_Plugin_MC93_Migz4CleanSubs" + } + }, + { + "name": "Input File", + "sourceRepo": "Community", + "pluginName": "inputFile", + "version": "1.0.0", + "fpEnabled": true, + "id": "7a6heYJTK", + "position": { + "x": 745.5374429265526, + "y": -489.16296601060316 + }, + "inputsDB": { + "fileAccessChecks": "true" + } + }, + { + "name": "Check Flow Variable Worker Type", + "sourceRepo": "Community", + "pluginName": "checkFlowVariable", + "version": "1.0.0", + "inputsDB": { + "variable": "{{{args.workerType}}}", + "value": "transcodecpu" + }, + "fpEnabled": true, + "id": "Xmz-M1Kcp", + "position": { + "x": 904.0823200805837, + "y": 320.37825420530805 + } + }, + { + "name": "Run Classic Transcode Plugin GPU", + "sourceRepo": "Community", + "pluginName": "runClassicTranscodePlugin", + "version": "1.0.0", + "fpEnabled": true, + "id": "eqw2BDcZn", + "position": { + "x": 1028.9893158321966, + "y": 421.071749581479 + }, + "inputsDB": { + "force_conform": "true", + "enable_bframes": "false", + "bitrate_cutoff": "6000" + } + }, + { + "name": "Run Classic Transcode Plugin CPU", + "sourceRepo": "Community", + "pluginName": "runClassicTranscodePlugin", + "version": "1.0.0", + "inputsDB": { + "pluginSourceId": "Community:Tdarr_Plugin_MC93_Migz1FFMPEG_CPU" + }, + "fpEnabled": true, + "id": "UINSF-Jto", + "position": { + "x": 838.6820192796439, + "y": 419.44165493200865 + } + }, + { + "name": "Replace Original File", + "sourceRepo": "Community", + "pluginName": "replaceOriginalFile", + "version": "1.0.0", + "fpEnabled": true, + "id": "lxwMPh0uu", + "position": { + "x": 726.7132049347772, + "y": 787.1670408035121 + } + }, + { + "name": "Check Video Codec", + "sourceRepo": "Community", + "pluginName": "checkVideoCodec", + "version": "1.0.0", + "fpEnabled": true, + "id": "proNYXeri", + "position": { + "x": 754.4955267329173, + "y": 248.17934236657024 + } + }, + { + "name": "Check if the worker is CPU or GPU", + "sourceRepo": "Community", + "pluginName": "comment", + "version": "1.0.0", + "fpEnabled": true, + "id": "AKPe0A7V8", + "position": { + "x": 999.1627250319154, + "y": 272.44570924713975 + } + }, + { + "name": "Reorder Streams", + "sourceRepo": "Community", + "pluginName": "ffmpegCommandRorderStreams", + "version": "1.0.0", + "id": "9Iq-auikp", + "position": { + "x": 751.1034972407653, + "y": -19.019617753479935 + }, + "fpEnabled": true, + "inputsDB": { + "languages": "en,eng" + } + }, + { + "name": "Require Mapped Node", + "sourceRepo": "Community", + "pluginName": "tagsWorkerType", + "version": "1.0.0", + "id": "f5zaCdKhe", + "position": { + "x": 607.9938257022797, + "y": 676.6087241705217 + }, + "fpEnabled": true, + "inputsDB": { + "requiredNodeTags": "mapped" + } + }, + { + "name": "Remove Images", + "sourceRepo": "Community", + "pluginName": "runClassicTranscodePlugin", + "version": "2.0.0", + "id": "LZDmWviXF", + "position": { + "x": 752.6675202666282, + "y": 142.72601165821703 + }, + "fpEnabled": true, + "inputsDB": { + "pluginSourceId": "Community:Tdarr_Plugin_MC93_MigzImageRemoval" + } + }, + { + "name": "Compare File Size Ratio", + "sourceRepo": "Community", + "pluginName": "compareFileSizeRatio", + "version": "2.0.0", + "id": "15DNvyWnF", + "position": { + "x": 729.7210215710763, + "y": 562.8112582270492 + }, + "fpEnabled": true, + "inputsDB": { + "greaterThan": "20" + } + }, + { + "name": "Remove Non-English Audio", + "sourceRepo": "Community", + "pluginName": "runClassicTranscodePlugin", + "version": "2.0.0", + "id": "dXiIBVBoj", + "position": { + "x": 754.0056076383194, + "y": -288.45389264024993 + }, + "fpEnabled": true, + "inputsDB": { + "pluginSourceId": "Community:Tdarr_Plugin_MC93_Migz3CleanAudio" + } + }, + { + "name": "Compare File Size", + "sourceRepo": "Community", + "pluginName": "compareFileSize", + "version": "1.0.0", + "id": "XY1JhuLyU", + "position": { + "x": 617.9692774671244, + "y": 434.7074449837517 + }, + "fpEnabled": true + }, + { + "name": "Copy to Working Directory", + "sourceRepo": "Community", + "pluginName": "copyToWorkDirectory", + "version": "1.0.0", + "id": "UGjyFWBfl", + "position": { + "x": 755.0988936143565, + "y": -351.81305110926854 + }, + "fpEnabled": true + }, + { + "name": "Begin Command", + "sourceRepo": "Community", + "pluginName": "ffmpegCommandStart", + "version": "1.0.0", + "id": "l70qMgwzn", + "position": { + "x": 751.9252159058733, + "y": -95.35245695187736 + }, + "fpEnabled": true + }, + { + "name": "Execute", + "sourceRepo": "Community", + "pluginName": "ffmpegCommandExecute", + "version": "1.0.0", + "id": "ui3TYZWDg", + "position": { + "x": 752.856251586643, + "y": 66.97227770409543 + }, + "fpEnabled": true + }, + { + "name": "Require GPU Worker", + "sourceRepo": "Community", + "pluginName": "tagsWorkerType", + "version": "1.0.0", + "id": "fAbSQFbBA", + "position": { + "x": 748.7108813599555, + "y": -405.8573829542489 + }, + "fpEnabled": true, + "inputsDB": { + "requiredWorkerType": "GPU" + } + }, + { + "name": "Keep Original File", + "sourceRepo": "Community", + "pluginName": "setOriginalFile", + "version": "1.0.0", + "id": "8c39N8_x4", + "position": { + "x": 808.1236180203158, + "y": 680.9230520520086 + }, + "fpEnabled": true + }, + { + "name": "Delete File", + "sourceRepo": "Community", + "pluginName": "deleteFile", + "version": "1.0.0", + "id": "WTr9uUZZT", + "position": { + "x": 938.9366947848891, + "y": 608.294687256061 + }, + "fpEnabled": true + } + ], + "flowEdges": [ + { + "source": "Xmz-M1Kcp", + "sourceHandle": "1", + "target": "UINSF-Jto", + "targetHandle": null, + "id": "uidvJfV-Y", + "animated": true, + "type": "smoothstep" + }, + { + "source": "Xmz-M1Kcp", + "sourceHandle": "2", + "target": "eqw2BDcZn", + "targetHandle": null, + "id": "NJYk1xAp8", + "animated": true, + "type": "smoothstep" + }, + { + "source": "proNYXeri", + "sourceHandle": "2", + "target": "Xmz-M1Kcp", + "targetHandle": null, + "id": "SLQVwGIPH", + "animated": true, + "type": "smoothstep" + }, + { + "source": "LZDmWviXF", + "sourceHandle": "1", + "target": "proNYXeri", + "targetHandle": null, + "id": "J-F6uII1U", + "animated": true, + "type": "smoothstep" + }, + { + "source": "LZDmWviXF", + "sourceHandle": "2", + "target": "proNYXeri", + "targetHandle": null, + "id": "_XSYGaZhR", + "animated": true, + "type": "smoothstep" + }, + { + "source": "f5zaCdKhe", + "sourceHandle": "err1", + "target": "3CfIQ9tB3", + "targetHandle": null, + "id": "SCJAwRlBW", + "animated": true, + "type": "smoothstep" + }, + { + "source": "lxwMPh0uu", + "sourceHandle": "1", + "target": "tKE24kDQI", + "targetHandle": null, + "id": "1rkHUq_uL", + "animated": true, + "type": "smoothstep" + }, + { + "source": "proNYXeri", + "sourceHandle": "1", + "target": "XY1JhuLyU", + "targetHandle": null, + "id": "fCYvdisqJ", + "animated": true, + "type": "smoothstep" + }, + { + "source": "UINSF-Jto", + "sourceHandle": "1", + "target": "15DNvyWnF", + "targetHandle": null, + "id": "afIgypYka", + "animated": true, + "type": "smoothstep" + }, + { + "source": "eqw2BDcZn", + "sourceHandle": "1", + "target": "15DNvyWnF", + "targetHandle": null, + "id": "XcXRQcM3e", + "animated": true, + "type": "smoothstep" + }, + { + "source": "f5zaCdKhe", + "sourceHandle": "1", + "target": "lxwMPh0uu", + "targetHandle": null, + "id": "duPtKVO-h", + "animated": true, + "type": "smoothstep" + }, + { + "source": "15DNvyWnF", + "sourceHandle": "1", + "target": "f5zaCdKhe", + "targetHandle": null, + "id": "56GKZLXKO", + "animated": true, + "type": "smoothstep" + }, + { + "source": "XY1JhuLyU", + "sourceHandle": "1", + "target": "15DNvyWnF", + "targetHandle": null, + "id": "D2oKufp5j", + "animated": true, + "type": "smoothstep" + }, + { + "source": "XY1JhuLyU", + "sourceHandle": "3", + "target": "15DNvyWnF", + "targetHandle": null, + "id": "y_Vz0XYft", + "animated": true, + "type": "smoothstep" + }, + { + "source": "l70qMgwzn", + "sourceHandle": "1", + "target": "9Iq-auikp", + "targetHandle": null, + "id": "raNKwDgd9", + "animated": true, + "type": "smoothstep" + }, + { + "source": "9Iq-auikp", + "sourceHandle": "err1", + "target": "LOjmHf_C-", + "targetHandle": null, + "id": "6-L10CXBz", + "animated": true, + "type": "smoothstep" + }, + { + "source": "dXiIBVBoj", + "sourceHandle": "err1", + "target": "LOjmHf_C-", + "targetHandle": null, + "id": "pnzePCmS5", + "animated": true, + "type": "smoothstep" + }, + { + "source": "LZDmWviXF", + "sourceHandle": "err1", + "target": "LOjmHf_C-", + "targetHandle": null, + "id": "t5-OEwnQF", + "animated": true, + "type": "smoothstep" + }, + { + "source": "proNYXeri", + "sourceHandle": "err1", + "target": "LOjmHf_C-", + "targetHandle": null, + "id": "1DvJyGuxP", + "animated": true, + "type": "smoothstep" + }, + { + "source": "Xmz-M1Kcp", + "sourceHandle": "err1", + "target": "LOjmHf_C-", + "targetHandle": null, + "id": "NVvcdTcno", + "animated": true, + "type": "smoothstep" + }, + { + "source": "UINSF-Jto", + "sourceHandle": "err1", + "target": "LOjmHf_C-", + "targetHandle": null, + "id": "gnEvaS5cL", + "animated": true, + "type": "smoothstep" + }, + { + "source": "eqw2BDcZn", + "sourceHandle": "err1", + "target": "LOjmHf_C-", + "targetHandle": null, + "id": "4TD-LzzVI", + "animated": true, + "type": "smoothstep" + }, + { + "source": "XY1JhuLyU", + "sourceHandle": "err1", + "target": "LOjmHf_C-", + "targetHandle": null, + "id": "LDCyY8NfE", + "animated": true, + "type": "smoothstep" + }, + { + "source": "9Iq-auikp", + "sourceHandle": "1", + "target": "ui3TYZWDg", + "targetHandle": null, + "id": "--7zhcAip", + "animated": true, + "type": "smoothstep" + }, + { + "source": "7a6heYJTK", + "sourceHandle": "1", + "target": "fAbSQFbBA", + "targetHandle": null, + "id": "GNSnDA1Sv", + "animated": true, + "type": "smoothstep" + }, + { + "source": "7a6heYJTK", + "sourceHandle": "err1", + "target": "fAbSQFbBA", + "targetHandle": null, + "id": "geaVXXDGc", + "animated": true, + "type": "smoothstep" + }, + { + "source": "fAbSQFbBA", + "sourceHandle": "1", + "target": "UGjyFWBfl", + "targetHandle": null, + "id": "ZOWjXSfzj", + "animated": true, + "type": "smoothstep" + }, + { + "source": "8c39N8_x4", + "sourceHandle": "1", + "target": "lxwMPh0uu", + "targetHandle": null, + "id": "XG_r2OqQU", + "animated": true, + "type": "smoothstep" + }, + { + "source": "15DNvyWnF", + "sourceHandle": "2", + "target": "WTr9uUZZT", + "targetHandle": null, + "id": "VukAoBuEc", + "animated": true, + "type": "smoothstep" + }, + { + "source": "15DNvyWnF", + "sourceHandle": "3", + "target": "WTr9uUZZT", + "targetHandle": null, + "id": "foC7vo2HL", + "animated": true, + "type": "smoothstep" + }, + { + "source": "WTr9uUZZT", + "sourceHandle": "1", + "target": "8c39N8_x4", + "targetHandle": null, + "id": "Se1DbI6Su", + "animated": true, + "type": "smoothstep" + }, + { + "source": "15DNvyWnF", + "sourceHandle": "err1", + "target": "WTr9uUZZT", + "targetHandle": null, + "id": "K5OVJD4LE", + "animated": true, + "type": "smoothstep" + }, + { + "source": "XY1JhuLyU", + "sourceHandle": "2", + "target": "WTr9uUZZT", + "targetHandle": null, + "id": "4KO87eUCC", + "animated": true, + "type": "smoothstep" + }, + { + "source": "UGjyFWBfl", + "sourceHandle": "1", + "target": "dXiIBVBoj", + "targetHandle": null, + "id": "hyC9IIzXG", + "animated": true, + "type": "smoothstep" + }, + { + "source": "ui3TYZWDg", + "sourceHandle": "1", + "target": "LZDmWviXF", + "targetHandle": null, + "id": "_8zjkKXFG", + "animated": true, + "type": "smoothstep" + }, + { + "source": "dXiIBVBoj", + "sourceHandle": "1", + "target": "Yzfcv6TiH", + "targetHandle": null, + "id": "pn4_6ZpQX", + "animated": true, + "type": "smoothstep" + }, + { + "source": "dXiIBVBoj", + "sourceHandle": "2", + "target": "Yzfcv6TiH", + "targetHandle": null, + "id": "N5n80WXqb", + "animated": true, + "type": "smoothstep" + }, + { + "source": "Yzfcv6TiH", + "sourceHandle": "1", + "target": "l70qMgwzn", + "targetHandle": null, + "id": "4Ug0azYg9", + "animated": true, + "type": "smoothstep" + }, + { + "source": "Yzfcv6TiH", + "sourceHandle": "2", + "target": "l70qMgwzn", + "targetHandle": null, + "id": "M2jFArhx4", + "animated": true, + "type": "smoothstep" + } + ], + "isUiLocked": false +} diff --git a/tdarr/troubleshooting.md b/tdarr/troubleshooting.md index 5b08c1e..76c7841 100644 --- a/tdarr/troubleshooting.md +++ b/tdarr/troubleshooting.md @@ -246,6 +246,64 @@ crontab -e # Delete tdarr lines - **Progress data**: Lost on container restart (unmapped nodes) - **Cache files**: Safe to delete, will re-download +## Database Modification & Requeue + +### Problem: UI "Requeue All" Button Has No Effect +**Symptoms**: Clicking "Requeue all items (transcode)" in library UI does nothing + +**Workaround**: Modify SQLite DB directly, then trigger scan: +```bash +# 1. Reset file statuses in DB (run Python on manticore) +python3 -c " +import sqlite3 +conn = sqlite3.connect('/home/cal/docker/tdarr/server-data/Tdarr/DB2/SQL/database.db') +conn.execute(\"UPDATE filejsondb SET json_data = json_set(json_data, '$.TranscodeDecisionMaker', '') WHERE json_extract(json_data, '$.DB') = ''\") +conn.commit() +conn.close() +" + +# 2. Restart Tdarr +cd /home/cal/docker/tdarr && docker compose down && docker compose up -d + +# 3. Trigger scan (required — DB changes alone won't queue files) +curl -s -X POST "http://localhost:8265/api/v2/scan-files" \ + -H "Content-Type: application/json" \ + -d '{"data":{"scanConfig":{"dbID":"","arrayOrPath":"/media/Movies/","mode":"scanFindNew"}}}' +``` + +**Library IDs**: Movies=`ZWgKkmzJp`, TV Shows=`EjfWXCdU8` + +**Note**: The CRUD API (`/api/v2/cruddb`) silently ignores write operations (update/insert/upsert all return 200 but don't persist). Always modify the SQLite DB directly. + +### Problem: Library filterCodecsSkip Blocks Flow Plugins +**Symptoms**: Job report shows "File video_codec_name (hevc) is in ignored codecs" +**Cause**: `filterCodecsSkip: "hevc"` in library settings skips files before the flow runs +**Solution**: Clear the filter in DB — the flow's own logic handles codec decisions: +```bash +# In librarysettingsjsondb, set filterCodecsSkip to empty string +``` + +## Flow Plugin Issues + +### Problem: clrSubDef Disposition Change Not Persisting (SRT→ASS Re-encode) +**Symptoms**: Job log shows "Clearing default flag from subtitle stream" but output file still has default subtitle. SRT subtitles become ASS in output. +**Root Cause**: The `clrSubDef` custom function pushed `-disposition:{outputIndex} 0` to `outputArgs` without also specifying `-c:{outputIndex} copy`. Tdarr's Execute plugin skips adding default `-c:N copy` for streams with custom `outputArgs`. Without a codec spec, ffmpeg re-encodes SRT→ASS (MKV default), resetting the disposition. +**Fix**: Always include codec copy when adding outputArgs: +```javascript +// WRONG - causes re-encode +stream.outputArgs.push('-disposition:{outputIndex}', '0'); +// RIGHT - preserves codec, changes only disposition +stream.outputArgs.push('-c:{outputIndex}', 'copy', '-disposition:{outputIndex}', '0'); +``` + +### Problem: ensAC3str Matches Commentary Tracks as Existing AC3 Stereo +**Symptoms**: File has commentary AC3 2ch track but no main-audio AC3 stereo. Plugin logs "File already has en stream in ac3, 2 channels". +**Root Cause**: The community `ffmpegCommandEnsureAudioStream` plugin doesn't filter by track title — any AC3 2ch eng track satisfies the check, including commentary. +**Fix**: Replaced with `customFunction` that filters out tracks with "commentary" in the title tag before checking. Updated in flow `KeayMCz5Y` via direct SQLite modification. + +### Combined Impact: Roku Playback Hang +When both bugs occur together (TrueHD default audio + default subtitle not cleared), Jellyfin must transcode audio AND burn-in subtitles simultaneously over HLS. The ~30s startup delay causes Roku to timeout at ~33% loading. Fixing either bug alone unblocks playback — clearing the subtitle default is sufficient since TrueHD-only transcoding is fast enough. + ## Common Error Patterns ### "Copy failed" in Staging Section