feat: coordinate authoritative playback and audio routing

This commit is contained in:
Media
2026-08-06 18:52:01 -05:00
parent 8b3bdae8a0
commit 3ee7f94d72
17 changed files with 4863 additions and 186 deletions
+109 -3
View File
@@ -4,9 +4,11 @@ from __future__ import annotations
import logging
import os
import signal
import shutil
import subprocess
import threading
import time
import uuid
from concurrent.futures import Future, ThreadPoolExecutor
from dataclasses import dataclass
@@ -32,6 +34,10 @@ class DownloadEvent:
message: str | None = None
job_id: str | None = None
generation: int | None = None
url: str | None = None
submitter: str | None = None
note: str | None = None
reused_slot: bool = False
EventCallback = Callable[[DownloadEvent], None]
@@ -66,7 +72,7 @@ class DownloadManager:
store: ControllerStore,
*,
on_event: EventCallback | None = None,
runner: Runner = subprocess.run,
runner: Runner | None = None,
executor: ThreadPoolExecutor | None = None,
):
self.config = config
@@ -80,6 +86,7 @@ class DownloadManager:
self._owns_executor = executor is None
self._lock = threading.Lock()
self._futures: set[Future[Any]] = set()
self._processes: set[subprocess.Popen[str]] = set()
self._stopping = False
def enqueue(
@@ -106,6 +113,10 @@ class DownloadManager:
state="queued",
job_id=reservation.job_id,
generation=reservation.generation,
url=url,
submitter=submitter,
note=note,
reused_slot=reservation.reused_slot,
)
)
try:
@@ -184,7 +195,7 @@ class DownloadManager:
url,
]
try:
result = self.runner(
result = self._run_command(
arguments,
check=True,
capture_output=True,
@@ -246,7 +257,7 @@ class DownloadManager:
),
url,
]
self.runner(
self._run_command(
arguments,
check=True,
text=True,
@@ -322,6 +333,90 @@ class DownloadManager:
return f"yt-dlp failed with exit code {exc.returncode}"
return f"download failed: {exc}"
def _run_command(
self,
arguments: list[str],
*,
check: bool,
text: bool,
timeout: float,
capture_output: bool = False,
) -> subprocess.CompletedProcess[str]:
if self.runner is not None:
return self.runner(
arguments,
check=check,
text=text,
timeout=timeout,
capture_output=capture_output,
)
with self._lock:
if self._stopping:
raise RuntimeError("Controller is shutting down")
process = subprocess.Popen(
arguments,
stdout=subprocess.PIPE if capture_output else None,
stderr=subprocess.PIPE if capture_output else None,
text=text,
start_new_session=True,
)
self._processes.add(process)
try:
try:
stdout, stderr = process.communicate(timeout=timeout)
except subprocess.TimeoutExpired:
self._terminate_process(process, force=False)
try:
stdout, stderr = process.communicate(timeout=2.0)
except subprocess.TimeoutExpired:
self._terminate_process(process, force=True)
stdout, stderr = process.communicate()
raise subprocess.TimeoutExpired(
arguments,
timeout,
output=stdout,
stderr=stderr,
)
completed = subprocess.CompletedProcess(
arguments,
process.returncode,
stdout=stdout,
stderr=stderr,
)
if check and process.returncode:
raise subprocess.CalledProcessError(
process.returncode,
arguments,
output=stdout,
stderr=stderr,
)
return completed
finally:
with self._lock:
self._processes.discard(process)
@staticmethod
def _terminate_process(
process: subprocess.Popen[str],
*,
force: bool,
) -> None:
if process.poll() is not None:
return
try:
os.killpg(
process.pid,
signal.SIGKILL if force else signal.SIGTERM,
)
except ProcessLookupError:
return
except OSError:
try:
process.kill() if force else process.terminate()
except ProcessLookupError:
pass
@staticmethod
def _fsync_directory(path: Path) -> None:
descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
@@ -387,6 +482,17 @@ class DownloadManager:
def shutdown(self, *, wait: bool = True) -> None:
with self._lock:
self._stopping = True
processes = tuple(self._processes)
for process in processes:
self._terminate_process(process, force=False)
deadline = time.monotonic() + 2.0
for process in processes:
if process.poll() is not None:
continue
try:
process.wait(timeout=max(0.0, deadline - time.monotonic()))
except subprocess.TimeoutExpired:
self._terminate_process(process, force=True)
if self._owns_executor:
self._executor.shutdown(wait=wait, cancel_futures=True)