docs: add caddy migration config, tdarr flow backup, and troubleshooting updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-03-08 13:13:15 -05:00
parent 1416af2ebb
commit c08e779e42
12 changed files with 1431 additions and 1 deletions

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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

View File

@ -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=<your 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 <VMID> 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 <VMID> 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"
```

View File

@ -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
```

View File

@ -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:

View File

@ -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."

View File

@ -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.

View File

@ -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
}

View File

@ -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') = '<LIBRARY_ID>'\")
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":"<LIBRARY_ID>","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