feat: implement goto previous/next chapter

This commit is contained in:
2025-12-14 09:28:05 +01:00
parent b2dd430ac9
commit d4e73e6a13

View File

@@ -18,22 +18,29 @@ from .playback import PlaybackController
if TYPE_CHECKING: if TYPE_CHECKING:
from textual.widgets._data_table import ColumnKey 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): class Auditui(App):
"""Main application class for the Audible TUI app.""" """Main application class for the Audible TUI app."""
BINDINGS = [ BINDINGS = [
("d", "toggle_dark", "Toggle dark mode"), ("d", "toggle_dark", "Dark mode"),
("s", "sort", "Sort by title"), ("s", "sort", "Sort"),
("r", "reverse_sort", "Reverse sort"), ("r", "reverse_sort", "Reverse"),
("p", "sort_by_progress", "Sort by progress"), ("p", "sort_by_progress", "Sort progress"),
("a", "show_all", "Show all books"), ("a", "show_all", "All books"),
("u", "show_unfinished", "Show unfinished"), ("u", "show_unfinished", "Unfinished"),
("enter", "play_selected", "Play selected book"), ("enter", "play_selected", "Play"),
("space", "toggle_playback", "Pause/Resume"), ("space", "toggle_playback", "Pause/Resume"),
("left", "seek_backward", "Seek -30s"), ("left", "seek_backward", "-30s"),
("right", "seek_forward", "Seek +30s"), ("right", "seek_forward", "+30s"),
("q", "quit", "Quit application"), ("ctrl+left", "previous_chapter", "Previous chapter"),
("ctrl+right", "next_chapter", "Next chapter"),
("q", "quit", "Quit"),
] ]
CSS = TABLE_CSS CSS = TABLE_CSS
@@ -54,7 +61,7 @@ class Auditui(App):
self.progress_sort_reverse = False self.progress_sort_reverse = False
self.title_column_key: ColumnKey | None = None self.title_column_key: ColumnKey | None = None
self.progress_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: def compose(self) -> ComposeResult:
yield Header() yield Header()
@@ -79,7 +86,8 @@ class Auditui(App):
self.update_status("Fetching library...") self.update_status("Fetching library...")
self.fetch_library() self.fetch_library()
else: 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(1.0, self._check_playback_status)
self.set_interval(0.5, self._update_progress) self.set_interval(0.5, self._update_progress)
@@ -89,7 +97,25 @@ class Auditui(App):
self.playback.stop() self.playback.stop()
def on_key(self, event: Key) -> None: 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 isinstance(self.focused, DataTable):
if event.key == "enter": if event.key == "enter":
event.prevent_default() event.prevent_default()
@@ -97,12 +123,6 @@ class Auditui(App):
elif event.key == "space": elif event.key == "space":
event.prevent_default() event.prevent_default()
self.action_toggle_playback() 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: def update_status(self, message: str) -> None:
"""Update the status message in the UI.""" """Update the status message in the UI."""
@@ -127,7 +147,7 @@ class Auditui(App):
except (OSError, ValueError, KeyError) as exc: except (OSError, ValueError, KeyError) as exc:
self.call_from_thread(self.on_library_error, str(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.""" """Handle successful library load."""
self.all_items = items self.all_items = items
self.update_status(f"Loaded {len(items)} books") self.update_status(f"Loaded {len(items)} books")
@@ -137,7 +157,7 @@ class Auditui(App):
"""Handle library fetch error.""" """Handle library fetch error."""
self.update_status(f"Error fetching library: {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.""" """Populate the DataTable with library items."""
table = self.query_one(DataTable) table = self.query_one(DataTable)
table.clear() table.clear()
@@ -149,14 +169,18 @@ class Auditui(App):
for item in items: for item in items:
title = self.library_client.extract_title(item) title = self.library_client.extract_title(item)
author_names = self.library_client.extract_authors(item) author_names = self.library_client.extract_authors(item)
if author_names and len(author_names) > 40: if author_names and len(author_names) > AUTHOR_NAME_MAX_LENGTH:
author_names = f"{author_names[:37]}..." author_names = f"{author_names[:AUTHOR_NAME_DISPLAY_LENGTH]}..."
minutes = self.library_client.extract_runtime_minutes(item) minutes = self.library_client.extract_runtime_minutes(item)
runtime_str = self.library_client.format_duration( runtime_str = self.library_client.format_duration(
minutes, unit="minutes", default_none="Unknown length" minutes, unit="minutes", default_none="Unknown length"
) )
percent_complete = self.library_client.extract_progress_info(item) 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( table.add_row(
title, title,
@@ -234,7 +258,8 @@ class Auditui(App):
def action_play_selected(self) -> None: def action_play_selected(self) -> None:
"""Start playing the selected book.""" """Start playing the selected book."""
if not self.download_manager: if not self.download_manager:
self.update_status("Not authenticated. Please restart and authenticate.") self.update_status(
"Not authenticated. Please restart and authenticate.")
return return
table = self.query_one(DataTable) table = self.query_one(DataTable)
@@ -247,8 +272,12 @@ class Auditui(App):
self.update_status("Invalid selection") self.update_status("Invalid selection")
return return
if not self.library_client:
self.update_status("Library client not available")
return
selected_item = self.current_items[cursor_row] 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: if not asin:
self.update_status("Could not get ASIN for selected book") self.update_status("Could not get ASIN for selected book")
@@ -263,12 +292,22 @@ class Auditui(App):
def action_seek_forward(self) -> None: def action_seek_forward(self) -> None:
"""Seek forward 30 seconds.""" """Seek forward 30 seconds."""
if not self.playback.seek_forward(30.0): if not self.playback.seek_forward(SEEK_SECONDS):
self._no_playback_message() self._no_playback_message()
def action_seek_backward(self) -> None: def action_seek_backward(self) -> None:
"""Seek backward 30 seconds.""" """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() self._no_playback_message()
def _no_playback_message(self) -> None: def _no_playback_message(self) -> None:
@@ -301,11 +340,13 @@ class Auditui(App):
progress_info = self.query_one("#progress_info", Static) progress_info = self.query_one("#progress_info", Static)
progress_bar = self.query_one("#progress_bar", ProgressBar) 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) progress_bar.update(progress=progress_percent)
chapter_elapsed_str = self._format_time(chapter_elapsed) chapter_elapsed_str = self._format_time(chapter_elapsed)
chapter_total_str = self._format_time(chapter_total) 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_info.display = True
progress_bar.display = True progress_bar.display = True