Refactor/code cleanup (#37)

* fix imports
remove unused imports
use full import path
sort import (1. Standard Library, 2. Third-Party, 3. Local) in alphabetic.

* - remove: Method 3 from src.core.ytsage_downlader:cleanup_subtitle_file
  - it could delete the subtitle file of other movies if it present in same directory as it scane recursively.

- refactor: migrate from os.path to pathlib.Path for path handling
  - Replaced os.path methods with pathlib.Path to improve readability,
  - avoid repeatation.
  - cross-platform compatibility, and maintain cleaner code.

- improve: enhance code readability
  - Standardized string literals to use double quotes for consistency
  - Removed unnecessary spaces to maintain cleaner formatting
  - Applied code formatting for better readability and maintainability

* - add: ytsage_constants.py file for one place to store all constants.

- imporve: return type hint for function.

- remove: src/gui/ytsage_gui_dialogs.py file to avoid repetation
  - src/gui/dialogs is renamed to src/gui/ytsage_gui_dialogs for same naming convection. (future import will remains same)
  - use of src/gui/ytsage_gui_dialogs/__init__.py to import the dilogs modules.

- change: variable self.parent to self._parent so it does not overwrite the parent()
  - add type hint checking.

* - refactor: QMetaObject.invokeMethod to Signal
  - I encounter error with incokeMethod. Could not solve it.
  - So, Changed it to Signal to match app code language.

- implement: the ytsage_constants.py to code
  - remove: unnecessary logic
  - remove: repetitive code logic.

- update: yt-dlp logic for src\gui\ytsage_gui_dialogs\ytsage_dialogs_update:_update_binary
  - yt-dlp update logic will use `yt-dlp -U`

* refactor: remove unused imports and streamline code formatting across multiple files
This commit is contained in:
Viren Hirpara
2025-08-26 18:57:12 +05:30
committed by GitHub
parent 6652d7f00f
commit 9c13b4b61c
21 changed files with 2805 additions and 2628 deletions
+153 -124
View File
@@ -1,33 +1,39 @@
import os
import sys
import subprocess
import requests
import shutil
import tempfile
import hashlib
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from PySide6.QtGui import QIcon
from .ytsage_logging import logger
def check_7zip_installed():
import requests
from src.core.ytsage_logging import logger
from src.utils.ytsage_constants import (
FFMPEG_7Z_DOWNLOAD_URL,
FFMPEG_7Z_SHA256_URL,
FFMPEG_ZIP_DOWNLOAD_URL,
OS_NAME,
SUBPROCESS_CREATIONFLAGS,
)
def check_7zip_installed() -> bool:
"""Check if 7-Zip is installed on Windows."""
try:
subprocess.run(['7z', '--help'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0)
subprocess.run(["7z", "--help"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=SUBPROCESS_CREATIONFLAGS)
return True
except (subprocess.SubprocessError, FileNotFoundError):
return False
def download_file(url, dest_path, progress_callback=None):
def download_file(url, dest_path, progress_callback=None) -> bool:
"""Download a file from URL to destination path with progress indication."""
try:
response = requests.get(url, stream=True, timeout=30) # Added timeout
response.raise_for_status() # Check for HTTP errors
total_size = int(response.headers.get('content-length', 0))
with open(dest_path, 'wb') as f:
total_size = int(response.headers.get("content-length", 0))
with open(dest_path, "wb") as f:
if total_size == 0:
f.write(response.content)
else:
@@ -43,7 +49,8 @@ def download_file(url, dest_path, progress_callback=None):
logger.info(f"Download error: {str(e)}")
return False
def get_file_sha256(file_path):
def get_file_sha256(file_path) -> str:
"""Calculate SHA-256 hash of a file."""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
@@ -51,17 +58,18 @@ def get_file_sha256(file_path):
sha256_hash.update(chunk)
return sha256_hash.hexdigest()
def verify_sha256(file_path, expected_hash_url):
def verify_sha256(file_path, expected_hash_url) -> bool:
"""Verify file SHA-256 hash against expected hash from URL."""
try:
# Download the SHA-256 hash
response = requests.get(expected_hash_url, timeout=10)
response.raise_for_status()
expected_hash = response.text.strip().split()[0] # Get just the hash part
# Calculate actual hash
actual_hash = get_file_sha256(file_path)
# Compare hashes
if actual_hash.lower() == expected_hash.lower():
logger.info("SHA-256 verification successful!")
@@ -75,20 +83,23 @@ def verify_sha256(file_path, expected_hash_url):
logger.info(f"⚠️ SHA-256 verification error: {str(e)}")
return False
def get_ffmpeg_install_path():
"""Get the FFmpeg installation path."""
if sys.platform == 'win32':
return os.path.join(os.getenv('LOCALAPPDATA'), 'ffmpeg', 'ffmpeg-7.1.1-full_build', 'bin')
elif sys.platform == 'darwin':
paths = ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin']
for path in paths:
if os.path.exists(os.path.join(path, 'ffmpeg')):
return path
return '/usr/local/bin' # Default Homebrew path
else:
return '/usr/bin' # Standard Linux path
def get_ffmpeg_path():
def get_ffmpeg_install_path() -> Path:
"""Get the FFmpeg installation path."""
if OS_NAME == "Windows":
return Path(os.getenv("LOCALAPPDATA")) / "ffmpeg" / "ffmpeg-7.1.1-full_build" / "bin" # type: ignore
elif OS_NAME == "Darwin":
paths = ["/usr/local/bin", "/opt/homebrew/bin", "/usr/bin"]
for path in paths:
if Path(path).joinpath("ffmpeg").exists():
return Path(path)
return Path("/usr/local/bin") # Default Homebrew path
else:
return Path("/usr/bin") # Standard Linux path
def get_ffmpeg_path() -> str | Path:
"""
Get the FFmpeg executable path, either from PATH or installation directory.
Returns:
@@ -96,150 +107,159 @@ def get_ffmpeg_path():
"""
try:
# First try to find ffmpeg in PATH using 'where' on Windows or 'which' on Unix
if sys.platform == 'win32':
if OS_NAME == "Windows":
# On Windows, use 'where' command and hide console window
startupinfo = None
if hasattr(subprocess, 'STARTUPINFO'):
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = 0 # SW_HIDE
# Extra logic moved to src\utils\ytsage_constants.py
result = subprocess.run(
['where', 'ffmpeg'],
capture_output=True,
text=True,
["where", "ffmpeg"],
capture_output=True,
text=True,
check=False,
startupinfo=startupinfo
creationflags=SUBPROCESS_CREATIONFLAGS,
)
if result.returncode == 0 and result.stdout.strip():
ffmpeg_path = result.stdout.strip().split('\n')[0]
ffmpeg_path = result.stdout.strip().split("\n")[0]
return ffmpeg_path
else:
# On Unix systems, use 'which' command
result = subprocess.run(['which', 'ffmpeg'], capture_output=True, text=True, check=False)
result = subprocess.run(["which", "ffmpeg"], capture_output=True, text=True, check=False)
if result.returncode == 0 and result.stdout.strip():
ffmpeg_path = result.stdout.strip()
return ffmpeg_path
except Exception as e:
logger.error(f"Error finding ffmpeg in PATH: {e}")
# If not found in PATH, check the installation directory
ffmpeg_install_path = get_ffmpeg_install_path()
if sys.platform == 'win32':
ffmpeg_exe = os.path.join(ffmpeg_install_path, 'ffmpeg.exe')
if OS_NAME == "Windows":
ffmpeg_exe = Path(ffmpeg_install_path).joinpath("ffmpeg.exe")
else:
ffmpeg_exe = os.path.join(ffmpeg_install_path, 'ffmpeg')
if os.path.exists(ffmpeg_exe):
ffmpeg_exe = Path(ffmpeg_install_path).joinpath("ffmpeg")
if ffmpeg_exe.exists():
return ffmpeg_exe
# Return command name as fallback
return "ffmpeg"
def check_ffmpeg_installed():
def check_ffmpeg_installed() -> bool:
"""Check if FFmpeg is installed and accessible."""
try:
# First try the PATH
result = subprocess.run(['ffmpeg', '-version'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0,
timeout=5) # Added timeout
result = subprocess.run(
["ffmpeg", "-version"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
creationflags=SUBPROCESS_CREATIONFLAGS,
timeout=5,
) # Added timeout
return True
except (subprocess.SubprocessError, FileNotFoundError):
# If not in PATH, check the installation directory
ffmpeg_path = get_ffmpeg_install_path()
if sys.platform == 'win32':
ffmpeg_exe = os.path.join(ffmpeg_path, 'ffmpeg.exe')
if OS_NAME == "Windows":
ffmpeg_exe = Path(ffmpeg_path).joinpath("ffmpeg.exe")
else:
ffmpeg_exe = os.path.join(ffmpeg_path, 'ffmpeg')
if os.path.exists(ffmpeg_exe):
ffmpeg_exe = Path(ffmpeg_path).joinpath("ffmpeg")
if ffmpeg_exe.exists():
# Add to PATH if found
os.environ['PATH'] = f"{ffmpeg_path}{os.pathsep}{os.environ.get('PATH', '')}"
os.environ["PATH"] = f"{ffmpeg_path}{os.pathsep}{os.environ.get('PATH', '')}"
return True
return False
except Exception as e:
logger.info(f"FFmpeg check error: {str(e)}")
return False
def install_ffmpeg_windows():
def install_ffmpeg_windows() -> bool:
"""Install FFmpeg on Windows using 7z method primarily, with zip as fallback."""
ffmpeg_path = get_ffmpeg_install_path()
# Check if already installed
if check_ffmpeg_installed():
logger.info("FFmpeg is already installed!")
return True
try:
# Define variables - prioritize 7z version
ffmpeg_7z_url = "https://github.com/GyanD/codexffmpeg/releases/download/7.1.1/ffmpeg-7.1.1-full_build.7z"
ffmpeg_zip_url = "https://github.com/GyanD/codexffmpeg/releases/download/7.1.1/ffmpeg-7.1.1-full_build.zip"
sha256_url = "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-7.1.1-full_build.7z.sha256"
extract_dir = os.path.join(os.getenv('LOCALAPPDATA'), 'ffmpeg')
full_build_dir = os.path.join(extract_dir, 'ffmpeg-7.1.1-full_build')
bin_dir = os.path.join(full_build_dir, 'bin')
# ffmpeg variables moved to src\utils\ytsage_constants.py
extract_dir = Path(os.getenv("LOCALAPPDATA")) / "ffmpeg" # type: ignore
full_build_dir = extract_dir / "ffmpeg-7.1.1-full_build"
bin_dir = full_build_dir / "bin"
# Create extraction directory if it doesn't exist
os.makedirs(extract_dir, exist_ok=True)
extract_dir.mkdir(exist_ok=True)
# Try 7z method first (smaller size)
use_7zip = check_7zip_installed()
if use_7zip:
logger.info("Using 7-Zip method (smaller download size)...")
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.7z').name
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".7z").name
# Download 7z file
if not download_file(ffmpeg_7z_url, temp_file,
progress_callback=lambda msg: logger.debug(msg)):
if not download_file(
FFMPEG_7Z_DOWNLOAD_URL,
temp_file,
progress_callback=lambda msg: logger.debug(msg),
):
logger.error("Failed to download 7z file, trying zip fallback...")
use_7zip = False
else:
# Verify SHA-256 hash for 7z file
if verify_sha256(temp_file, sha256_url):
if verify_sha256(temp_file, FFMPEG_7Z_SHA256_URL):
logger.info("Extracting FFmpeg components from 7z archive...")
try:
subprocess.run(['7z', 'x', temp_file, f'-o{extract_dir}', '-y'],
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0,
timeout=300) # 5-minute timeout
subprocess.run(
["7z", "x", temp_file, f"-o{extract_dir}", "-y"],
creationflags=SUBPROCESS_CREATIONFLAGS,
timeout=300,
) # 5-minute timeout
except Exception as e:
logger.error(f"7z extraction failed: {str(e)}, trying zip fallback...")
use_7zip = False
else:
logger.error("SHA-256 verification failed for 7z file, trying zip fallback...")
use_7zip = False
# Fallback to zip method if 7z failed or not available
if not use_7zip:
logger.info("Using ZIP method as fallback...")
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.zip').name
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip").name
# Download zip file
if not download_file(ffmpeg_zip_url, temp_file,
progress_callback=lambda msg: logger.debug(msg)):
if not download_file(
FFMPEG_ZIP_DOWNLOAD_URL,
temp_file,
progress_callback=lambda msg: logger.debug(msg),
):
raise Exception("Failed to download FFmpeg (both 7z and zip methods failed)")
logger.info("Extracting FFmpeg components from zip archive...")
try:
import zipfile
with zipfile.ZipFile(temp_file, 'r') as zip_ref:
with zipfile.ZipFile(temp_file, "r") as zip_ref:
zip_ref.extractall(extract_dir)
except Exception as e:
raise Exception(f"Extraction failed: {str(e)}")
logger.info("Configuring system paths...")
# Add to System Path
user_path = os.environ.get('PATH', '')
if bin_dir not in user_path:
subprocess.run(['setx', 'PATH', f"{user_path};{bin_dir}"],
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0)
os.environ['PATH'] = f"{user_path};{bin_dir}"
user_path = os.environ.get("PATH", "")
if str(bin_dir) not in user_path.split(os.pathsep):
subprocess.run(
["setx", "PATH", f"{user_path};{bin_dir}"],
creationflags=SUBPROCESS_CREATIONFLAGS,
)
os.environ["PATH"] = f"{user_path};{bin_dir}"
# Clean up
try:
os.unlink(temp_file)
Path(temp_file).unlink(missing_ok=True)
except Exception:
pass # Ignore cleanup errors
@@ -254,16 +274,19 @@ def install_ffmpeg_windows():
logger.error(f"Error installing FFmpeg: {str(e)}")
return False
def install_ffmpeg_macos():
def install_ffmpeg_macos() -> bool:
"""Install FFmpeg on macOS using Homebrew."""
try:
# Check if Homebrew is installed
try:
subprocess.run(['brew', '--version'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
timeout=5)
subprocess.run(
["brew", "--version"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
timeout=5,
)
except (subprocess.SubprocessError, FileNotFoundError):
logger.info("Installing Homebrew...")
brew_install_cmd = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
@@ -271,56 +294,62 @@ def install_ffmpeg_macos():
# Install FFmpeg
logger.info("Installing FFmpeg...")
subprocess.run(['brew', 'install', 'ffmpeg'], check=True, timeout=300)
subprocess.run(["brew", "install", "ffmpeg"], check=True, timeout=300)
# Verify installation
if not check_ffmpeg_installed():
raise Exception("FFmpeg installation verification failed")
return True
except Exception as e:
logger.error(f"Error installing FFmpeg: {str(e)}")
return False
def install_ffmpeg_linux():
def install_ffmpeg_linux() -> bool:
"""Install FFmpeg on Linux using appropriate package manager."""
try:
# Detect the package manager
if shutil.which('apt'):
if shutil.which("apt"):
# Debian/Ubuntu
subprocess.run(['sudo', 'apt', 'update'], check=True, timeout=60)
subprocess.run(['sudo', 'apt', 'install', '-y', 'ffmpeg'], check=True, timeout=300)
elif shutil.which('dnf'):
subprocess.run(["sudo", "apt", "update"], check=True, timeout=60)
subprocess.run(["sudo", "apt", "install", "-y", "ffmpeg"], check=True, timeout=300)
elif shutil.which("dnf"):
# Fedora
subprocess.run(['sudo', 'dnf', 'install', '-y', 'ffmpeg'], check=True, timeout=300)
elif shutil.which('pacman'):
subprocess.run(["sudo", "dnf", "install", "-y", "ffmpeg"], check=True, timeout=300)
elif shutil.which("pacman"):
# Arch Linux
subprocess.run(['sudo', 'pacman', '-S', '--noconfirm', 'ffmpeg'], check=True, timeout=300)
elif shutil.which('snap'):
subprocess.run(
["sudo", "pacman", "-S", "--noconfirm", "ffmpeg"],
check=True,
timeout=300,
)
elif shutil.which("snap"):
# Universal snap package
subprocess.run(['sudo', 'snap', 'install', 'ffmpeg'], check=True, timeout=300)
subprocess.run(["sudo", "snap", "install", "ffmpeg"], check=True, timeout=300)
else:
raise Exception("No supported package manager found")
# Verify installation
if not check_ffmpeg_installed():
raise Exception("FFmpeg installation verification failed")
return True
except Exception as e:
logger.error(f"Error installing FFmpeg: {str(e)}")
return False
def auto_install_ffmpeg():
def auto_install_ffmpeg() -> bool:
"""Automatically install FFmpeg based on the operating system."""
if sys.platform == 'win32':
if OS_NAME == "Windows":
return install_ffmpeg_windows()
elif sys.platform == 'darwin':
elif OS_NAME == "Darwin":
return install_ffmpeg_macos()
elif sys.platform.startswith('linux'):
elif OS_NAME == "Linux":
return install_ffmpeg_linux()
else:
logger.info(f"Unsupported operating system: {sys.platform}")
return False
logger.info(f"Unsupported operating system: {OS_NAME}")
return False