feat: add methods to mark books as finished/unfinished
This commit is contained in:
@@ -13,6 +13,7 @@ class LibraryClient:
|
|||||||
|
|
||||||
def __init__(self, client: audible.Client) -> None:
|
def __init__(self, client: audible.Client) -> None:
|
||||||
self.client = client
|
self.client = client
|
||||||
|
self._saved_positions: dict[str, float] = {}
|
||||||
|
|
||||||
def fetch_all_items(self, on_progress: ProgressCallback | None = None) -> list:
|
def fetch_all_items(self, on_progress: ProgressCallback | None = None) -> list:
|
||||||
"""Fetch all library items from the API."""
|
"""Fetch all library items from the API."""
|
||||||
@@ -175,9 +176,9 @@ class LibraryClient:
|
|||||||
except (OSError, ValueError, KeyError):
|
except (OSError, ValueError, KeyError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def save_last_position(self, asin: str, position_seconds: float) -> bool:
|
def _update_position(self, asin: str, position_seconds: float) -> bool:
|
||||||
"""Save the last playback position for a book."""
|
"""Update the playback position for a book."""
|
||||||
if position_seconds <= 0:
|
if position_seconds < 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
content_ref = self._get_content_reference(asin)
|
content_ref = self._get_content_reference(asin)
|
||||||
@@ -206,6 +207,12 @@ class LibraryClient:
|
|||||||
except (OSError, ValueError, KeyError):
|
except (OSError, ValueError, KeyError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def save_last_position(self, asin: str, position_seconds: float) -> bool:
|
||||||
|
"""Save the last playback position for a book."""
|
||||||
|
if position_seconds <= 0:
|
||||||
|
return False
|
||||||
|
return self._update_position(asin, position_seconds)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_duration(
|
def format_duration(
|
||||||
value: int | None, unit: str = "minutes", default_none: str | None = None
|
value: int | None, unit: str = "minutes", default_none: str | None = None
|
||||||
@@ -228,6 +235,62 @@ class LibraryClient:
|
|||||||
|
|
||||||
return " ".join(parts) if parts else default_none
|
return " ".join(parts) if parts else default_none
|
||||||
|
|
||||||
|
def _get_total_duration(self, asin: str, item: dict | None = None) -> float | None:
|
||||||
|
"""Get total duration in seconds, trying item data first, then API."""
|
||||||
|
if item:
|
||||||
|
duration = self._get_total_duration_from_item(item)
|
||||||
|
if duration:
|
||||||
|
return duration
|
||||||
|
return self._get_total_duration_from_api(asin)
|
||||||
|
|
||||||
|
def mark_as_finished(self, asin: str, item: dict | None = None) -> bool:
|
||||||
|
"""Mark a book as finished (100% complete) by setting position near end."""
|
||||||
|
current_position = self.get_last_position(asin)
|
||||||
|
if current_position and current_position > 0:
|
||||||
|
self._saved_positions[asin] = current_position
|
||||||
|
|
||||||
|
total_duration_seconds = self._get_total_duration(asin, item)
|
||||||
|
if total_duration_seconds and total_duration_seconds > 0:
|
||||||
|
position_seconds = max(0, total_duration_seconds - 10)
|
||||||
|
else:
|
||||||
|
position_seconds = 999999
|
||||||
|
|
||||||
|
return self._update_position(asin, position_seconds)
|
||||||
|
|
||||||
|
def _get_total_duration_from_api(self, asin: str) -> float | None:
|
||||||
|
"""Get total duration in seconds from API."""
|
||||||
|
try:
|
||||||
|
response = self.client.get(
|
||||||
|
path=f"1.0/content/{asin}/metadata",
|
||||||
|
response_groups="runtime",
|
||||||
|
)
|
||||||
|
content_metadata = response.get("content_metadata", {})
|
||||||
|
runtime = content_metadata.get("runtime", {})
|
||||||
|
if isinstance(runtime, dict):
|
||||||
|
runtime_ms = runtime.get("runtime_ms")
|
||||||
|
if runtime_ms:
|
||||||
|
return float(runtime_ms) / 1000.0
|
||||||
|
return None
|
||||||
|
except (OSError, ValueError, KeyError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def mark_as_unfinished(self, asin: str, item: dict | None = None) -> bool:
|
||||||
|
"""Mark a book as unfinished by restoring saved position."""
|
||||||
|
saved_position = self._saved_positions.pop(asin, None)
|
||||||
|
if saved_position is None:
|
||||||
|
saved_position = self.get_last_position(asin)
|
||||||
|
if saved_position is None or saved_position <= 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self._update_position(asin, saved_position)
|
||||||
|
|
||||||
|
def _get_total_duration_from_item(self, item: dict) -> float | None:
|
||||||
|
"""Get total duration in seconds from library item data."""
|
||||||
|
runtime_minutes = self.extract_runtime_minutes(item)
|
||||||
|
if runtime_minutes:
|
||||||
|
return float(runtime_minutes * 60)
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_time(seconds: float) -> str:
|
def format_time(seconds: float) -> str:
|
||||||
"""Format seconds as HH:MM:SS or MM:SS."""
|
"""Format seconds as HH:MM:SS or MM:SS."""
|
||||||
|
|||||||
Reference in New Issue
Block a user