Compare commits

...

11 Commits

Author SHA1 Message Date
c6edfa5572 style: align help list rows 2026-01-19 15:25:13 +01:00
ac99643dbc style: densify help modal layout 2026-01-19 15:25:06 +01:00
889ac62a9a refactor: tidy modal styles 2026-01-19 15:15:26 +01:00
0bf6db7980 chore: bump version to 0.1.4 2026-01-19 14:41:23 +01:00
6aa4ebb33f chore: update lockfile version 2026-01-19 14:41:15 +01:00
ca43ea8858 chore: bump version to 0.1.4 2026-01-19 14:41:09 +01:00
733e35b0d2 fix: constrain progress bar width 2026-01-19 14:41:01 +01:00
f3573dfffc fix: center progress bar container 2026-01-19 14:40:50 +01:00
d17cb6f4d2 chore: bump version to 0.1.3 2026-01-09 19:01:29 +01:00
6e3eb87f76 chore: update pyproject 2026-01-09 19:01:20 +01:00
b5f82d6e33 feat: speed up library fetching with concurrent page requests 2026-01-09 19:01:14 +01:00
7 changed files with 137 additions and 74 deletions

View File

@@ -1,3 +1,3 @@
"""Auditui package""" """Auditui package"""
__version__ = "0.1.2" __version__ = "0.1.4"

View File

@@ -98,6 +98,7 @@ class Auditui(App):
table.cursor_type = "row" table.cursor_type = "row"
yield table yield table
yield Static("", id="progress_info") yield Static("", id="progress_info")
with Horizontal(id="progress_bar_container"):
yield ProgressBar(id="progress_bar", show_eta=False, show_percentage=False, total=100) yield ProgressBar(id="progress_bar", show_eta=False, show_percentage=False, total=100)
def on_mount(self) -> None: def on_mount(self) -> None:
@@ -525,6 +526,8 @@ 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_bar_container = self.query_one(
"#progress_bar_container", Horizontal)
progress_percent = min(100.0, max( progress_percent = min(100.0, max(
0.0, (chapter_elapsed / chapter_total) * 100.0)) 0.0, (chapter_elapsed / chapter_total) * 100.0))
@@ -534,14 +537,15 @@ class Auditui(App):
progress_info.update( progress_info.update(
f"{chapter_name} | {chapter_elapsed_str} / {chapter_total_str}") f"{chapter_name} | {chapter_elapsed_str} / {chapter_total_str}")
progress_info.display = True progress_info.display = True
progress_bar.display = True progress_bar_container.display = True
def _hide_progress(self) -> None: def _hide_progress(self) -> None:
"""Hide the progress widget.""" """Hide the progress widget."""
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_container = self.query_one(
"#progress_bar_container", Horizontal)
progress_info.display = False progress_info.display = False
progress_bar.display = False progress_bar_container.display = False
def _save_position_periodically(self) -> None: def _save_position_periodically(self) -> None:
"""Periodically save playback position.""" """Periodically save playback position."""

View File

