From f339197861e84aa98ad621b86016b4d59d5ee10b Mon Sep 17 00:00:00 2001 From: luzadev Date: Tue, 9 Jun 2026 01:08:11 +0200 Subject: [PATCH] Registra: messaggi di errore azionabili + log tecnici espandibili Sul sistema dell'utente il comando ffmpeg che usa l'app funziona correttamente (testato come subprocess identico), ma il vecchio reporting era confuso quando l'errore era transitorio (BlackHole non pronto, clock drift, ecc.) o quando la parola 'abort' non compariva nello stderr. Cambiamenti: core/recorder.py: - _extract_friendly_error riconosce ora piu casi: input/output error (BlackHole non pronto -> suggerisce 'sudo killall coreaudiod'), device not configured, permission denied, no such device - rc=-6 su macOS ritorna SEMPRE il messaggio sul permesso Microfono (prima richiedeva 'abort' nello stderr, non sempre presente) - Fallback finale mostra anche le ultime 3 righe di stderr - Nuova funzione get_last_stderr() per debugging api/bridge.py: - Nuovo metodo get_recorder_log() che ritorna stderr per la UI webui: - Nuova card di errore persistente con bottoni 'Mostra log tecnici' / 'Copia log' - Si resetta automaticamente all'avvio di una nuova registrazione Co-Authored-By: Claude Opus 4.7 --- api/bridge.py | 6 ++++ core/recorder.py | 69 +++++++++++++++++++++++++++++++++++---------- webui/css/style.css | 35 +++++++++++++++++++++++ webui/index.html | 10 +++++++ webui/js/app.js | 36 ++++++++++++++++++++++- 5 files changed, 140 insertions(+), 16 deletions(-) diff --git a/api/bridge.py b/api/bridge.py index 1e71fb4..c1dbcfc 100644 --- a/api/bridge.py +++ b/api/bridge.py @@ -27,6 +27,7 @@ from core.recorder import ( start_recording, stop_recording as recorder_stop, is_recording, + get_last_stderr as recorder_last_stderr, ) from core.upgrader import ( upgrade_folder, @@ -376,6 +377,11 @@ class Api: def is_audio_recording(self) -> dict: return {"recording": is_recording()} + def get_recorder_log(self) -> dict: + """Ritorna le ultime righe di stderr di ffmpeg dell'ultima sessione + di registrazione. Utile per diagnosticare un errore.""" + return {"ok": True, "lines": recorder_last_stderr(80)} + # ------------------------------------------------------------------ # Audio folder scan (Upgrade tab) # ------------------------------------------------------------------ diff --git a/core/recorder.py b/core/recorder.py index 468d222..ca9818f 100644 --- a/core/recorder.py +++ b/core/recorder.py @@ -335,39 +335,78 @@ def start_recording( def _extract_friendly_error(rc: int) -> str: """Trasforma rc + stderr di ffmpeg in un messaggio leggibile. Riconosce i casi tipici: permessi microfono, device occupato, - device non disponibile, ...""" + device non disponibile, clock drift di BlackHole, ecc. + + Fallback: se non riconosce il pattern, ritorna le ultime righe + significative dello stderr cosi' l'utente puo' diagnosticare + o aprire un ticket. + """ with _stderr_lock: lines = list(_stderr_buf) text = "\n".join(lines).lower() + is_mac = sys.platform == "darwin" - if "abort" in text and rc == -6 and sys.platform == "darwin": + # ---- Errori specifici (priorita' alta) ---- + + if "input/output error" in text or "errno 22" in text: return ( - "ffmpeg abortito (errore -6): probabilmente l'app non ha il " - "permesso 'Microfono' di macOS. Vai su Preferenze di Sistema " - "→ Privacy e sicurezza → Microfono e abilita Terminal/Python. " - "Poi riavvia l'app." + "Dispositivo audio non pronto (BlackHole o scheda virtuale). " + "Spesso si risolve cosi':\n" + " 1) Smetti la registrazione e riprova fra 5 secondi\n" + " 2) Se persiste, da Terminale: 'sudo killall coreaudiod'\n" + " 3) Verifica che nessun'altra app stia gia' registrando il device" ) - if any(s in text for s in ("input/output error", "errno 22", "device not configured")): - return "Dispositivo non disponibile (riavvia il servizio audio o ricontrolla la selezione)." + if "device not configured" in text or "device not available" in text: + return "Dispositivo non configurato. Premi 'Aggiorna' e riseleziona." - if "permission" in text or "not authorized" in text or "not permitted" in text: - return "Permesso microfono negato da macOS. Abilita Python/MusicTools in Privacy → Microfono." + if "permission" in text or "not authorized" in text or "not permitted" in text \ + or "tcc" in text or "denied" in text: + return _permission_message() - if "no such device" in text or "no such audio device" in text: - return "Dispositivo non trovato. Premi '↻ Aggiorna' e riseleziona." + if "no such device" in text or "no such audio device" in text or "invalid device" in text: + return "Dispositivo non trovato. Premi 'Aggiorna' e riseleziona dalla lista." - # Restituisci l'ultima riga utile dello stderr (esclude le righe di banner/progress) + # ---- Macro per rc=-6 (SIGABRT) su macOS ---- + # Su macOS rc=-6 e' quasi sempre il sintomo di un crash di ffmpeg + # dovuto al permesso Microfono mancante. La parola 'abort' non + # sempre compare nello stderr di ffmpeg recenti. + if rc == -6 and is_mac: + return _permission_message() + + # ---- Fallback: cerca le ultime righe significative ---- for line in reversed(lines): l = line.strip() if not l: continue + low = l.lower() + # Riga AVFoundation/dshow specifica (es. "[avfoundation @ 0x...] Could not...") if l.startswith("[avfoundation") or l.startswith("[dshow") or "@ 0x" in l: return l - if "error" in l.lower() or "fail" in l.lower() or "denied" in l.lower(): + if "error" in low or "fail" in low or "denied" in low or "cannot" in low: return l - return f"ffmpeg exit {rc}" + + # Ultima spiaggia: rc + ultime 3 righe di stderr per debug + tail = " | ".join(line for line in lines[-3:] if line.strip()) or "(nessuno)" + return f"ffmpeg exit {rc}. Ultimo stderr: {tail}" + + +def _permission_message() -> str: + """Messaggio standardizzato per il problema permesso Microfono macOS.""" + return ( + "Permesso Microfono mancante. Su macOS:\n" + " Impostazioni di Sistema -> Privacy e sicurezza -> Microfono\n" + " Abilita 'Terminal' (se lanci l'app da terminale) o 'MusicTools'\n" + " (se usi l'app installata). Poi riavvia l'app." + ) + + +def get_last_stderr(max_lines: int = 50) -> list[str]: + """Espone le ultime righe di stderr ai fini diagnostici. + Chiamato dal bridge per la UI 'Mostra log tecnici'.""" + with _stderr_lock: + return list(_stderr_buf[-max_lines:]) def stop_recording() -> dict: diff --git a/webui/css/style.css b/webui/css/style.css index 0975afa..52985b4 100644 --- a/webui/css/style.css +++ b/webui/css/style.css @@ -1410,3 +1410,38 @@ input[type="number"]::-webkit-inner-spin-button { gap: 10px; flex-wrap: wrap; } + +/* ============================================================ + REGISTRA: card errore con log tecnici + ============================================================ */ +.rec-error-card { + border-left: 4px solid var(--red, #f87171); +} + +.micro-label.red { + color: #ff8a92; + font-weight: 800; +} + +.rec-error-msg { + margin-top: 6px; + font-size: 14px; + color: var(--text); + white-space: pre-line; + line-height: 1.55; +} + +.rec-error-log { + margin-top: 12px; + background: #0d0d0d; + border: 1px solid var(--border); + border-radius: 10px; + padding: 14px; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 12px; + color: #c0c0c0; + max-height: 320px; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; +} diff --git a/webui/index.html b/webui/index.html index c7fd68d..d4536c7 100644 --- a/webui/index.html +++ b/webui/index.html @@ -343,6 +343,16 @@
Ultima registrazione
+ + diff --git a/webui/js/app.js b/webui/js/app.js index ac67164..440abed 100644 --- a/webui/js/app.js +++ b/webui/js/app.js @@ -222,6 +222,7 @@ const bridgeHandlers = { $("#recRefreshBtn").disabled = true; $("#recTimer").textContent = "00:00:00"; $("#recStatus").textContent = "● Registrazione in corso…"; + $("#recErrorCard").hidden = true; // pulisci eventuale errore precedente } else if (status === "tick") { state.rec.seconds = p.seconds || 0; $("#recTimer").textContent = formatHms(state.rec.seconds); @@ -247,7 +248,14 @@ const bridgeHandlers = { $("#recDevice").disabled = false; $("#recRefreshBtn").disabled = false; $("#recStatus").textContent = ""; - toast("Errore registrazione: " + (p.error || ""), "error"); + const msg = p.error || "Errore sconosciuto"; + $("#recErrorMsg").textContent = msg; + $("#recErrorCard").hidden = false; + $("#recErrorLog").hidden = true; + $("#recErrorLog").textContent = ""; + $("#recCopyLogBtn").hidden = true; + $("#recShowLogBtn").textContent = "Mostra log tecnici"; + toast("Errore registrazione", "error"); } }, }; @@ -695,6 +703,32 @@ $("#recStopBtn").addEventListener("click", async () => { await window.pywebview.api.stop_audio_recording(); }); +$("#recShowLogBtn")?.addEventListener("click", async () => { + const pre = $("#recErrorLog"); + if (!pre.hidden) { + pre.hidden = true; + $("#recShowLogBtn").textContent = "Mostra log tecnici"; + $("#recCopyLogBtn").hidden = true; + return; + } + const res = await window.pywebview.api.get_recorder_log(); + const lines = (res && res.lines) || []; + pre.textContent = lines.length ? lines.join("\n") : "(log vuoto)"; + pre.hidden = false; + $("#recShowLogBtn").textContent = "Nascondi log tecnici"; + $("#recCopyLogBtn").hidden = false; +}); + +$("#recCopyLogBtn")?.addEventListener("click", async () => { + const text = $("#recErrorLog").textContent || ""; + try { + await navigator.clipboard.writeText(text); + toast("Log copiato negli appunti", "success"); + } catch { + toast("Impossibile copiare", "error"); + } +}); + // ============================================================ // METADATA editor // ============================================================