Files
qbz/scripts/qconnect-lan-fake-renderer.sh
T
vicrodh a106ef790c feat(qt): Qobuz Connect lists renderers on this network and pairs them
A LAN-only receiver — a BluOS player, another QBZ — was invisible until an
official app paired it into the session; that is the "it showed up once,
for a day" report. The Qt LAN runtime now browses _qobuz-connect._tcp
alongside its own receiver (same lifetime: Qobuz Connect enabled and
ready), probes every announcement that is not us, and the picker shows the
answers under "On this network" with a Pair action. Pairing mints the
delegated pair with /qws/delegateAuth for the renderer's app id and hands
it over with this controller's session uuid and become_active, exactly as
the official controllers do; the cloud then lists the device and its LAN
row drops out by uuid. scripts/qconnect-lan-fake-renderer.sh is a receiver
that never joins the cloud, for exercising this without a Node. Pending:
qbzd parity, the 5-minute retry backoff, become_active=false.

cc
2026-09-13 13:46:13 -06:00

72 lines
3.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# A fake Qobuz Connect LAN receiver, for exercising QBZ's controller half
# without a BluOS player: announces `_qobuz-connect._tcp` through Avahi and
# serves the three official endpoints. It never joins the cloud (it has no
# credentials of its own), so from QBZ's point of view it is exactly what a
# Bluesound Node is before an official app pairs it: on the network, not in
# the session. The handoff body it receives is logged with both JWTs redacted.
#
# bash scripts/qconnect-lan-fake-renderer.sh [port] [friendly name]
#
# Needs avahi-publish-service (avahi-utils) and python3. Ctrl-C stops both.
set -euo pipefail
PORT="${1:-8765}"
NAME="${2:-Fake Node}"
UUID="${FAKE_DEVICE_UUID:-$(python3 -c 'import uuid; print(uuid.uuid4())')}"
command -v avahi-publish-service >/dev/null || { echo "avahi-publish-service not found (install avahi-utils)"; exit 1; }
avahi-publish-service "$NAME" _qobuz-connect._tcp "$PORT" "device_uuid=$UUID" "sdk_version=1.0.0" "path=" &
AVAHI_PID=$!
trap 'kill $AVAHI_PID 2>/dev/null || true' EXIT
echo "[fake-renderer] announcing '$NAME' device_uuid=$UUID on port $PORT"
NAME="$NAME" UUID="$UUID" PORT="$PORT" python3 - <<'PY'
import json, os, sys
from http.server import BaseHTTPRequestHandler, HTTPServer
NAME, UUID, PORT = os.environ["NAME"], os.environ["UUID"], int(os.environ["PORT"])
class Handler(BaseHTTPRequestHandler):
def _json(self, payload):
body = json.dumps(payload).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path.rstrip("/").endswith("get-display-info"):
return self._json({
"friendly_name": NAME, "serial_number": UUID[:8],
"brand_display_name": "QBZ", "model_display_name": "Fake receiver",
"max_audio_quality": "UP_TO_HIRES_192", "type": "Streamer",
"software_version": "0.0.1",
})
if self.path.rstrip("/").endswith("get-connect-info"):
return self._json({"app_id": "fake-renderer-app-id", "current_session_id": None})
self.send_error(404)
def do_POST(self):
if not self.path.rstrip("/").endswith("connect-to-qconnect"):
return self.send_error(404)
length = int(self.headers.get("Content-Length", "0"))
raw = self.rfile.read(length)
try:
body = json.loads(raw)
for key in ("jwt_api", "jwt_qconnect"):
if isinstance(body.get(key), dict) and "jwt" in body[key]:
body[key]["jwt"] = f"<{len(body[key]['jwt'])} bytes redacted>"
print("[fake-renderer] connect-to-qconnect:", json.dumps(body, indent=2), flush=True)
except Exception as exc: # noqa: BLE001
print("[fake-renderer] connect-to-qconnect: unparsable body:", exc, flush=True)
self._json({})
def log_message(self, fmt, *args):
print("[fake-renderer]", self.address_string(), fmt % args, flush=True)
print(f"[fake-renderer] serving on 0.0.0.0:{PORT}", flush=True)
HTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
PY