Compare commits
2 Commits
ce0d313187
...
d4e73e6a13
| Author | SHA1 | Date | |
|---|---|---|---|
| d4e73e6a13 | |||
| b2dd430ac9 |
101
auditui/app.py
101
auditui/app.py
@@ -18,22 +18,29 @@ from .playback import PlaybackController
|
||||
if TYPE_CHECKING:
|
||||
from textual.widgets._data_table import ColumnKey
|
||||
|
||||
AUTHOR_NAME_MAX_LENGTH = 40
|
||||
AUTHOR_NAME_DISPLAY_LENGTH = 37
|
||||
PROGRESS_COLUMN_INDEX = 3
|
||||
SEEK_SECONDS = 30.0
|
||||
|
||||
|
||||
class Auditui(App):
|
||||
"""Main application class for the Audible TUI app."""
|
||||
|
||||
BINDINGS = [
|
||||
("d", "toggle_dark", "Toggle dark mode"),
|
||||
("s", "sort", "Sort by title"),
|
||||
("r", "reverse_sort", "Reverse sort"),
|
||||
("p", "sort_by_progress", "Sort by progress"),
|
||||
("a", "show_all", "Show all books"),
|
||||
("u", "show_unfinished", "Show unfinished"),
|
||||
("enter", "play_selected", "Play selected book"),
|
||||
("d", "toggle_dark", "Dark mode"),
|
||||
("s", "sort", "Sort"),
|
||||
("r", "reverse_sort", "Reverse"),
|
||||
("p", "sort_by_progress", "Sort progress"),
|
||||
("a", "show_all", "All books"),
|
||||
("u", "show_unfinished", "Unfinished"),
|
||||
("enter", "play_selected", "Play"),
|
||||
("space", "toggle_playback", "Pause/Resume"),
|
||||
("left", "seek_backward", "Seek -30s"),
|
||||
("right", "seek_forward", "Seek +30s"),
|
||||
("q", "quit", "Quit application"),
|
||||
("left", "seek_backward", "-30s"),
|
||||
("right", "seek_forward", "+30s"),
|
||||
("ctrl+left", "previous_chapter", "Previous chapter"),
|
||||
("ctrl+right", "next_chapter", "Next chapter"),
|
||||
("q", "quit", "Quit"),
|
||||
]
|
||||
|
||||
CSS = TABLE_CSS
|
||||
@@ -54,7 +61,7 @@ class Auditui(App):
|
||||
self.progress_sort_reverse = False
|
||||
self.title_column_key: ColumnKey | None = None
|
||||
self.progress_column_key: ColumnKey | None = None
|
||||
self.progress_column_index = 3
|
||||
self.progress_column_index = PROGRESS_COLUMN_INDEX
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
@@ -79,7 +86,8 @@ class Auditui(App):
|
||||
self.update_status("Fetching library...")
|
||||
self.fetch_library()
|
||||
else:
|
||||
self.update_status("Not authenticated. Please restart and authenticate.")
|
||||
self.update_status(
|
||||
"Not authenticated. Please restart and authenticate.")
|
||||
|
||||
self.set_interval(1.0, self._check_playback_status)
|
||||
self.set_interval(0.5, self._update_progress)
|
||||
@@ -89,7 +97,25 @@ class Auditui(App):
|
||||
self.playback.stop()
|
||||
|
||||
def on_key(self, event: Key) -> None:
|
||||
"""Handle key presses on DataTable."""
|
||||
"""Handle key presses."""
|
||||
if self.playback.is_playing:
|
||||
if event.key == "ctrl+left":
|
||||
event.prevent_default()
|
||||
self.action_previous_chapter()
|
||||
return
|
||||
elif event.key == "ctrl+right":
|
||||
event.prevent_default()
|
||||
self.action_next_chapter()
|
||||
return
|
||||
elif event.key == "left":
|
||||
event.prevent_default()
|
||||
self.action_seek_backward()
|
||||
return
|
||||
elif event.key == "right":
|
||||
event.prevent_default()
|
||||
self.action_seek_forward()
|
||||
return
|
||||
|
||||
if isinstance(self.focused, DataTable):
|
||||
if event.key == "enter":
|
||||
event.prevent_default()
|
||||
@@ -97,12 +123,6 @@ class Auditui(App):
|
||||
elif event.key == "space":
|
||||
event.prevent_default()
|
||||
self.action_toggle_playback()
|
||||
elif event.key == "left" and self.playback.is_playing:
|
||||
event.prevent_default()
|
||||
self.action_seek_backward()
|
||||
elif event.key == "right" and self.playback.is_playing:
|
||||
event.prevent_default()
|
||||
self.action_seek_forward()
|
||||
|
||||
def update_status(self, message: str) -> None:
|
||||
"""Update the status message in the UI."""
|
||||
@@ -127,7 +147,7 @@ class Auditui(App):
|
||||
except (OSError, ValueError, KeyError) as exc:
|
||||
self.call_from_thread(self.on_library_error, str(exc))
|
||||
|
||||
def on_library_loaded(self, items: list) -> None:
|
||||
def on_library_loaded(self, items: list[dict]) -> None:
|
||||
"""Handle successful library load."""
|
||||
self.all_items = items
|
||||
self.update_status(f"Loaded {len(items)} books")
|
||||
@@ -137,7 +157,7 @@ class Auditui(App):
|
||||
"""Handle library fetch error."""
|
||||
self.update_status(f"Error fetching library: {error}")
|
||||
|
||||
def _populate_table(self, items: list) -> None:
|
||||
def _populate_table(self, items: list[dict]) -> None:
|
||||
"""Populate the DataTable with library items."""
|
||||
table = self.query_one(DataTable)
|
||||
table.clear()
|
||||
@@ -149,14 +169,18 @@ class Auditui(App):
|
||||
for item in items:
|
||||
title = self.library_client.extract_title(item)
|
||||
author_names = self.library_client.extract_authors(item)
|
||||
if author_names and len(author_names) > 40:
|
||||
author_names = f"{author_names[:37]}..."
|
||||
if author_names and len(author_names) > AUTHOR_NAME_MAX_LENGTH:
|
||||
author_names = f"{author_names[:AUTHOR_NAME_DISPLAY_LENGTH]}..."
|
||||
minutes = self.library_client.extract_runtime_minutes(item)
|
||||
runtime_str = self.library_client.format_duration(
|
||||
minutes, unit="minutes", default_none="Unknown length"
|
||||
)
|
||||
percent_complete = self.library_client.extract_progress_info(item)
|
||||
progress_str = f"{percent_complete:.1f}%" if percent_complete and percent_complete > 0 else "0%"
|
||||
progress_str = (
|
||||
f"{percent_complete:.1f}%"
|
||||
if percent_complete and percent_complete > 0
|
||||
else "0%"
|
||||
)
|
||||
|
||||
table.add_row(
|
||||
title,
|
||||
@@ -234,7 +258,8 @@ class Auditui(App):
|
||||
def action_play_selected(self) -> None:
|
||||
"""Start playing the selected book."""
|
||||
if not self.download_manager:
|
||||
self.update_status("Not authenticated. Please restart and authenticate.")
|
||||
self.update_status(
|
||||
"Not authenticated. Please restart and authenticate.")
|
||||
return
|
||||
|
||||
table = self.query_one(DataTable)
|
||||
@@ -247,8 +272,12 @@ class Auditui(App):
|
||||
self.update_status("Invalid selection")
|
||||
return
|
||||
|
||||
if not self.library_client:
|
||||
self.update_status("Library client not available")
|
||||
return
|
||||
|
||||
selected_item = self.current_items[cursor_row]
|
||||
asin = self.library_client.extract_asin(selected_item) if self.library_client else None
|
||||
asin = self.library_client.extract_asin(selected_item)
|
||||
|
||||
if not asin:
|
||||
self.update_status("Could not get ASIN for selected book")
|
||||
@@ -263,12 +292,22 @@ class Auditui(App):
|
||||
|
||||
def action_seek_forward(self) -> None:
|
||||
"""Seek forward 30 seconds."""
|
||||
if not self.playback.seek_forward(30.0):
|
||||
if not self.playback.seek_forward(SEEK_SECONDS):
|
||||
self._no_playback_message()
|
||||
|
||||
def action_seek_backward(self) -> None:
|
||||
"""Seek backward 30 seconds."""
|
||||
if not self.playback.seek_backward(30.0):
|
||||
if not self.playback.seek_backward(SEEK_SECONDS):
|
||||
self._no_playback_message()
|
||||
|
||||
def action_next_chapter(self) -> None:
|
||||
"""Seek to the next chapter."""
|
||||
if not self.playback.seek_to_next_chapter():
|
||||
self._no_playback_message()
|
||||
|
||||
def action_previous_chapter(self) -> None:
|
||||
"""Seek to the previous chapter."""
|
||||
if not self.playback.seek_to_previous_chapter():
|
||||
self._no_playback_message()
|
||||
|
||||
def _no_playback_message(self) -> None:
|
||||
@@ -301,11 +340,13 @@ class Auditui(App):
|
||||
progress_info = self.query_one("#progress_info", Static)
|
||||
progress_bar = self.query_one("#progress_bar", ProgressBar)
|
||||
|
||||
progress_percent = min(100.0, max(0.0, (chapter_elapsed / chapter_total) * 100.0))
|
||||
progress_percent = min(100.0, max(
|
||||
0.0, (chapter_elapsed / chapter_total) * 100.0))
|
||||
progress_bar.update(progress=progress_percent)
|
||||
chapter_elapsed_str = self._format_time(chapter_elapsed)
|
||||
chapter_total_str = self._format_time(chapter_total)
|
||||
progress_info.update(f"{chapter_name} | {chapter_elapsed_str} / {chapter_total_str}")
|
||||
progress_info.update(
|
||||
f"{chapter_name} | {chapter_elapsed_str} / {chapter_total_str}")
|
||||
progress_info.display = True
|
||||
progress_bar.display = True
|
||||
|
||||
|
||||
@@ -74,7 +74,8 @@ class PlaybackController:
|
||||
notify("Reached end of file")
|
||||
self._reset_state()
|
||||
return False
|
||||
notify(f"Playback process exited immediately (code: {return_code})")
|
||||
notify(
|
||||
f"Playback process exited immediately (code: {return_code})")
|
||||
self.playback_process = None
|
||||
return False
|
||||
|
||||
@@ -380,7 +381,8 @@ class PlaybackController:
|
||||
|
||||
elapsed = self._get_current_elapsed()
|
||||
total_elapsed = self.seek_offset + elapsed
|
||||
chapter_name, chapter_elapsed, chapter_total = self._get_current_chapter(total_elapsed)
|
||||
chapter_name, chapter_elapsed, chapter_total = self._get_current_chapter(
|
||||
total_elapsed)
|
||||
return (chapter_name, chapter_elapsed, chapter_total)
|
||||
|
||||
def _get_current_chapter(self, elapsed: float) -> tuple[str, float, float]:
|
||||
@@ -398,3 +400,78 @@ class PlaybackController:
|
||||
chapter_elapsed = max(0.0, elapsed - last_chapter["start_time"])
|
||||
chapter_total = last_chapter["end_time"] - last_chapter["start_time"]
|
||||
return (last_chapter["title"], chapter_elapsed, chapter_total)
|
||||
|
||||
def _get_current_chapter_index(self, elapsed: float) -> int | None:
|
||||
"""Get the index of the current chapter based on elapsed time."""
|
||||
if not self.chapters:
|
||||
return None
|
||||
|
||||
for idx, chapter in enumerate(self.chapters):
|
||||
if chapter["start_time"] <= elapsed < chapter["end_time"]:
|
||||
return idx
|
||||
|
||||
return len(self.chapters) - 1
|
||||
|
||||
def seek_to_chapter(self, direction: str) -> bool:
|
||||
"""Seek to next or previous chapter."""
|
||||
if not self.is_playing or not self.current_file_path:
|
||||
return False
|
||||
|
||||
if not self.chapters:
|
||||
self.notify("No chapter information available")
|
||||
return False
|
||||
|
||||
elapsed = self._get_current_elapsed()
|
||||
current_total_position = self.seek_offset + elapsed
|
||||
current_chapter_idx = self._get_current_chapter_index(
|
||||
current_total_position)
|
||||
|
||||
if current_chapter_idx is None:
|
||||
self.notify("Could not determine current chapter")
|
||||
return False
|
||||
|
||||
if direction == "next":
|
||||
if current_chapter_idx >= len(self.chapters) - 1:
|
||||
self.notify("Already at last chapter")
|
||||
return False
|
||||
target_chapter = self.chapters[current_chapter_idx + 1]
|
||||
new_position = target_chapter["start_time"]
|
||||
message = f"Next chapter: {target_chapter['title']}"
|
||||
else:
|
||||
if current_chapter_idx <= 0:
|
||||
self.notify("Already at first chapter")
|
||||
return False
|
||||
target_chapter = self.chapters[current_chapter_idx - 1]
|
||||
new_position = target_chapter["start_time"]
|
||||
message = f"Previous chapter: {target_chapter['title']}"
|
||||
|
||||
was_paused = self.is_paused
|
||||
saved_state = {
|
||||
"file_path": self.current_file_path,
|
||||
"asin": self.current_asin,
|
||||
"activation": self.activation_hex,
|
||||
"duration": self.total_duration,
|
||||
"chapters": self.chapters.copy(),
|
||||
}
|
||||
|
||||
self._stop_process()
|
||||
time.sleep(0.2)
|
||||
|
||||
if self.start(saved_state["file_path"], saved_state["activation"], self.notify, new_position):
|
||||
self.current_asin = saved_state["asin"]
|
||||
self.total_duration = saved_state["duration"]
|
||||
self.chapters = saved_state["chapters"]
|
||||
if was_paused:
|
||||
time.sleep(0.3)
|
||||
self.pause()
|
||||
self.notify(message)
|
||||
return True
|
||||
return False
|
||||
|
||||
def seek_to_next_chapter(self) -> bool:
|
||||
"""Seek to the next chapter. Returns True if action was taken."""
|
||||
return self.seek_to_chapter("next")
|
||||
|
||||
def seek_to_previous_chapter(self) -> bool:
|
||||
"""Seek to the previous chapter. Returns True if action was taken."""
|
||||
return self.seek_to_chapter("previous")
|
||||
|
||||
Reference in New Issue
Block a user