|
@@ -8,6 +8,7 @@ import re
|
|
|
import select
|
|
import select
|
|
|
import sys
|
|
import sys
|
|
|
import time
|
|
import time
|
|
|
|
|
+import traceback
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
from asot_player.feed import Episode, fetch_episodes
|
|
from asot_player.feed import Episode, fetch_episodes
|
|
@@ -180,8 +181,8 @@ class Player:
|
|
|
try:
|
|
try:
|
|
|
self.engine.play(ep.url, seek_to=seek_to, muted=muted, volume=volume)
|
|
self.engine.play(ep.url, seek_to=seek_to, muted=muted, volume=volume)
|
|
|
except EngineError as e:
|
|
except EngineError as e:
|
|
|
- print(f"[asot] {reason}: {e}", file=sys.stderr)
|
|
|
|
|
- sys.exit(2)
|
|
|
|
|
|
|
+ # let the caller report it above the bar and pick the exit code
|
|
|
|
|
+ raise EngineError(f"{reason}: {e}") from e
|
|
|
self.msg_override = f"{reason.capitalize()} \u2192 ASOT {ep.number:04d}"
|
|
self.msg_override = f"{reason.capitalize()} \u2192 ASOT {ep.number:04d}"
|
|
|
self.msg_until = time.time() + 3.0
|
|
self.msg_until = time.time() + 3.0
|
|
|
|
|
|
|
@@ -280,6 +281,15 @@ class Player:
|
|
|
# entry point
|
|
# entry point
|
|
|
# ---------------------------------------------------------------------------
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
+def _print_above(ui, text: str):
|
|
|
|
|
+ """Report a problem without disturbing the bar: erase the player block,
|
|
|
|
|
+ print ``text`` at the line above it (the last line of normal output), and
|
|
|
|
|
+ leave the bottom free — the next frame redraws the bar there, so two bars
|
|
|
|
|
+ can never coexist."""
|
|
|
|
|
+ sys.stdout.write(ui.clear_block())
|
|
|
|
|
+ sys.stdout.write(text.rstrip("\n") + "\n")
|
|
|
|
|
+ sys.stdout.flush()
|
|
|
|
|
+
|
|
|
def main():
|
|
def main():
|
|
|
ap = argparse.ArgumentParser(
|
|
ap = argparse.ArgumentParser(
|
|
|
prog="asot",
|
|
prog="asot",
|
|
@@ -345,6 +355,7 @@ def main():
|
|
|
msg = f"[asot] starting ASOT {start_ep.number:04d}"
|
|
msg = f"[asot] starting ASOT {start_ep.number:04d}"
|
|
|
print(msg, file=sys.stderr)
|
|
print(msg, file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
+ rc = 0
|
|
|
try:
|
|
try:
|
|
|
p._load(start_ep, reason="Starting",
|
|
p._load(start_ep, reason="Starting",
|
|
|
seek_to=resume_pos if resume else 0.0)
|
|
seek_to=resume_pos if resume else 0.0)
|
|
@@ -354,59 +365,71 @@ def main():
|
|
|
last_save = time.time()
|
|
last_save = time.time()
|
|
|
while not p.finished and tick < 50_000:
|
|
while not p.finished and tick < 50_000:
|
|
|
t0 = time.time()
|
|
t0 = time.time()
|
|
|
-
|
|
|
|
|
- # keys (applied immediately)
|
|
|
|
|
- keys = kb.drain() if kb is not None else []
|
|
|
|
|
- for k in keys:
|
|
|
|
|
- if p.finished:
|
|
|
|
|
- break
|
|
|
|
|
- p.handle_key(k)
|
|
|
|
|
-
|
|
|
|
|
try:
|
|
try:
|
|
|
- st = p._status()
|
|
|
|
|
- drops = 0 # healthy tick resets the budget
|
|
|
|
|
- except EngineError as e:
|
|
|
|
|
- # mpv died (stream drop / EOF race / network loss). Do not
|
|
|
|
|
- # crash: resume from the saved position, end only when the
|
|
|
|
|
- # episode has truly finished.
|
|
|
|
|
- drops += 1
|
|
|
|
|
- print(f"[asot] playback dropped ({e.__class__.__name__}); "
|
|
|
|
|
- f"recovery attempt {drops}…", file=sys.stderr)
|
|
|
|
|
- if drops > 5 or not p.recover():
|
|
|
|
|
- p.msg_override = "Playback failed — stream unavailable"
|
|
|
|
|
|
|
+ # keys (applied immediately)
|
|
|
|
|
+ keys = kb.drain() if kb is not None else []
|
|
|
|
|
+ for k in keys:
|
|
|
|
|
+ if p.finished:
|
|
|
|
|
+ break
|
|
|
|
|
+ p.handle_key(k)
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ st = p._status()
|
|
|
|
|
+ drops = 0 # healthy tick resets the budget
|
|
|
|
|
+ except EngineError as e:
|
|
|
|
|
+ # mpv died (stream drop / EOF race / network loss): clear
|
|
|
|
|
+ # the bar, say so above it, then resume from the saved
|
|
|
|
|
+ # position — end only when the episode has truly finished.
|
|
|
|
|
+ drops += 1
|
|
|
|
|
+ _print_above(p.ui, f"[asot] playback dropped "
|
|
|
|
|
+ f"({e.__class__.__name__}); recovery "
|
|
|
|
|
+ f"attempt {drops}…")
|
|
|
|
|
+ if drops > 5 or not p.recover():
|
|
|
|
|
+ p.msg_override = "Playback failed — stream unavailable"
|
|
|
|
|
+ p.msg_until = time.time() + 6.0
|
|
|
|
|
+ break
|
|
|
|
|
+ tick += 1 # keep the global tick cap binding
|
|
|
|
|
+ time.sleep(1.0) # don't hammer a dead stream
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # eof → next episode (auto-advance)
|
|
|
|
|
+ if st.state == "eof":
|
|
|
|
|
+ if p.advance(1):
|
|
|
|
|
+ continue
|
|
|
|
|
+ p.msg_override = f"Finished — end of feed at ASOT {p.ep.number:04d}"
|
|
|
p.msg_until = time.time() + 6.0
|
|
p.msg_until = time.time() + 6.0
|
|
|
|
|
+ p.render_once(st)
|
|
|
|
|
+ p.finished = True
|
|
|
break
|
|
break
|
|
|
- tick += 1 # keep the global tick cap binding
|
|
|
|
|
- time.sleep(1.0) # don't hammer a dead stream
|
|
|
|
|
- continue
|
|
|
|
|
|
|
|
|
|
- # eof → next episode (auto-advance)
|
|
|
|
|
- if st.state == "eof":
|
|
|
|
|
- if p.advance(1):
|
|
|
|
|
- continue
|
|
|
|
|
- p.msg_override = f"Finished — end of feed at ASOT {p.ep.number:04d}"
|
|
|
|
|
- p.msg_until = time.time() + 6.0
|
|
|
|
|
p.render_once(st)
|
|
p.render_once(st)
|
|
|
- p.finished = True
|
|
|
|
|
- break
|
|
|
|
|
|
|
|
|
|
- p.render_once(st)
|
|
|
|
|
-
|
|
|
|
|
- changed = (abs(st.position - p.state.position) > 1.0
|
|
|
|
|
- or st.muted != p.state.muted
|
|
|
|
|
- or st.paused != p.state.paused)
|
|
|
|
|
- if changed or time.time() - last_save >= 3.0:
|
|
|
|
|
- p.record(st)
|
|
|
|
|
- last_save = time.time()
|
|
|
|
|
-
|
|
|
|
|
- elapsed = time.time() - t0
|
|
|
|
|
- sleep_for = 0.25 if keys else 0.9
|
|
|
|
|
- rem = sleep_for - elapsed
|
|
|
|
|
- if rem > 0.1:
|
|
|
|
|
- time.sleep(min(rem, 0.3))
|
|
|
|
|
- tick += 1
|
|
|
|
|
|
|
+ changed = (abs(st.position - p.state.position) > 1.0
|
|
|
|
|
+ or st.muted != p.state.muted
|
|
|
|
|
+ or st.paused != p.state.paused)
|
|
|
|
|
+ if changed or time.time() - last_save >= 3.0:
|
|
|
|
|
+ p.record(st)
|
|
|
|
|
+ last_save = time.time()
|
|
|
|
|
+
|
|
|
|
|
+ elapsed = time.time() - t0
|
|
|
|
|
+ sleep_for = 0.25 if keys else 0.9
|
|
|
|
|
+ rem = sleep_for - elapsed
|
|
|
|
|
+ if rem > 0.1:
|
|
|
|
|
+ time.sleep(min(rem, 0.3))
|
|
|
|
|
+ tick += 1
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ # anything unexpected: erase the bar, print the traceback at
|
|
|
|
|
+ # the line above it, then keep drawing the bar at the bottom
|
|
|
|
|
+ _print_above(p.ui, traceback.format_exc().rstrip())
|
|
|
|
|
+ tick += 1
|
|
|
|
|
+ time.sleep(1.0)
|
|
|
|
|
+ continue
|
|
|
except EngineError as e:
|
|
except EngineError as e:
|
|
|
- print(f"[asot] playback error: {e}", file=sys.stderr)
|
|
|
|
|
|
|
+ _print_above(p.ui, f"[asot] playback error: {e}")
|
|
|
|
|
+ rc = 2
|
|
|
|
|
+ except KeyboardInterrupt:
|
|
|
|
|
+ _print_above(p.ui, "[asot] interrupted")
|
|
|
|
|
+ rc = 130
|
|
|
finally:
|
|
finally:
|
|
|
try:
|
|
try:
|
|
|
if kb is not None:
|
|
if kb is not None:
|