aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-24 03:22:29 -0400
committerhistoria <historiavg@proton.me>2026-08-24 03:22:29 -0400
commit471798cf5e967b2d1bceb02d12a47fe9ad1cbed1 (patch)
treeb974708bb60424fa5d39825e54813ed133bccc42
parent8e54a42db2690e0ac7f09fd400b29ad2fb2c9c5a (diff)
downloadtts-audiobook-generator-471798cf5e967b2d1bceb02d12a47fe9ad1cbed1.tar.gz
feat: tui ffmpeg check
-rw-r--r--README.md2
-rw-r--r--app/tests/test_hub.py31
-rw-r--r--app/tests/test_tui.py19
-rw-r--r--app/ui/hub.py7
-rw-r--r--app/ui/tui.py14
5 files changed, 70 insertions, 3 deletions
diff --git a/README.md b/README.md
index 5b020ab..cf6d4a1 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ The converter sends text extracted from your books to a locally running TTS serv
- Python 3.12+
- ffmpeg
-
+## Quick Start
```bash
git clone https://git.historia.vg/git/tts-audiobook-generator
diff --git a/app/tests/test_hub.py b/app/tests/test_hub.py
index b3a17e8..749d2e6 100644
--- a/app/tests/test_hub.py
+++ b/app/tests/test_hub.py
@@ -160,6 +160,37 @@ class HubMenuTests(unittest.TestCase):
["Convert books...", "Set up a backend...",
"Configure a backend...", "Server...", "Settings...", "Quit"])
+ def test_ffmpeg_warning_shown_when_missing(self):
+ # ffmpeg not on PATH → a red notice is passed above the table.
+ captured = {}
+
+ def fake_menu(stdscr, title, options, **kwargs):
+ captured["notice_lines"] = kwargs.get("notice_lines")
+ return "quit"
+
+ screen = FakeScreen()
+ with patch.object(hub.tui, "menu", fake_menu), \
+ patch.object(hub, "detect_all", return_value=[]), \
+ patch.object(hub.shutil, "which", return_value=None):
+ hub._hub_menu(screen)
+ self.assertEqual(captured["notice_lines"],
+ [("Warning: ffmpeg not installed!", "err")])
+
+ def test_ffmpeg_warning_hidden_when_installed(self):
+ # ffmpeg on PATH → no notice is passed at all.
+ captured = {}
+
+ def fake_menu(stdscr, title, options, **kwargs):
+ captured["notice_lines"] = kwargs.get("notice_lines")
+ return "quit"
+
+ screen = FakeScreen()
+ with patch.object(hub.tui, "menu", fake_menu), \
+ patch.object(hub, "detect_all", return_value=[]), \
+ patch.object(hub.shutil, "which", return_value="/usr/bin/ffmpeg"):
+ hub._hub_menu(screen)
+ self.assertIsNone(captured["notice_lines"])
+
def test_convert_with_no_available_backend_offers_setup(self):
# One installed-but-not-ready backend → Convert is offered. The
# convert menu lists no available backend, so only "Set up a
diff --git a/app/tests/test_tui.py b/app/tests/test_tui.py
index c121d55..c87374f 100644
--- a/app/tests/test_tui.py
+++ b/app/tests/test_tui.py
@@ -300,6 +300,25 @@ class MenuTableTests(TuiTestCase):
table_title="Backend status", table_rows=self.ROWS)
self.assert_inside_border(screen)
+ def test_notice_line_is_red_and_above_the_table(self):
+ screen = FakeScreen(keys=[10])
+ tui.menu(screen, "Hub", [("Quit", "quit")],
+ table_title="Backend status", table_rows=self.ROWS,
+ notice_lines=[("Warning: ffmpeg not installed!", "err")])
+ x0, _ = self.dialog_box(screen)
+ margin = x0 + 1 + tui.Frame.LIST_MARGIN
+ x, attr = next((x, a) for _, x, text, a in screen.strings
+ if text == "Warning: ffmpeg not installed!")
+ self.assertEqual(x, margin)
+ self.assertEqual(attr, tui._THEME["err"])
+ # The notice sits above the table title ("Backend status").
+ notice_y = next(y for y, _, text, _ in screen.strings
+ if text == "Warning: ffmpeg not installed!")
+ title_y = next(y for y, _, text, _ in screen.strings
+ if text == "Backend status")
+ self.assertLess(notice_y, title_y)
+ self.assert_inside_border(screen)
+
class ConfirmTests(TuiTestCase):
def test_tab_switches_and_enter_activates(self):
diff --git a/app/ui/hub.py b/app/ui/hub.py
index 6a94096..5b18869 100644
--- a/app/ui/hub.py
+++ b/app/ui/hub.py
@@ -14,6 +14,7 @@ main menu.
import json
import re
+import shutil
from pathlib import Path
from typing import Optional, Tuple
@@ -84,9 +85,13 @@ def _hub_menu(stdscr) -> Optional[tuple]:
options.append(("Settings...", "settings"))
options.append(("Quit", "quit"))
rows = [(st.label, *_status_mark(st)) for st in statuses]
+ notice_lines = None
+ if shutil.which("ffmpeg") is None:
+ notice_lines = [("Warning: ffmpeg not installed!", "err")]
choice = tui.menu(
stdscr, "tts-audiobook-generator", options,
- table_title="Backend status", table_rows=rows)
+ table_title="Backend status", table_rows=rows,
+ notice_lines=notice_lines)
if choice is None or choice == "quit":
return None
if choice == "convert":
diff --git a/app/ui/tui.py b/app/ui/tui.py
index 9047f36..c3d79f6 100644
--- a/app/ui/tui.py
+++ b/app/ui/tui.py
@@ -611,7 +611,8 @@ def menu(scr, title: str, options: Sequence[tuple], default_index: int = 0,
help_lines: Optional[Sequence[str]] = None,
back_value: object = None,
table_title: Optional[str] = None,
- table_rows: Optional[Sequence[tuple]] = None):
+ table_rows: Optional[Sequence[tuple]] = None,
+ notice_lines: Optional[Sequence[Tuple[str, str]]] = None):
"""Show OPTIONS as (label, value) pairs; return the chosen value.
The cursor starts on DEFAULT_INDEX; Enter returns the highlighted
@@ -628,6 +629,11 @@ def menu(scr, title: str, options: Sequence[tuple], default_index: int = 0,
hub to show each backend's state (unavailable / installed / running)
in matching columns with color.
+ NOTICE_LINES render above the table (and after HELP_LINES): each
+ entry is (text, kind) where KIND is a theme key, so the hub can warn
+ in red (e.g. "Warning: ffmpeg not installed!") without polluting the
+ status table.
+
Esc (or 'q') aborts the wizard unless BACK_VALUE is given (not None),
in which case Esc returns it so the caller can fall back a screen.
"""
@@ -642,6 +648,12 @@ def menu(scr, title: str, options: Sequence[tuple], default_index: int = 0,
frame.mark(line, frame.theme["dim"])
if help_lines:
frame.mark("")
+ if notice_lines:
+ for text, kind in notice_lines:
+ frame.mark(text,
+ frame.theme.get(kind, frame.theme["body"]),
+ align="left")
+ frame.mark("")
if table_rows:
if table_title:
frame.mark(table_title, frame.theme["dim"], align="left")