Here's what Claude Code gave me when I was looking at this earlier:
What's actually broken
Four places, all defined inside main.go, call the adb binary with no PYATV check at all:
Function main.go Trigger
adbControl 899, 958 /api/tuner/:index/control/:action, /api/tuners/control/:action (remote buttons)
audioBaseline β adbAudio 429 Every network tune, when PLAYBACK_DETECTION=TRUE
waitForPlayback β adbAudio 239 Same tune, polls during playback
Fixing this means: when PYATV=true, none of these paths may exec adb. The remote-control endpoints should return "unsupported for pyatv tuners" instead of shelling out, and the playback-detection gate should skip straight to whatever fallback already exists (the code already treats an unreachable adb target as "gate on motion alone" β main.go:2293 β so a PYATV deployment should hit that same fallback state without ever spawning adb).
The actual obstacle
The logic that needs to change lives entirely in main.go, and CLAUDE.md is explicit: don't edit that file, even when it's clearly the right place, even for a real bug β "fix the contention instead, or raise it and wait to be told." There's no other file to redirect this fix into the way rule 7 does for the captions/driver contention, because this code doesn't call out to another component β it is the component.
Two honest ways to reconcile that:
A. Relocate, don't edit. Move adbControl, adbAudio, audioBaseline, waitForPlayback, adbKeycodes, and the two constants into a new file (e.g. tunerremote.go), matching the project's existing pattern of "domain logic gets its own file, main.go just wires it up" (same rule already applied to captions/model files). Since Go doesn't care which file in a package defines a symbol, every call site in main.go stays byte-identical β no inline edits, no reformatting. The PYATV gate goes inside the relocated functions. Net effect: main.go loses ~150-200 lines it currently owns.
B. Edit in place. Add PYATV checks directly at the two API handlers and inside audioBaseline/waitForPlayback, leaving everything else where it is. Smallest possible diff, but it's a direct edit to the protected file.
Either way, the same downstream work applies once the gate is in:
Add a small pyatvEnabled() helper (strings.EqualFold(os.Getenv("PYATV"), "true")).
Confirm waitForPlayback's caller (main.go:239) tolerates a nil baseline / no-op gate cleanly β no goroutine left blocked on ready/gateDone if the adb path is skipped outright rather than failing into it.
Decide the API response for pyatv tuners hitting /control/:action β 400 with a clear reason, not a silent no-op.
Update README.md's pyatv section if it currently implies remote-control buttons work there.
Since main and ui-refactor must stay byte-identical apart from Dockerfile/build.yml, whatever's implemented gets committed to main and cherry-picked to ui-refactor, same as caption work.
Option A is the one I'd lean toward β it's consistent with how the rest of the codebase already separates concerns out of main.go, and it doesn't touch a single existing line of that file. But given CLAUDE.md's instruction to wait to be told rather than assume, I want your call before I write any code.
AskUserQuestion
OUT
Your questions have been answered: "How should the PYATV/adb fix be implemented, given main.go is off-limits by default?"="Just write up the issue, no code yet". You can now continue with these answers in mind.
Issue: ADB keys generated under /root/.android when PYATV=true
Symptom: Users running with PYATV=true (pyatv/atvremote-based tuners, no Android devices) see adb key files (adbkey, adbkey.pub) appear under /root/.android in the container.
Root cause: docker-start-beta.sh correctly branches on PYATV at startup β it calls atvConnections (which only shells out to atvremote) instead of adbConnections when PYATV=true. But that branching is startup-script-only. The Go binary itself has no PYATV awareness (grep -r PYATV *.go returns nothing), and four code paths in main.go call the adb binary unconditionally, independent of tuner type:
Function Location Trigger
adbControl main.go:899 POST /api/tuner/:index/control/:action β remote-control buttons in the UI
adbControl main.go:958 POST /api/tuners/control/:action β bulk remote control
audioBaseline β adbAudio main.go:429 Every network tune, when PLAYBACK_DETECTION=TRUE
waitForPlayback β adbAudio main.go:239 Polls during the same tune
The adb client host tool generates its keypair on first invocation, whether or not any device answers β it doesn't need a successful connection, just a process start. So on a PYATV deployment with PLAYBACK_DETECTION=TRUE, the very first tune runs adb connect <appletv-ip> (main.go:2247), which can never succeed against an Apple TV, but still spins up the local adb server and drops the key files before waitForPlayback gives up after 3 failures (main.go:2292) and falls back to motion-only gating. The same thing happens if a user presses a remote-control button against a pyatv tuner in the web UI.
What "fixed" looks like:
/api/tuner/:index/control/:action and /api/tuners/control/:action should reject (e.g. 400, "not supported for pyatv tuners") instead of calling adbControl when PYATV=true.
The playback-detection tune path should skip audioBaseline/waitForPlayback entirely under PYATV=true and go straight to the "gate on motion alone" fallback state that already exists for unreachable adb targets β without ever spawning adb.
Why this isn't a quick patch here: the logic that needs to change is defined entirely inside main.go, and CLAUDE.md says not to edit that file β its diff against upstream is kept purely additive, and ui-refactor carries it. That rule holds "even when main.go looks like the right place," which this is. Unlike the caption/driver contention case (rule 7 in CLAUDE.md), there's no separate component to redirect the fix into β adbControl, adbAudio, audioBaseline, and waitForPlayback are defined in main.go itself, not called into it from elsewhere.
Two real options when this gets picked up (left for whoever owns that decision, per "raise it and wait to be told"):
Relocate the four functions (+ adbKeycodes, adbTimeout, adbGiveUp) into a new file (e.g. tunerremote.go) and gate on PYATV there. Every call site in main.go stays byte-identical since Go doesn't care which file in the package defines a symbol β but main.go still loses ~150-200 lines it currently owns, so this is a real diff even though no existing line is touched.
Edit main.go directly with inline PYATV checks at the four sites β smallest footprint, but a direct edit to the protected file.
Either path also needs: a pyatvEnabled() helper, confirming waitForPlayback's caller doesn't leave a goroutine blocked on ready/gateDone when the gate is skipped outright, and β since main and ui-refactor must stay byte-identical apart from Dockerfile/build.yml β committing to main and cherry-picking to ui-refactor.
Not sure about the recommendations, but this should at least give you an idea where to focus.