mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 13:59:40 +00:00
fix(fmrelay): start path + JSON API + mounts XML + inline live preview
Four bugs surfaced when actually running a relay live (#377): 1. cmd_start sanity check still referenced lame_present after the ffmpeg switch — every start failed with `fmrelayctl: line 209: lame_present: command not found`. Renamed to ffmpeg_present. 2. log()/warn() wrote the human banner to stdout, which the API's subprocess.check_output(..., stderr=subprocess.STDOUT) merged into the JSON parser. /start returned 500 even when the runner spawned correctly. log() now writes to stderr; api/_ctl_json switches to subprocess.run(capture_output=True) so stdout stays clean JSON and stderr is only surfaced on failure. 3. emit_mounts_json grep'd `<mount>...</mount>` elements but icecast2 /admin/listmounts uses `<source mount="...">` attribute form. The mounts table always showed empty. New parser handles the real XML shape + reports connected_sec + content_type alongside listeners. 4. admin webui had only the "Open Webradio →" link (which targets a vhost that doesn't exist yet). Added a card with `<audio controls>` that auto-binds to the active LAN mount via /access + /mounts, so the operator can verify playback without leaving the dashboard.
This commit is contained in:
parent
79ee6468e8
commit
14a7590c82
|
|
@ -45,18 +45,22 @@ app = FastAPI(
|
|||
|
||||
|
||||
def _ctl_json(*args: str) -> Dict[str, Any]:
|
||||
"""Invoke fmrelayctl with --json and parse the result."""
|
||||
"""Invoke fmrelayctl with --json and parse the result.
|
||||
|
||||
stderr is captured separately so human-readable log lines from
|
||||
`log()` don't pollute the JSON parser on stdout. The captured
|
||||
stderr is only surfaced when the call fails."""
|
||||
cmd = [CTL, *args, "--json"]
|
||||
try:
|
||||
out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, timeout=15)
|
||||
proc = subprocess.run(cmd, capture_output=True, timeout=15, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise HTTPException(status_code=500, detail=f"fmrelayctl failed: {e.output!r}")
|
||||
raise HTTPException(status_code=500, detail=f"fmrelayctl failed: {e.stderr!r}")
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=500, detail=f"fmrelayctl not found at {CTL}")
|
||||
try:
|
||||
return json.loads(out.decode())
|
||||
return json.loads(proc.stdout.decode())
|
||||
except json.JSONDecodeError as e:
|
||||
raise HTTPException(status_code=500, detail=f"fmrelayctl non-JSON: {e}; raw={out!r}")
|
||||
raise HTTPException(status_code=500, detail=f"fmrelayctl non-JSON: {e}; stdout={proc.stdout!r}; stderr={proc.stderr!r}")
|
||||
|
||||
|
||||
def _ctl_text(*args: str) -> str:
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ readonly GREEN='\033[0;32m'
|
|||
readonly YELLOW='\033[1;33m'
|
||||
readonly NC='\033[0m'
|
||||
|
||||
log() { printf '%b[fmrelay]%b %s\n' "$GREEN" "$NC" "$*"; }
|
||||
log() { printf '%b[fmrelay]%b %s\n' "$GREEN" "$NC" "$*" >&2; }
|
||||
warn() { printf '%b[warn]%b %s\n' "$YELLOW" "$NC" "$*" >&2; }
|
||||
err() { printf '%b[error]%b %s\n' "$RED" "$NC" "$*" >&2; }
|
||||
|
||||
|
|
@ -165,13 +165,28 @@ emit_mounts_json() {
|
|||
return
|
||||
fi
|
||||
# Hand-parse the XML response — full XML parser would need lxml as
|
||||
# a hard dep on the host. icecast's <listmounts> shape is stable.
|
||||
# a hard dep on the host. icecast2 /admin/listmounts shape:
|
||||
# <source mount="/foo.mp3">
|
||||
# <listeners>0</listeners>
|
||||
# <Connected>63</Connected>
|
||||
# <content-type>audio/mpeg</content-type>
|
||||
# </source>
|
||||
python3 - <<PY
|
||||
import re, json
|
||||
xml = """${raw//\"/\\\"}"""
|
||||
mounts = []
|
||||
for m in re.finditer(r"<mount>([^<]+)</mount>.*?<listeners>(\d+)</listeners>", xml, re.S):
|
||||
mounts.append({"mount": m.group(1), "listeners": int(m.group(2))})
|
||||
for m in re.finditer(r'<source\b[^>]*\bmount="([^"]+)"[^>]*>(.*?)</source>', xml, re.S):
|
||||
mount, body = m.group(1), m.group(2)
|
||||
def field(name):
|
||||
mm = re.search(rf"<{name}>([^<]*)</{name}>", body)
|
||||
return mm.group(1) if mm else ""
|
||||
listeners = field("listeners") or "0"
|
||||
mounts.append({
|
||||
"mount": mount,
|
||||
"listeners": int(listeners),
|
||||
"connected_sec": int(field("Connected") or 0),
|
||||
"content_type": field("content-type"),
|
||||
})
|
||||
print(json.dumps({"module": "fmrelay", "mounts": mounts}))
|
||||
PY
|
||||
}
|
||||
|
|
@ -206,7 +221,7 @@ cmd_start() {
|
|||
|
||||
if ! icecast_running; then err "icecast2 not running on ${ICECAST_HOST}:${ICECAST_PORT}"; exit 2; fi
|
||||
if ! rtl_fm_present; then err "rtl_fm not installed (apt install rtl-sdr)"; exit 2; fi
|
||||
if ! lame_present; then err "lame not installed (apt install lame)"; exit 2; fi
|
||||
if ! ffmpeg_present; then err "ffmpeg not installed (apt install ffmpeg)"; exit 2; fi
|
||||
|
||||
log "Starting fmrelay: freq=$freq mount=$mount bitrate=${bitrate}k gain=${gain}dB ppm=${ppm}"
|
||||
echo "$freq" > "$CURRENT_FREQ_FILE"
|
||||
|
|
|
|||
|
|
@ -67,6 +67,18 @@
|
|||
</a>
|
||||
</header>
|
||||
|
||||
<section class="card" style="margin-bottom: 1rem;">
|
||||
<h3>Live preview</h3>
|
||||
<p class="hint" style="margin-bottom: 0.5rem;">
|
||||
Direct playback of the active LAN mount — handy to verify the stream
|
||||
is healthy without leaving the dashboard.
|
||||
</p>
|
||||
<audio id="live-audio" controls preload="none" style="width: 100%; max-width: 500px;"></audio>
|
||||
<div id="live-audio-meta" style="margin-top: 0.5rem; font-size: 0.85rem; color: var(--muted);">
|
||||
no active mount
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cards">
|
||||
<article class="card">
|
||||
<h3>Status</h3>
|
||||
|
|
@ -180,6 +192,11 @@
|
|||
const target = (pub && pub.url) || (lan && lan.url) || "#";
|
||||
const btn = document.getElementById("open-webradio");
|
||||
if (btn) btn.href = target;
|
||||
|
||||
// Cache the LAN base for refreshLivePreview() (called by loadMounts
|
||||
// once the mounts list is in — avoids relying on poll order).
|
||||
window._fmrelayLanBase = (lan && lan.url) ? lan.url.replace(/\/$/, "") : "";
|
||||
refreshLivePreview();
|
||||
} catch (e) {
|
||||
document.getElementById("access-v").innerHTML = `<span class="empty">${esc(e.message)}</span>`;
|
||||
}
|
||||
|
|
@ -209,6 +226,8 @@
|
|||
try {
|
||||
const d = await api("/mounts");
|
||||
const list = d.mounts || [];
|
||||
window._fmrelayLastMounts = list;
|
||||
refreshLivePreview();
|
||||
if (!list.length) {
|
||||
wrap.innerHTML = `<div class="empty">no active mounts${d.reason ? " — " + esc(d.reason) : ""}</div>`;
|
||||
return;
|
||||
|
|
@ -257,6 +276,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
function refreshLivePreview() {
|
||||
try {
|
||||
const audio = document.getElementById("live-audio");
|
||||
const meta = document.getElementById("live-audio-meta");
|
||||
const base = window._fmrelayLanBase || "";
|
||||
const mounts = window._fmrelayLastMounts || [];
|
||||
const liveMount = mounts.find(m => (m.connected_sec || 0) > 0);
|
||||
if (audio && base && liveMount) {
|
||||
const url = base + liveMount.mount;
|
||||
if (audio.getAttribute("src") !== url) audio.setAttribute("src", url);
|
||||
if (meta) meta.textContent = `mount ${liveMount.mount} · ${liveMount.connected_sec}s · ${liveMount.content_type || ""}`;
|
||||
} else if (audio) {
|
||||
audio.removeAttribute("src");
|
||||
if (meta) meta.textContent = "no active mount — start a relay below";
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function loadAll() {
|
||||
loadStatus();
|
||||
loadComponents();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user