@@ -61,8 +61,8 @@ Screen {
DataTable { DataTable {
height: 1fr; height: 1fr;
background: #141622; background: #141622;
color: #d6dbf2; color: #c7cfe8;
border: solid #2b2f45; border: solid #262a3f;
scrollbar-size-horizontal: 0; scrollbar-size-horizontal: 0;
} }
@@ -105,6 +105,14 @@ Static#progress_info {
text-style: bold; text-style: bold;
margin: 0; margin: 0;
padding: 0; padding: 0;
text-align: center;
width: 100%;
}
#progress_bar_container {
align: center middle;
width: 100%;
height: 1;
} }
ProgressBar#progress_bar { ProgressBar#progress_bar {
@@ -112,8 +120,10 @@ ProgressBar#progress_bar {
background: #10131f; background: #10131f;
border: none; border: none;
margin: 0; margin: 0;
padding: 0 1; padding: 0;
align: center middle; width: auto;
min-width: 40;
max-width: 80;
} }
ProgressBar#progress_bar > .progress-bar--track { ProgressBar#progress_bar > .progress-bar--track {
@@ -124,21 +134,16 @@ ProgressBar#progress_bar > .progress-bar--bar {
background: #8bd5ca; background: #8bd5ca;
} }
HelpScreen { HelpScreen,
StatsScreen,
FilterScreen {
align: center middle; align: center middle;
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0.7);
} }
HelpScreen Static { HelpScreen Static,
background: transparent; StatsScreen Static,
} FilterScreen Static {
StatsScreen {
align: center middle;
background: rgba(0, 0, 0, 0.7);
}
StatsScreen Static {
background: transparent; background: transparent;
} }
@@ -173,25 +178,25 @@ StatsScreen .help_list > ListItem > Label {
} }
#help_container { #help_container {
width: 88%; width: 72%;
max-width: 120; max-width: 90;
min-width: 48; min-width: 44;
height: auto; height: auto;
max-height: 80%; max-height: 80%;
min-height: 14; min-height: 14;
background: #181a2a; background: #181a2a;
border: heavy #7aa2f7; border: heavy #7aa2f7;
padding: 1 2; padding: 1 1;
} }
#help_title { #help_title {
width: 100%; width: 100%;
height: 3; height: 2;
text-align: center; text-align: center;
text-style: bold; text-style: bold;
color: #7aa2f7; color: #7aa2f7;
content-align: center middle; content-align: center middle;
margin-bottom: 1; margin-bottom: 0;
border-bottom: solid #4b5165; border-bottom: solid #4b5165;
} }
@@ -200,19 +205,21 @@ StatsScreen .help_list > ListItem > Label {
height: auto; height: auto;
padding: 0; padding: 0;
margin: 0 0 1 0; margin: 0 0 1 0;
align: center middle;
} }
.help_list { .help_list {
width: 1fr; width: 100%;
height: auto; height: auto;
background: transparent; background: transparent;
padding: 0 1; padding: 0;
scrollbar-size: 0 0; scrollbar-size: 0 0;
} }
.help_list > ListItem { .help_list > ListItem {
background: #1b1f33; background: #1b1f33;
padding: 0 1; padding: 0 1;
height: 1;
} }
.help_list > ListItem:hover { .help_list > ListItem:hover {
@@ -226,23 +233,14 @@ StatsScreen .help_list > ListItem > Label {
#help_footer { #help_footer {
width: 100%; width: 100%;
height: 3; height: 2;
text-align: center; text-align: center;
content-align: center middle; content-align: center middle;
color: #bac2de; color: #b6bfdc;
margin-top: 1; margin-top: 0;
border-top: solid #4b5165; border-top: solid #4b5165;
} }
FilterScreen {
align: center middle;
background: rgba(0, 0, 0, 0.7);
}
FilterScreen Static {
background: transparent;
}
#filter_container { #filter_container {
width: 60; width: 60;
height: auto; height: auto;
@@ -271,7 +269,7 @@ FilterScreen Static {
height: 2; height: 2;
text-align: center; text-align: center;
content-align: center middle; content-align: center middle;
color: #bac2de; color: #b6bfdc;
margin-top: 1; margin-top: 1;
} }
""" """

View File

