|
|
@@ -25,6 +25,12 @@ except ImportError:
|
|
|
tty = None
|
|
|
|
|
|
|
|
|
+# Wall-clock safety net for an unattended session (ticks are ~0.3–0.9 s each,
|
|
|
+# so the old 50 000-tick cap silently ended long/idle sessions after ~5 h with
|
|
|
+# exit code 0). Reaching this limit is reported, and exits non-zero.
|
|
|
+MAX_RUNTIME_SECONDS = 36 * 3600
|
|
|
+
|
|
|
+
|
|
|
# ---------------------------------------------------------------------------
|
|
|
# key handling (non-full-screen; cbreak mode, restored on exit)
|
|
|
# ---------------------------------------------------------------------------
|
|
|
@@ -244,7 +250,12 @@ class Player:
|
|
|
volume=max(0.0, min(1.0, float(st.volume))))
|
|
|
except EngineError:
|
|
|
return False
|
|
|
- st.paused = False
|
|
|
+ if st.paused:
|
|
|
+ # a paused player stays paused across the restart
|
|
|
+ try:
|
|
|
+ self.engine.set_pause(True)
|
|
|
+ except EngineError:
|
|
|
+ st.paused = False
|
|
|
self.msg_override = f"Recovered \u2192 ASOT {self.ep.number:04d}"
|
|
|
self.msg_until = time.time() + 3.0
|
|
|
return True
|
|
|
@@ -290,6 +301,83 @@ def _print_above(ui, text: str):
|
|
|
sys.stdout.write(text.rstrip("\n") + "\n")
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
+
|
|
|
+def run_player(p, kb) -> int:
|
|
|
+ """The main playback loop. Returns the exit code: 0 when playback ended
|
|
|
+ normally (quit key / end of feed); 1 when it was stopped by the drop
|
|
|
+ budget or the wall-clock runtime limit — both are failures, and both
|
|
|
+ print a reason above the bar, so a closed player is never silent."""
|
|
|
+ deadline = time.time() + MAX_RUNTIME_SECONDS
|
|
|
+ drops = 0 # consecutive mpv deaths recovered from
|
|
|
+ last_save = time.time()
|
|
|
+ stop_msg = "" # why the loop ended when not finished
|
|
|
+ while not p.finished and time.time() < deadline:
|
|
|
+ t0 = time.time()
|
|
|
+ try:
|
|
|
+ # 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():
|
|
|
+ stop_msg = "[asot] playback failed — stream unavailable"
|
|
|
+ break
|
|
|
+ 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.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))
|
|
|
+ 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())
|
|
|
+ time.sleep(1.0)
|
|
|
+ continue
|
|
|
+
|
|
|
+ if p.finished:
|
|
|
+ return 0
|
|
|
+ # the loop ended on the drop budget or the runtime limit: a failure, not
|
|
|
+ # a clean exit — report it above the bar and exit non-zero. (The old
|
|
|
+ # 50 000-tick cap ended here silently with code 0: a player left paused
|
|
|
+ # for ~5 h "just closed" with no trace.)
|
|
|
+ _print_above(p.ui, stop_msg or "[asot] runtime limit reached — stopping")
|
|
|
+ return 1
|
|
|
+
|
|
|
def main():
|
|
|
ap = argparse.ArgumentParser(
|
|
|
prog="asot",
|
|
|
@@ -360,70 +448,7 @@ def main():
|
|
|
p._load(start_ep, reason="Starting",
|
|
|
seek_to=resume_pos if resume else 0.0)
|
|
|
|
|
|
- tick = 0
|
|
|
- drops = 0 # consecutive mpv deaths recovered from
|
|
|
- last_save = time.time()
|
|
|
- while not p.finished and tick < 50_000:
|
|
|
- t0 = time.time()
|
|
|
- try:
|
|
|
- # 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.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
|
|
|
- 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
|
|
|
+ rc = run_player(p, kb)
|
|
|
except EngineError as e:
|
|
|
_print_above(p.ui, f"[asot] playback error: {e}")
|
|
|
rc = 2
|
|
|
@@ -449,7 +474,7 @@ def main():
|
|
|
sys.stdout.flush()
|
|
|
except Exception:
|
|
|
pass
|
|
|
- return 0
|
|
|
+ return rc
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|