diff --git a/main.py b/main.py index a01e374..cf46651 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,50 @@ import sys import os -# Suppress console window for windowed applications (PyInstaller --windowed) -if getattr(sys, 'frozen', False): - # Running as compiled executable - if sys.stdout is None or sys.stderr is None: - # Windowed mode - redirect stdout/stderr to prevent console window flicker - import io - sys.stdout = io.StringIO() - sys.stderr = io.StringIO() +# Early detection and handling of windowed mode to prevent console window flicker +def setup_windowed_mode(): + """ + Setup windowed mode handling to prevent console window flickering. + This must be called as early as possible in the application startup. + """ + if getattr(sys, 'frozen', False): + # Running as compiled executable (PyInstaller) + if sys.stdout is None or sys.stderr is None: + # Windowed mode - completely suppress console output + import io + null_stream = io.StringIO() + if sys.stdout is None: + sys.stdout = null_stream + if sys.stderr is None: + sys.stderr = null_stream + else: + # Console mode but compiled - redirect to null to prevent window flicker + if os.name == 'nt': # Windows + try: + import io + # Create a null output stream + null_stream = io.StringIO() + # Only redirect if we detect this might cause console window issues + # Check if we're not already in a console + try: + # Try to get console window handle - if this fails, we're likely windowed + import ctypes + kernel32 = ctypes.windll.kernel32 + console_window = kernel32.GetConsoleWindow() + if console_window == 0: + # No console window exists, redirect to prevent one from appearing + sys.stdout = null_stream + sys.stderr = null_stream + except: + # If console detection fails, err on the side of caution + sys.stdout = null_stream + sys.stderr = null_stream + except: + # If all else fails, continue normally + pass + +# Call windowed mode setup immediately +setup_windowed_mode() from PySide6.QtWidgets import QApplication, QMessageBox diff --git a/src/core/ytsage_logging.py b/src/core/ytsage_logging.py index 31b5a68..37d1478 100644 --- a/src/core/ytsage_logging.py +++ b/src/core/ytsage_logging.py @@ -98,8 +98,21 @@ def setup_logging(): stdout_available = sys.stdout is not None is_windowed_app = getattr(sys, 'frozen', False) and sys.stdout is None - # Skip console output if we're in a windowed application to prevent brief console window - if stdout_available and not is_windowed_app: + # Additional check for windowed mode - check if console window exists on Windows + is_console_hidden = False + if sys.platform == 'win32' and getattr(sys, 'frozen', False): + try: + import ctypes + kernel32 = ctypes.windll.kernel32 + console_window = kernel32.GetConsoleWindow() + if console_window == 0: + is_console_hidden = True + except: + # If console detection fails, assume we might be windowed + is_console_hidden = True + + # Skip console output if we're in a windowed application or console is hidden + if stdout_available and not is_windowed_app and not is_console_hidden: try: logger.add( sys.stdout, @@ -120,8 +133,8 @@ def setup_logging(): except Exception: stdout_available = False - # If stdout is not available or we're in windowed mode, try stderr for critical messages only - if (not stdout_available or is_windowed_app) and not is_windowed_app: + # If stdout is not available or we're in windowed mode, skip stderr too to prevent console windows + if (not stdout_available or is_windowed_app or is_console_hidden) and not (is_windowed_app or is_console_hidden): try: if sys.stderr is not None: logger.add( diff --git a/src/core/ytsage_utils.py b/src/core/ytsage_utils.py index bea694b..9eba40f 100644 --- a/src/core/ytsage_utils.py +++ b/src/core/ytsage_utils.py @@ -36,6 +36,37 @@ _version_cache = { CACHE_EXPIRY = 300 +def run_subprocess_hidden(cmd_args, **kwargs): + """ + Run subprocess with proper console window hiding for windowed applications. + + Args: + cmd_args: Command arguments list + **kwargs: Additional subprocess.run arguments + + Returns: + subprocess.CompletedProcess result + """ + # Set up creation flags to hide console window + creation_flags = SUBPROCESS_CREATIONFLAGS + if getattr(sys, 'frozen', False): + # Running as compiled executable - use additional flags to prevent console flicker + creation_flags |= subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS + + # Set default parameters to suppress console + default_kwargs = { + 'capture_output': True, + 'text': True, + 'creationflags': creation_flags, + 'stdin': subprocess.DEVNULL + } + + # Merge with provided kwargs (user kwargs take precedence) + default_kwargs.update(kwargs) + + return subprocess.run(cmd_args, **default_kwargs) + + def get_file_mtime(filepath) -> float: """Get file modification time safely.""" try: @@ -196,8 +227,8 @@ def get_ytdlp_version_direct(yt_dlp_path=None) -> str: return "Not found" # Extra logic moved to src\utils\ytsage_constants.py - result = subprocess.run( - [yt_dlp_path, "--version"], capture_output=True, text=True, timeout=10, creationflags=SUBPROCESS_CREATIONFLAGS + result = run_subprocess_hidden( + [yt_dlp_path, "--version"], timeout=10 ) if result.returncode == 0: @@ -213,8 +244,8 @@ def get_ffmpeg_version_direct() -> str: """Get FFmpeg version directly without caching.""" try: # Extra logic moved to src\utils\ytsage_constants.py - result = subprocess.run( - ["ffmpeg", "-version"], capture_output=True, text=True, timeout=10, creationflags=SUBPROCESS_CREATIONFLAGS + result = run_subprocess_hidden( + ["ffmpeg", "-version"], timeout=10 ) if result.returncode == 0: @@ -242,8 +273,8 @@ def get_ffmpeg_version_direct() -> str: ffmpeg_exe = Path(ffmpeg_path).joinpath("ffmpeg") if ffmpeg_exe.exists(): - result = subprocess.run( - [ffmpeg_exe, "-version"], capture_output=True, text=True, timeout=10, creationflags=SUBPROCESS_CREATIONFLAGS + result = run_subprocess_hidden( + [ffmpeg_exe, "-version"], timeout=10 ) if result.returncode == 0: @@ -489,7 +520,7 @@ def update_yt_dlp() -> bool: # Compare versions and update if needed if version.parse(latest_version) > version.parse(current_version): logger.info(f"Updating yt-dlp from {current_version} to {latest_version}...") - update_result = subprocess.run( + update_result = run_subprocess_hidden( [ sys.executable, "-m", @@ -498,10 +529,7 @@ def update_yt_dlp() -> bool: "--upgrade", "yt-dlp", ], - capture_output=True, - text=True, - check=False, - creationflags=SUBPROCESS_CREATIONFLAGS, + check=False ) if update_result.returncode == 0: logger.info("yt-dlp successfully updated") diff --git a/src/core/ytsage_yt_dlp.py b/src/core/ytsage_yt_dlp.py index b0b2a30..f74f244 100644 --- a/src/core/ytsage_yt_dlp.py +++ b/src/core/ytsage_yt_dlp.py @@ -1,6 +1,7 @@ import os import shutil import subprocess +import sys from pathlib import Path from typing import Optional @@ -500,10 +501,22 @@ def check_ytdlp_binary() -> Optional[Path]: # Use subprocess to check if yt-dlp is available if OS_NAME == "Windows": # On Windows, use 'where' command and hide console window - # Extra logic moved to src\utils\ytsage_constants.py + # For windowed applications, use additional flags to prevent console window flicker + creation_flags = SUBPROCESS_CREATIONFLAGS + if getattr(sys, 'frozen', False): + # Running as compiled executable - use additional flags + creation_flags |= subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS result = subprocess.run( - ["where", "yt-dlp"], capture_output=True, text=True, check=False, creationflags=SUBPROCESS_CREATIONFLAGS + ["where", "yt-dlp"], + capture_output=True, + text=True, + check=False, + creationflags=creation_flags, + # Additional parameters to suppress console + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE ) if result.returncode == 0 and result.stdout.strip(): yt_dlp_path = result.stdout.strip().split("\n")[0] @@ -534,9 +547,22 @@ def check_ytdlp_installed() -> bool: if ytdlp_path: # Try to run yt-dlp --version to verify it's working try: - # Extra logic moved to src\utils\ytsage_constants.py + # For windowed applications, use additional flags to prevent console window flicker + creation_flags = SUBPROCESS_CREATIONFLAGS + if getattr(sys, 'frozen', False): + # Running as compiled executable - use additional flags + creation_flags |= subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS + result = subprocess.run( - [ytdlp_path, "--version"], capture_output=True, text=True, timeout=5, creationflags=SUBPROCESS_CREATIONFLAGS + [ytdlp_path, "--version"], + capture_output=True, + text=True, + timeout=5, + creationflags=creation_flags, + # Additional parameters to suppress console + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE ) return result.returncode == 0 except Exception: diff --git a/src/gui/ytsage_gui_main.py b/src/gui/ytsage_gui_main.py index edc174d..3fbc478 100644 --- a/src/gui/ytsage_gui_main.py +++ b/src/gui/ytsage_gui_main.py @@ -1,5 +1,6 @@ import json import subprocess +import sys import threading import webbrowser from pathlib import Path @@ -1735,7 +1736,21 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from # Execute command with hidden console window on Windows # Extra logic moved to src\utils\ytsage_constants.py - result = subprocess.run(cmd, capture_output=True, text=True, timeout=60, creationflags=SUBPROCESS_CREATIONFLAGS) + creation_flags = SUBPROCESS_CREATIONFLAGS + if getattr(sys, 'frozen', False): + # Running as compiled executable - use additional flags to prevent console flicker + creation_flags |= subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=60, + creationflags=creation_flags, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) if result.returncode != 0: raise Exception(f"yt-dlp failed: {result.stderr}")