@@ -1,5 +1,6 @@
"""Library helpers for fetching and formatting Audible data.""" """Library helpers for fetching and formatting Audible data."""
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Callable from typing import Callable
import audible import audible
@@ -18,38 +19,103 @@ class LibraryClient:
"""Fetch all library items from the API.""" """Fetch all library items from the API."""
response_groups = ( response_groups = (
"contributors,media,product_attrs,product_desc,product_details," "contributors,media,product_attrs,product_desc,product_details,"
"rating,is_finished,listening_status,percent_complete" "is_finished,listening_status,percent_complete"
) )
return self._fetch_all_pages(response_groups, on_progress) return self._fetch_all_pages(response_groups, on_progress)
def _fetch_all_pages( def _fetch_page(
self, response_groups: str, on_progress: ProgressCallback | None = None self, page: int, page_size: int, response_groups: str
) -> list: ) -> tuple[int, list[dict]]:
"""Fetch all pages of library items from the API.""" """Fetch a single page of library items."""
all_items: list[dict] = []
page = 1
page_size = 50
while True:
library = self.client.get( library = self.client.get(
path="library", path="library",
num_results=page_size, num_results=page_size,
page=page, page=page,
response_groups=response_groups, response_groups=response_groups,
) )
items = library.get("items", [])
return page, list(items)
items = list(library.get("items", [])) def _fetch_all_pages(
if not items: self, response_groups: str, on_progress: ProgressCallback | None = None
) -> list:
"""Fetch all pages of library items from the API using maximum parallel fetching."""
library_response = None
page_size = 200
for attempt_size in [200, 100, 50]:
try:
library_response = self.client.get(
path="library",
num_results=attempt_size,
page=1,
response_groups=response_groups,
)
page_size = attempt_size
break break
except Exception:
continue
all_items.extend(items) if not library_response:
return []
first_page_items = library_response.get("items", [])
if not first_page_items:
return []
all_items: list[dict] = list(first_page_items)
if on_progress: if on_progress:
on_progress(f"Fetched page {page} ({len(items)} items)...") on_progress(f"Fetched page 1 ({len(first_page_items)} items)...")
if len(items) < page_size: if len(first_page_items) < page_size:
return all_items
total_items_estimate = library_response.get(
"total_results") or library_response.get("total")
if total_items_estimate:
estimated_pages = (total_items_estimate +
page_size - 1) // page_size
estimated_pages = min(estimated_pages, 1000)
else:
estimated_pages = 500
max_workers = 50
page_results: dict[int, list[dict]] = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_page: dict = {}
for page in range(2, estimated_pages + 1):
future = executor.submit(
self._fetch_page, page, page_size, response_groups
)
future_to_page[future] = page
completed_count = 0
total_items = len(first_page_items)
for future in as_completed(future_to_page):
page_num = future_to_page.pop(future)
try:
fetched_page, items = future.result()
if not items or len(items) < page_size:
for remaining_future in list(future_to_page.keys()):
remaining_future.cancel()
break break
page += 1 page_results[fetched_page] = items
total_items += len(items)
completed_count += 1
if on_progress and completed_count % 20 == 0:
on_progress(
f"Fetched {completed_count} pages ({total_items} items)..."
)
except Exception:
pass
for page_num in sorted(page_results.keys()):
all_items.extend(page_results[page_num])
return all_items return all_items

View File

@@ -5,7 +5,7 @@ from datetime import date, datetime
from typing import Any, Callable, Protocol, TYPE_CHECKING, cast from typing import Any, Callable, Protocol, TYPE_CHECKING, cast
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.containers import Container, Horizontal, Vertical from textual.containers import Container, Vertical
from textual.screen import ModalScreen from textual.screen import ModalScreen
from textual.timer import Timer from textual.timer import Timer
from textual.widgets import Input, Label, ListItem, ListView, Static from textual.widgets import Input, Label, ListItem, ListView, Static
@@ -69,23 +69,18 @@ class HelpScreen(AppContextMixin, ModalScreen):
"""Create a ListItem for a single binding.""" """Create a ListItem for a single binding."""
key, description = self._parse_binding(binding) key, description = self._parse_binding(binding)
key_display = self._format_key_display(key) key_display = self._format_key_display(key)
text = f"[bold {KEY_COLOR}]{key_display:>12}[/] [{DESC_COLOR}]{description}[/]" text = f"[bold {KEY_COLOR}]{key_display:>16}[/] [{DESC_COLOR}]{description:<25}[/]"
return ListItem(Label(text)) return ListItem(Label(text))
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
app = self._app() app = self._app()
bindings = list(app.BINDINGS) bindings = list(app.BINDINGS)
mid = (len(bindings) + 1) // 2
with Container(id="help_container"): with Container(id="help_container"):
yield Static("Key Bindings", id="help_title") yield Static("Keybindings", id="help_title")
with Horizontal(id="help_content"): with Vertical(id="help_content"):
yield ListView( yield ListView(
*[self._make_item(b) for b in bindings[:mid]], *[self._make_item(b) for b in bindings],
classes="help_list",
)
yield ListView(
*[self._make_item(b) for b in bindings[mid:]],
classes="help_list", classes="help_list",
) )
yield Static( yield Static(

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "auditui" name = "auditui"
version = "0.1.2" version = "0.1.4"
description = "An Audible TUI client" description = "An Audible TUI client"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10,<3.13" requires-python = ">=3.10,<3.13"

2
uv.lock generated
View File

@@ -35,7 +35,7 @@ wheels = [
[[package]] [[package]]
name = "auditui" name = "auditui"
version = "0.1.2" version = "0.1.4"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "audible" }, { name = "audible" },