feat: print chapter and progress in the footer of the app while a book is playing
This commit is contained in:
@@ -2,10 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
@@ -24,9 +26,17 @@ class PlaybackController:
|
||||
self.is_paused = False
|
||||
self.current_file_path: Path | None = None
|
||||
self.current_asin: str | None = None
|
||||
self.playback_start_time: float | None = None
|
||||
self.paused_duration: float = 0.0
|
||||
self.pause_start_time: float | None = None
|
||||
self.total_duration: float | None = None
|
||||
self.chapters: list[dict] = []
|
||||
|
||||
def start(
|
||||
self, path: Path, activation_hex: str | None = None, status_callback: StatusCallback | None = None
|
||||
self,
|
||||
path: Path,
|
||||
activation_hex: str | None = None,
|
||||
status_callback: StatusCallback | None = None,
|
||||
) -> bool:
|
||||
"""Start playing a local file using ffplay."""
|
||||
notify = status_callback or self.notify
|
||||
@@ -59,6 +69,10 @@ class PlaybackController:
|
||||
self.is_playing = True
|
||||
self.is_paused = False
|
||||
self.current_file_path = path
|
||||
self.playback_start_time = time.time()
|
||||
self.paused_duration = 0.0
|
||||
self.pause_start_time = None
|
||||
self._load_media_info(path, activation_hex)
|
||||
notify(f"Playing: {path.name}")
|
||||
return True
|
||||
|
||||
@@ -89,6 +103,7 @@ class PlaybackController:
|
||||
if not self._validate_playback_state(require_paused=False):
|
||||
return
|
||||
|
||||
self.pause_start_time = time.time()
|
||||
self._send_signal(signal.SIGSTOP, "Paused", "pause")
|
||||
|
||||
def resume(self) -> None:
|
||||
@@ -96,6 +111,9 @@ class PlaybackController:
|
||||
if not self._validate_playback_state(require_paused=True):
|
||||
return
|
||||
|
||||
if self.pause_start_time is not None:
|
||||
self.paused_duration += time.time() - self.pause_start_time
|
||||
self.pause_start_time = None
|
||||
self._send_signal(signal.SIGCONT, "Playing", "resume")
|
||||
|
||||
def check_status(self) -> str | None:
|
||||
@@ -123,6 +141,11 @@ class PlaybackController:
|
||||
self.is_paused = False
|
||||
self.current_file_path = None
|
||||
self.current_asin = None
|
||||
self.playback_start_time = None
|
||||
self.paused_duration = 0.0
|
||||
self.pause_start_time = None
|
||||
self.total_duration = None
|
||||
self.chapters = []
|
||||
|
||||
def _validate_playback_state(self, require_paused: bool) -> bool:
|
||||
"""Validate playback state before pause/resume operations."""
|
||||
@@ -149,7 +172,7 @@ class PlaybackController:
|
||||
try:
|
||||
os.kill(self.playback_process.pid, sig)
|
||||
self.is_paused = sig == signal.SIGSTOP
|
||||
filename = self.current_file_path.name if self.current_file_path else ""
|
||||
filename = self.current_file_path.name if self.current_file_path else None
|
||||
message = f"{status_prefix}: {filename}" if filename else status_prefix
|
||||
self.notify(message)
|
||||
except ProcessLookupError:
|
||||
@@ -211,3 +234,74 @@ class PlaybackController:
|
||||
else:
|
||||
self.pause()
|
||||
return True
|
||||
|
||||
def _load_media_info(self, path: Path, activation_hex: str | None) -> None:
|
||||
"""Load media information including duration and chapters using ffprobe."""
|
||||
if not shutil.which("ffprobe"):
|
||||
return
|
||||
|
||||
try:
|
||||
cmd = ["ffprobe", "-v", "quiet", "-print_format",
|
||||
"json", "-show_format", "-show_chapters"]
|
||||
if activation_hex:
|
||||
cmd.extend(["-activation_bytes", activation_hex])
|
||||
cmd.append(str(path))
|
||||
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=10)
|
||||
if result.returncode != 0:
|
||||
return
|
||||
|
||||
data = json.loads(result.stdout)
|
||||
format_info = data.get("format", {})
|
||||
duration_str = format_info.get("duration")
|
||||
if duration_str:
|
||||
self.total_duration = float(duration_str)
|
||||
|
||||
chapters_data = data.get("chapters", [])
|
||||
self.chapters = [
|
||||
{
|
||||
"start_time": float(ch.get("start_time", 0)),
|
||||
"end_time": float(ch.get("end_time", 0)),
|
||||
"title": ch.get("tags", {}).get("title", f"Chapter {idx + 1}"),
|
||||
}
|
||||
for idx, ch in enumerate(chapters_data)
|
||||
]
|
||||
except (json.JSONDecodeError, subprocess.TimeoutExpired, ValueError, KeyError):
|
||||
pass
|
||||
|
||||
def get_current_progress(self) -> tuple[str, float, float] | None:
|
||||
"""Get current playback progress."""
|
||||
if not self.is_playing or self.playback_start_time is None:
|
||||
return None
|
||||
|
||||
current_time = time.time()
|
||||
if self.is_paused and self.pause_start_time is not None:
|
||||
elapsed = (self.pause_start_time -
|
||||
self.playback_start_time) - self.paused_duration
|
||||
else:
|
||||
if self.pause_start_time is not None:
|
||||
self.paused_duration += current_time - self.pause_start_time
|
||||
self.pause_start_time = None
|
||||
elapsed = max(
|
||||
0.0, (current_time - self.playback_start_time) - self.paused_duration)
|
||||
|
||||
chapter_name, chapter_elapsed, chapter_total = self._get_current_chapter(
|
||||
elapsed)
|
||||
return (chapter_name, chapter_elapsed, chapter_total)
|
||||
|
||||
def _get_current_chapter(self, elapsed: float) -> tuple[str, float, float]:
|
||||
"""Get current chapter info."""
|
||||
if not self.chapters:
|
||||
return ("Unknown Chapter", elapsed, self.total_duration or 0.0)
|
||||
|
||||
for chapter in self.chapters:
|
||||
if chapter["start_time"] <= elapsed < chapter["end_time"]:
|
||||
chapter_elapsed = elapsed - chapter["start_time"]
|
||||
chapter_total = chapter["end_time"] - chapter["start_time"]
|
||||
return (chapter["title"], chapter_elapsed, chapter_total)
|
||||
|
||||
last_chapter = self.chapters[-1]
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user