Let the Device field be picked, not typed blind
Diagnosing capture on a real TV meant reading pactlSources off the screen and typing an exact PulseAudio source name back in through the same remote-driven text field — no way to copy-paste, easy to mistype, and the one piece of information (which source, if any, is actually RUNNING) was buried in a JSON dump. Added two choice() pickers bound to the same capture.device setting: one built from pactlSources (pulse/auto backends), one built from alsaCapturePcms (alsa backend), both parsed from diagnostics the service already collects — no new Luna method needed. Diagnostics already run once at boot, so the picker is populated immediately, before the user ever presses "Run diagnostics" by hand. Picking a value writes straight into capture.device, and the plain text field stays as the fallback for anything the parser misses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f3a4cddfd6
commit
0759cc00aa
Executable
+108
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Checks unraid/lgtv-audiocap-loopback.plg without needing an Unraid box.
|
||||
|
||||
Verifies the plugin is well-formed XML (a CDATA-free bash script anywhere in
|
||||
it means a stray "&" or "<" one edit away from breaking the DOCTYPE entity
|
||||
expansion Unraid's installer relies on), that entities substitute the way
|
||||
Unraid's installer would substitute them, that both embedded scripts are
|
||||
syntactically valid bash, and that the install/remove pair is idempotent and
|
||||
symmetric against a scratch go-file.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import os
|
||||
import xml.dom.minidom as minidom
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
PLG = os.path.join(HERE, os.pardir, "unraid", "lgtv-audiocap-loopback.plg")
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
|
||||
def check(condition, description):
|
||||
global passed, failed
|
||||
if condition:
|
||||
print(" ok %s" % description)
|
||||
passed += 1
|
||||
else:
|
||||
print(" FAIL %s" % description)
|
||||
failed += 1
|
||||
|
||||
|
||||
def bash_syntax_ok(script):
|
||||
result = subprocess.run(["bash", "-n"], input=script, text=True,
|
||||
capture_output=True)
|
||||
return result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
def main():
|
||||
doc = minidom.parse(PLG)
|
||||
|
||||
plugin = doc.getElementsByTagName("PLUGIN")
|
||||
check(len(plugin) == 1, "exactly one PLUGIN element")
|
||||
attrs = dict(plugin[0].attributes.items()) if plugin else {}
|
||||
for key in ("name", "author", "version", "pluginURL", "min"):
|
||||
check(bool(attrs.get(key)), "PLUGIN has a non-empty %s attribute" % key)
|
||||
check(attrs.get("name") == "lgtv-audiocap-loopback", "name matches the filename's stem")
|
||||
check(attrs.get("pluginURL", "").endswith(attrs.get("name", "\0") + ".plg"),
|
||||
"pluginURL points at this same file's name")
|
||||
|
||||
files = doc.getElementsByTagName("FILE")
|
||||
check(len(files) == 2, "exactly two FILE blocks (install + remove)")
|
||||
|
||||
install_script = remove_script = None
|
||||
for f in files:
|
||||
inline = f.getElementsByTagName("INLINE")
|
||||
check(len(inline) == 1, "FILE (Method=%s) has one INLINE child" % (f.getAttribute("Method") or "install"))
|
||||
script = inline[0].firstChild.data if inline and inline[0].firstChild else ""
|
||||
ok, stderr = bash_syntax_ok(script)
|
||||
check(ok, "FILE (Method=%s) script is valid bash%s" % (
|
||||
f.getAttribute("Method") or "install", "" if ok else ": " + stderr.strip()))
|
||||
if f.getAttribute("Method") == "remove":
|
||||
remove_script = script
|
||||
else:
|
||||
install_script = script
|
||||
|
||||
check(install_script is not None, "found the install script")
|
||||
check(remove_script is not None, "found the remove script")
|
||||
check("lgtv-audiocap-loopback" in (install_script or ""),
|
||||
"&name; entity actually expanded inside the install script (not left literal)")
|
||||
|
||||
# The plugin appends to /boot/config/go; redirect that at a scratch file
|
||||
# to exercise the real install/remove logic end to end, not just parse it.
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
go = os.path.join(tmp, "go")
|
||||
with open(go, "w") as fh:
|
||||
fh.write("#!/bin/bash\n/usr/local/sbin/emhttp\n")
|
||||
original = open(go).read()
|
||||
|
||||
# modprobe isn't run for real here; the script already tolerates that
|
||||
# (it warns and continues), so there's nothing to stub out beyond
|
||||
# keeping its stderr out of /tmp.
|
||||
env_script = install_script.replace("GO=/boot/config/go", "GO=%s" % go)
|
||||
env_script = env_script.replace("/tmp/${NAME}.err", os.path.join(tmp, "err"))
|
||||
subprocess.run(["bash", "-c", env_script], check=True)
|
||||
after_install = open(go).read()
|
||||
check(after_install != original, "install actually appended something to go")
|
||||
check("modprobe snd-aloop" in after_install, "the modprobe line ended up in go")
|
||||
|
||||
subprocess.run(["bash", "-c", env_script], check=True)
|
||||
after_second_install = open(go).read()
|
||||
check(after_second_install == after_install, "installing twice does not duplicate the block")
|
||||
|
||||
env_remove = remove_script.replace("GO=/boot/config/go", "GO=%s" % go)
|
||||
env_remove = env_remove.replace("/sbin/rmmod snd_aloop 2>/dev/null || true", "true")
|
||||
subprocess.run(["bash", "-c", env_remove], check=True)
|
||||
after_remove = open(go).read()
|
||||
check(after_remove == original, "remove restores go to its original contents exactly")
|
||||
|
||||
print()
|
||||
print("%d/%d checks passed" % (passed, passed + failed))
|
||||
return 0 if failed == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user