feat: overhaul video player controller

This commit is contained in:
Media
2026-08-06 20:08:12 -05:00
parent 72cc66df92
commit 5e81403be3
10 changed files with 2019 additions and 122 deletions
+167
View File
@@ -24,6 +24,24 @@ class ImmediateExecutor:
return future
class QueuedExecutor:
def __init__(self):
self.calls = []
def submit(self, function, *args, **kwargs):
future = Future()
self.calls.append((future, function, args, kwargs))
return future
def run_next(self):
future, function, args, kwargs = self.calls.pop(0)
try:
future.set_result(function(*args, **kwargs))
except BaseException as exc:
future.set_exception(exc)
return future
class FakeYtDlp:
def __init__(
self,
@@ -267,6 +285,155 @@ class DownloadManagerTests(unittest.TestCase):
self.assertEqual(manager.recover_pending_deletions(), [1])
self.assertFalse(slot.exists())
def test_async_deletion_failure_remains_durable_and_is_retryable(self):
reservation = self.store.reserve_download(
"https://example.test/retry-delete",
None,
None,
job_id="retry-delete",
)
self.store.fail_download(
"retry-delete",
1,
reservation.generation,
"failed",
)
slot = self.media / "slot1"
slot.mkdir(parents=True)
(slot / "retry.mp4").write_bytes(b"retry")
events = []
manager = self.manager(FakeYtDlp(self.media), events)
original_recover = manager._recover_deletion
attempts = 0
def fail_once(token):
nonlocal attempts
attempts += 1
if attempts == 1:
raise OSError("transient media failure")
return original_recover(token)
manager._recover_deletion = fail_once
token = manager.schedule_delete_slot(1, reason="manual")
self.assertEqual(token.slot, 1)
self.assertEqual(self.store.get_slot(1).media_state, "deleting")
self.assertEqual(events[-1].state, "delete_error")
self.assertEqual(len(self.store.pending_deletions()), 1)
self.assertTrue(manager.schedule_pending_deletion_recovery())
self.assertEqual(attempts, 2)
self.assertEqual(events[-1].state, "deleted")
self.assertEqual(self.store.get_slot(1).media_state, "empty")
self.assertFalse(slot.exists())
def test_async_pending_recovery_retries_after_guard_is_cleared(self):
reservation = self.store.reserve_download(
"https://example.test/deferred-async-delete",
None,
None,
job_id="deferred-async-delete",
)
self.store.fail_download(
"deferred-async-delete",
1,
reservation.generation,
"failed",
)
self.store.begin_delete(1, reason="manual")
slot = self.media / "slot1"
slot.mkdir(parents=True)
(slot / "active.mp4").write_bytes(b"active")
self.store.protect_slot(1, "player_active", guard_id="active-player")
events = []
manager = self.manager(FakeYtDlp(self.media), events)
self.assertTrue(manager.schedule_pending_deletion_recovery())
self.assertEqual(events[-1].state, "delete_deferred")
self.assertEqual(self.store.get_slot(1).media_state, "deleting")
self.assertTrue(slot.exists())
self.store.clear_playback_guards(reason="fresh terminal idle evidence")
self.assertTrue(manager.schedule_pending_deletion_recovery())
self.assertEqual(events[-1].state, "deleted")
self.assertEqual(self.store.get_slot(1).media_state, "empty")
self.assertFalse(slot.exists())
def test_guarded_recovery_restores_crash_staged_trash_to_canonical_slot(self):
reservation = self.store.reserve_download(
"https://example.test/restore-trash",
None,
None,
job_id="restore-trash",
)
self.store.fail_download(
"restore-trash",
1,
reservation.generation,
"failed",
)
token = self.store.begin_delete(1, reason="expired")
trash = (
self.media
/ ".controller-trash"
/ f"slot1.generation{token.generation}.crash"
)
trash.mkdir(parents=True)
(trash / "still-active.mp4").write_bytes(b"active")
self.store.protect_slot(1, "player_active", guard_id="fresh-active")
events = []
manager = self.manager(FakeYtDlp(self.media), events)
self.assertTrue(manager.schedule_pending_deletion_recovery())
canonical = self.media / "slot1"
self.assertEqual(events[-1].state, "delete_deferred")
self.assertTrue((canonical / "still-active.mp4").exists())
self.assertFalse(trash.exists())
self.assertEqual(self.store.get_slot(1).media_state, "deleting")
def test_queued_deletion_rechecks_guard_before_filesystem_mutation(self):
reservation = self.store.reserve_download(
"https://example.test/queued-delete-guard",
None,
None,
job_id="queued-delete-guard",
)
self.store.fail_download(
"queued-delete-guard",
1,
reservation.generation,
"failed",
)
self.store.begin_delete(1, reason="manual")
slot = self.media / "slot1"
slot.mkdir(parents=True)
(slot / "must-survive.mp4").write_bytes(b"active")
events = []
executor = QueuedExecutor()
manager = DownloadManager(
self.config,
self.store,
runner=FakeYtDlp(self.media),
executor=executor,
on_event=events.append,
)
self.assertTrue(manager.schedule_pending_deletion_recovery())
self.store.protect_slot(1, "player_active", guard_id="late-player")
executor.run_next()
self.assertEqual(events[-1].state, "delete_deferred")
self.assertEqual(self.store.get_slot(1).media_state, "deleting")
self.assertTrue((slot / "must-survive.mp4").exists())
self.store.clear_playback_guards(reason="fresh terminal idle evidence")
self.assertTrue(manager.schedule_pending_deletion_recovery())
executor.run_next()
self.assertEqual(events[-1].state, "deleted")
self.assertEqual(self.store.get_slot(1).media_state, "empty")
self.assertFalse(slot.exists())
def test_shutdown_terminates_tracked_process_without_waiting_for_job_timeout(self):
manager = DownloadManager(self.config, self.store)
errors = []