diff --git a/SYS/background_services.py b/SYS/background_services.py index 04bdc41..e631f4b 100644 --- a/SYS/background_services.py +++ b/SYS/background_services.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os import sys import subprocess from pathlib import Path @@ -98,6 +99,7 @@ def ensure_zerotier_server_running() -> None: "--storage-path", str(storage_path), "--port", str(port), "--monitor"] + cmd += ["--parent-pid", str(os.getpid())] if api_key: cmd += ["--api-key", str(api_key)] diff --git a/cmdlet/_shared.py b/cmdlet/_shared.py index c593fee..3e7ef84 100644 --- a/cmdlet/_shared.py +++ b/cmdlet/_shared.py @@ -3828,10 +3828,43 @@ def check_url_exists_in_storage( except Exception: cm = nullcontext() + auto_confirm_reason: Optional[str] = None + if in_pipeline and stage_ctx is not None: + try: + total_stages = int(getattr(stage_ctx, "total_stages", 0)) + except Exception: + total_stages = 0 + try: + is_last_stage = bool(getattr(stage_ctx, "is_last_stage", False)) + except Exception: + is_last_stage = False + if total_stages > 1 and not is_last_stage: + auto_confirm_reason = "pipeline stage (pre-last)" + if auto_confirm_reason is None: + try: + stdin_interactive = bool(sys.stdin and sys.stdin.isatty()) + except Exception: + stdin_interactive = False + if not stdin_interactive: + auto_confirm_reason = "non-interactive stdin" + + answered_yes = True with cm: get_stderr_console().print(table) setattr(table, "_rendered_by_cmdlet", True) - answered_yes = bool(Confirm.ask("Continue anyway?", default=False, console=get_stderr_console())) + if auto_confirm_reason is None: + answered_yes = bool(Confirm.ask("Continue anyway?", default=False, console=get_stderr_console())) + else: + debug( + f"Bulk URL preflight auto-confirmed duplicates ({auto_confirm_reason}); continuing without user input." + ) + try: + log( + f"Auto-confirmed duplicate URL warning ({auto_confirm_reason}). Continuing...", + file=sys.stderr, + ) + except Exception: + pass if in_pipeline: try: diff --git a/scripts/remote_storage_server.py b/scripts/remote_storage_server.py index 32a1e05..7fe83f6 100644 --- a/scripts/remote_storage_server.py +++ b/scripts/remote_storage_server.py @@ -696,16 +696,22 @@ def main(): action="store_true", help="Shut down if parent process dies" ) + parser.add_argument( + "--parent-pid", + type=int, + default=None, + help="Explicit PID to monitor (defaults to the immediate parent process)", + ) args = parser.parse_args() # Start monitor thread if requested if args.monitor: - ppid = os.getppid() - if ppid > 1: + monitor_pid = args.parent_pid or os.getppid() + if monitor_pid > 1: monitor_thread = threading.Thread( target=monitor_parent, - args=(ppid, ), + args=(monitor_pid, ), daemon=True ) monitor_thread.start() diff --git a/tool/ytdlp.py b/tool/ytdlp.py index 3da8d47..0a5f9ee 100644 --- a/tool/ytdlp.py +++ b/tool/ytdlp.py @@ -165,6 +165,74 @@ def _parse_csv_list(value: Any) -> Optional[List[str]]: return parts or None +_BROWSER_COOKIES_AVAILABLE: Optional[bool] = None +_BROWSER_COOKIE_WARNING_EMITTED = False + + +def _browser_cookie_candidate_paths() -> List[Path]: + try: + home = Path.home() + except Exception: + home = Path.cwd() + + candidates: List[Path] = [] + if os.name == "nt": + for env_value in (os.getenv("LOCALAPPDATA"), os.getenv("APPDATA")): + if not env_value: + continue + base_path = Path(env_value) + if not base_path: + continue + candidates.extend([ + base_path / "Google" / "Chrome" / "User Data" / "Default" / "Cookies", + base_path / "Chromium" / "User Data" / "Default" / "Cookies", + base_path / "BraveSoftware" / "Brave-Browser" / "User Data" / "Default" / "Cookies", + ]) + else: + candidates.extend([ + home / ".config" / "google-chrome" / "Default" / "Cookies", + home / ".config" / "chromium" / "Default" / "Cookies", + home / ".config" / "BraveSoftware" / "Brave-Browser" / "Default" / "Cookies", + ]) + if sys.platform == "darwin": + candidates.extend([ + home / "Library" / "Application Support" / "Google" / "Chrome" / "Default" / "Cookies", + home / "Library" / "Application Support" / "Chromium" / "Default" / "Cookies", + home / "Library" / "Application Support" / "BraveSoftware" / "Brave-Browser" / "Default" / "Cookies", + ]) + return candidates + + +def _has_browser_cookie_database() -> bool: + global _BROWSER_COOKIES_AVAILABLE + if _BROWSER_COOKIES_AVAILABLE is not None: + return _BROWSER_COOKIES_AVAILABLE + + for path in _browser_cookie_candidate_paths(): + try: + if path.is_file(): + _BROWSER_COOKIES_AVAILABLE = True + return True + except Exception: + continue + + _BROWSER_COOKIES_AVAILABLE = False + return False + + +def _add_browser_cookies_if_available(options: Dict[str, Any]) -> None: + global _BROWSER_COOKIE_WARNING_EMITTED + if _has_browser_cookie_database(): + options["cookiesfrombrowser"] = ["chrome"] + return + if not _BROWSER_COOKIE_WARNING_EMITTED: + log( + "Browser cookie extraction skipped because no Chrome-compatible cookie database was found. " + "Provide a cookies file via config or --cookies if authentication is required." + ) + _BROWSER_COOKIE_WARNING_EMITTED = True + + def ensure_yt_dlp_ready() -> None: """Verify yt-dlp is importable, raising DownloadError if missing.""" @@ -272,7 +340,7 @@ def list_formats( ydl_opts["cookiefile"] = str(cookiefile) else: # Best effort attempt to use browser cookies if no file is explicitly passed - ydl_opts["cookiesfrombrowser"] = ["chrome"] + _add_browser_cookies_if_available(ydl_opts) if no_playlist: ydl_opts["noplaylist"] = True @@ -365,7 +433,7 @@ def probe_url( ydl_opts["cookiefile"] = str(cookiefile) else: # Best effort fallback - ydl_opts["cookiesfrombrowser"] = ["chrome"] + _add_browser_cookies_if_available(ydl_opts) if no_playlist: ydl_opts["noplaylist"] = True @@ -708,7 +776,7 @@ class YtDlpTool: # Add browser cookies support "just in case" if no file found (best effort) # This uses yt-dlp's support for extracting from common browsers. # Defaulting to 'chrome' as the most common path. - base_options["cookiesfrombrowser"] = ["chrome"] + _add_browser_cookies_if_available(base_options) # Special handling for format keywords if opts.ytdl_format == "audio":