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 // ============================================================