Compare commits
3 Commits
e620ea8369
...
bcad61d78a
| Author | SHA1 | Date | |
|---|---|---|---|
| bcad61d78a | |||
| f9c4771ee4 | |||
| 964b888e4c |
40
README.md
40
README.md
@@ -24,25 +24,25 @@ Please also note that as of now, you need to have [ffmpeg](https://ffmpeg.org/)
|
|||||||
|
|
||||||
### Bindings
|
### Bindings
|
||||||
|
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
| ------------ | --------------------------- |
|
| ------------ | -------------------------- |
|
||||||
| `?` | Show help screen |
|
| `?` | Show help screen |
|
||||||
| `n` | Sort by name |
|
| `n` | Sort by name |
|
||||||
| `p` | Sort by progress |
|
| `p` | Sort by progress |
|
||||||
| `a` | Show all/unfinished |
|
| `a` | Show all/unfinished |
|
||||||
| `enter` | Play the selected book |
|
| `enter` | Play the selected book |
|
||||||
| `space` | Pause/resume the playback |
|
| `space` | Pause/resume the playback |
|
||||||
| `left` | Seek backward 30 seconds |
|
| `left` | Seek backward 30 seconds |
|
||||||
| `right` | Seek forward 30 seconds |
|
| `right` | Seek forward 30 seconds |
|
||||||
| `ctrl+left` | Go to the previous chapter |
|
| `ctrl+left` | Go to the previous chapter |
|
||||||
| `ctrl+right` | Go to the next chapter |
|
| `ctrl+right` | Go to the next chapter |
|
||||||
| `up` | Increase playback speed |
|
| `up` | Increase playback speed |
|
||||||
| `down` | Decrease playback speed |
|
| `down` | Decrease playback speed |
|
||||||
| `f` | Mark as finished/unfinished |
|
| `f` | Mark as finished |
|
||||||
| `d` | Download/delete from cache |
|
| `d` | Download/delete from cache |
|
||||||
| `s` | Show stats screen |
|
| `s` | Show stats screen |
|
||||||
| `/` | Filter library |
|
| `/` | Filter library |
|
||||||
| `q` | Quit the application |
|
| `q` | Quit the application |
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ Please also note that as of now, you need to have [ffmpeg](https://ffmpeg.org/)
|
|||||||
- [x] download/remove a book in the cache without having to play it
|
- [x] download/remove a book in the cache without having to play it
|
||||||
- [x] add a help screen with all the keybindings
|
- [x] add a help screen with all the keybindings
|
||||||
- [x] increase/decrease reading speed
|
- [x] increase/decrease reading speed
|
||||||
- [x] mark a book as finished or unfinished
|
- [x] mark a book as finished
|
||||||
- [x] make ui responsive
|
- [x] make ui responsive
|
||||||
- [x] get your stats in a separated pane
|
- [x] get your stats in a separated pane
|
||||||
- [x] search/filter within your library
|
- [x] search/filter within your library
|
||||||
|
|||||||
@@ -403,11 +403,12 @@ class Auditui(App):
|
|||||||
is_currently_finished = self.library_client.is_finished(selected_item)
|
is_currently_finished = self.library_client.is_finished(selected_item)
|
||||||
|
|
||||||
if is_currently_finished:
|
if is_currently_finished:
|
||||||
success = self.library_client.mark_as_unfinished(asin)
|
self.call_from_thread(self.update_status,
|
||||||
message = "Marked as unfinished" if success else "Failed to mark as unfinished"
|
"Already marked as finished")
|
||||||
else:
|
return
|
||||||
success = self.library_client.mark_as_finished(asin, selected_item)
|
|
||||||
message = "Marked as finished" if success else "Failed to mark as finished"
|
success = self.library_client.mark_as_finished(asin, selected_item)
|
||||||
|
message = "Marked as finished" if success else "Failed to mark as finished"
|
||||||
|
|
||||||
self.call_from_thread(self.update_status, message)
|
self.call_from_thread(self.update_status, message)
|
||||||
if success:
|
if success:
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ 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."""
|
||||||
@@ -231,61 +230,63 @@ class LibraryClient:
|
|||||||
return f"{hours}h{minutes:02d}" if minutes else f"{hours}h"
|
return f"{hours}h{minutes:02d}" if minutes else f"{hours}h"
|
||||||
return f"{minutes}m"
|
return f"{minutes}m"
|
||||||
|
|
||||||
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:
|
def mark_as_finished(self, asin: str, item: dict | None = None) -> bool:
|
||||||
"""Mark a book as finished (100% complete) by setting position near end."""
|
"""Mark a book as finished by setting position to the end."""
|
||||||
current_position = self.get_last_position(asin)
|
total_ms = self._get_runtime_ms(asin, item)
|
||||||
if current_position and current_position > 0:
|
if not total_ms:
|
||||||
self._saved_positions[asin] = current_position
|
return False
|
||||||
|
|
||||||
total_duration_seconds = self._get_total_duration(asin, item)
|
position_ms = total_ms
|
||||||
if total_duration_seconds and total_duration_seconds > 0:
|
acr = self._get_acr(asin)
|
||||||
position_seconds = max(0, total_duration_seconds - 10)
|
if not acr:
|
||||||
else:
|
return False
|
||||||
position_seconds = 999999
|
|
||||||
|
|
||||||
return self._update_position(asin, position_seconds)
|
try:
|
||||||
|
self.client.put(
|
||||||
|
path=f"1.0/lastpositions/{asin}",
|
||||||
|
body={"asin": asin, "acr": acr, "position_ms": position_ms},
|
||||||
|
)
|
||||||
|
if item:
|
||||||
|
item["is_finished"] = True
|
||||||
|
listening_status = item.get("listening_status", {})
|
||||||
|
if isinstance(listening_status, dict):
|
||||||
|
listening_status["is_finished"] = True
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_runtime_ms(self, asin: str, item: dict | None = None) -> int | None:
|
||||||
|
"""Get total runtime in milliseconds."""
|
||||||
|
if item:
|
||||||
|
runtime_min = self.extract_runtime_minutes(item)
|
||||||
|
if runtime_min:
|
||||||
|
return runtime_min * 60 * 1000
|
||||||
|
|
||||||
def _get_total_duration_from_api(self, asin: str) -> float | None:
|
|
||||||
"""Get total duration in seconds from API."""
|
|
||||||
try:
|
try:
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
path=f"1.0/content/{asin}/metadata",
|
path=f"1.0/content/{asin}/metadata",
|
||||||
response_groups="runtime",
|
response_groups="chapter_info",
|
||||||
)
|
)
|
||||||
content_metadata = response.get("content_metadata", {})
|
chapter_info = response.get(
|
||||||
runtime = content_metadata.get("runtime", {})
|
"content_metadata", {}).get("chapter_info", {})
|
||||||
if isinstance(runtime, dict):
|
return chapter_info.get("runtime_length_ms")
|
||||||
runtime_ms = runtime.get("runtime_ms")
|
except Exception:
|
||||||
if runtime_ms:
|
|
||||||
return float(runtime_ms) / 1000.0
|
|
||||||
return None
|
|
||||||
except (OSError, ValueError, KeyError):
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def mark_as_unfinished(self, asin: str) -> bool:
|
def _get_acr(self, asin: str) -> str | None:
|
||||||
"""Mark a book as unfinished by restoring saved position."""
|
"""Get ACR token needed for position updates."""
|
||||||
saved_position = self._saved_positions.pop(asin, None)
|
try:
|
||||||
if saved_position is None:
|
response = self.client.post(
|
||||||
saved_position = self.get_last_position(asin)
|
path=f"1.0/content/{asin}/licenserequest",
|
||||||
if saved_position is None or saved_position <= 0:
|
body={
|
||||||
return False
|
"response_groups": "content_reference",
|
||||||
|
"consumption_type": "Download",
|
||||||
return self._update_position(asin, saved_position)
|
"drm_type": "Adrm",
|
||||||
|
},
|
||||||
def _get_total_duration_from_item(self, item: dict) -> float | None:
|
)
|
||||||
"""Get total duration in seconds from library item data."""
|
return response.get("content_license", {}).get("acr")
|
||||||
runtime_minutes = self.extract_runtime_minutes(item)
|
except Exception:
|
||||||
if runtime_minutes:
|
return None
|
||||||
return float(runtime_minutes * 60)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_time(seconds: float) -> str:
|
def format_time(seconds: float) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user