Add YtdlpClient: shared JSON invocation layer for yt-dlp
Every metadata call site previously built its own command list. The new ytsage/core/ytsage_client.py centralizes binary resolution (managed, verified binary only), cookie/proxy args from ConfigManager (with session-state overrides), utf-8 output handling, process-group-safe timeouts, and a short-TTL cache for flat/search results. Provides fetch_video_info, fetch_flat_info, fetch_flat_entries (with -I range pagination), search (ytsearchN:), and fetch_account_feed (youtube.com/feed/subscriptions with cookies) plus a generic YtdlpWorker QThread. AnalysisThread now delegates its subprocess execution to the shared runner; its signal surface is unchanged. Groundwork for the SageTube watch/search/browse/feed features. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,13 @@
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast
|
||||
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
from PySide6.QtCore import QMetaObject, Qt, Q_ARG, QThread, Signal, QTimer
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
from ..core.ytsage_client import run_ytdlp_capture
|
||||
from ..core.ytsage_utils import validate_video_url, parse_yt_dlp_error
|
||||
from ..core.ytsage_yt_dlp import get_yt_dlp_path
|
||||
from ..utils.ytsage_constants import SUBPROCESS_CREATIONFLAGS
|
||||
from ..utils.ytsage_localization import _
|
||||
from ..utils.ytsage_logger import logger
|
||||
|
||||
@@ -100,44 +97,8 @@ class AnalysisThread(QThread):
|
||||
|
||||
@staticmethod
|
||||
def _run_ytdlp(cmd: list, timeout: int) -> subprocess.CompletedProcess:
|
||||
"""Run yt-dlp capturing output, killing the whole process group on timeout.
|
||||
|
||||
subprocess.run() only kills the direct child on TimeoutExpired, leaking
|
||||
grandchildren (deno); it also loses any stderr produced before the
|
||||
timeout. Use Popen with a new session so the entire group can be
|
||||
reaped, and surface the partial stderr in the raised exception.
|
||||
"""
|
||||
popen_kwargs: Dict[str, Any] = {
|
||||
"stdout": subprocess.PIPE,
|
||||
"stderr": subprocess.PIPE,
|
||||
"text": True,
|
||||
"encoding": "utf-8",
|
||||
"errors": "replace",
|
||||
}
|
||||
if sys.platform == "win32":
|
||||
popen_kwargs["creationflags"] = SUBPROCESS_CREATIONFLAGS
|
||||
else:
|
||||
popen_kwargs["start_new_session"] = True
|
||||
|
||||
proc = subprocess.Popen(cmd, **popen_kwargs)
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=timeout)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
if sys.platform == "win32":
|
||||
subprocess.run(
|
||||
["taskkill", "/F", "/T", "/PID", str(proc.pid)],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
creationflags=SUBPROCESS_CREATIONFLAGS,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
||||
except (ProcessLookupError, PermissionError, OSError):
|
||||
pass
|
||||
stdout, stderr = proc.communicate()
|
||||
exc.stdout, exc.stderr = stdout, stderr
|
||||
raise
|
||||
return subprocess.CompletedProcess(cmd, proc.returncode, stdout, stderr)
|
||||
"""Run yt-dlp via the shared client runner (process-group-safe)."""
|
||||
return run_ytdlp_capture(cmd, timeout=timeout)
|
||||
|
||||
def _analyze_url_with_subprocess(self, url: str) -> None:
|
||||
"""Analyze URL using yt-dlp executable."""
|
||||
|
||||
Reference in New Issue
Block a user