import logging import json import os import platform import re import shutil import socket import string import subprocess import sys from datetime import datetime from typing import Optional, List, Dict, Any logger = logging.getLogger() def launch_url(url: str) -> None: logger = logging.getLogger(__name__) if shutil.which('xdg-open'): opener = 'xdg-open' elif shutil.which('open'): opener = 'open' elif shutil.which('cmd'): opener = 'start' else: error_message = f"No valid launchers found to launch URL: {url}" logger.error(error_message) raise OSError(error_message) try: if opener == 'start': # For Windows, use 'cmd /c start' subprocess.run(['cmd', '/c', 'start', url], shell=False) else: subprocess.run([opener, url]) except Exception as e: logger.error(f"Failed to launch URL: {url}. Error: {e}") def file_exists_in_mounts(filepath: str) -> Optional[str]: """ Check if a file exists in any mounted directory. It searches for the file in common mount points like '/Volumes', '/mnt', and '/media'. Returns the path to the file in the mount if found, otherwise returns None. Example: Before: filepath = '/path/to/file.txt' After: '/Volumes/ExternalDrive/path/to/file.txt' """ def get_path_components(path): path = os.path.normpath(path) components = [] while True: path, comp = os.path.split(path) if comp: components.append(comp) else: if path: components.append(path) break components.reverse() return components # Get path components of the directory of the file path_components = get_path_components(os.path.dirname(filepath).strip('/')) # Iterate over possible root paths - this may need to be rethought for Windows support for root in ['/Volumes', '/mnt', '/media']: if os.path.exists(root): # Iterate over mounts in the root path for mount in os.listdir(root): # Since we don't know the home directory, we iterate until we find matching parts of the path matching_components = [s for s in path_components if s in mount] for component in matching_components: possible_mount_path = os.path.join(root, mount, filepath.split(component)[-1].lstrip('/')) if os.path.exists(possible_mount_path): return possible_mount_path def get_time_elapsed(start_time: Optional[datetime] = None, end_time: Optional[datetime] = None) -> str: def strfdelta(tdelta, fmt='%H:%M:%S'): days = tdelta.days hours, rem = divmod(tdelta.seconds, 3600) minutes, seconds = divmod(rem, 60) # Using f-strings for formatting formatted_str = fmt.replace('%D', f'{days}') formatted_str = formatted_str.replace('%H', f'{hours:02d}') formatted_str = formatted_str.replace('%M', f'{minutes:02d}') formatted_str = formatted_str.replace('%S', f'{seconds:02d}') return formatted_str # calculate elapsed time elapsed_time = None if start_time: if end_time: elapsed_time = end_time - start_time else: elapsed_time = datetime.now() - start_time elapsed_time_string = strfdelta(elapsed_time) if elapsed_time else None return elapsed_time_string def get_file_size_human(file_path: str) -> str: size_in_bytes = os.path.getsize(file_path) # Convert size to a human-readable format if size_in_bytes < 1024: return f"{size_in_bytes} B" elif size_in_bytes < 1024 ** 2: return f"{size_in_bytes / 1024:.2f} KB" elif size_in_bytes < 1024 ** 3: return f"{size_in_bytes / 1024 ** 2:.2f} MB" elif size_in_bytes < 1024 ** 4: return f"{size_in_bytes / 1024 ** 3:.2f} GB" else: return f"{size_in_bytes / 1024 ** 4:.2f} TB" def current_system_os() -> str: return platform.system().lower().replace('darwin', 'macos') def current_system_os_version() -> str: return platform.release() def current_system_cpu() -> str: return platform.machine().lower().replace('amd64', 'x64') def current_system_cpu_brand() -> str: """Fast cross-platform CPU brand string""" if sys.platform.startswith('darwin'): # macOS try: return subprocess.check_output(['sysctl', '-n', 'machdep.cpu.brand_string']).decode().strip() except Exception: pass elif sys.platform.startswith('win'): # Windows from winreg import HKEY_LOCAL_MACHINE, OpenKey, QueryValueEx try: # Open the registry key where Windows stores the CPU name key = OpenKey(HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System\CentralProcessor\0") # The value name is "ProcessorNameString" value, _ = QueryValueEx(key, "ProcessorNameString") return value.strip() # Usually perfect, with full marketing name except Exception: # Fallback: sometimes the key is under a different index, try 1 try: key = OpenKey(HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System\CentralProcessor\1") value, _ = QueryValueEx(key, "ProcessorNameString") return value.strip() except Exception: return "Unknown CPU" elif sys.platform.startswith('linux'): try: with open('/proc/cpuinfo') as f: for line in f: if line.startswith('model name'): return line.split(':', 1)[1].strip() except Exception: pass # Ultimate fallback return platform.processor() or 'Unknown CPU' def resources_dir() -> str: return os.path.join(os.path.dirname(__file__), '..', '..', 'resources') def copy_directory_contents(src_dir: str, dst_dir: str) -> None: for item in os.listdir(src_dir): src_path = os.path.join(src_dir, item) dst_path = os.path.join(dst_dir, item) if os.path.isdir(src_path): shutil.copytree(src_path, dst_path) else: shutil.copy2(src_path, dst_path) def check_for_updates(repo_name: str, repo_owner: str, app_name: str, current_version: str) -> Optional[Dict[str, Any]]: def get_github_releases(owner, repo): import requests url = f"https://api.github.com/repos/{owner}/{repo}/releases" try: response = requests.get(url, timeout=3) response.raise_for_status() releases = response.json() return releases except Exception as e: logger.error(f"Error checking for updates: {e}") return [] releases = get_github_releases(repo_owner, repo_name) if not releases: return None latest_version = releases[0] latest_version_tag = latest_version['tag_name'] from packaging import version if version.parse(latest_version_tag) > version.parse(current_version): logger.info(f"Newer version of {app_name} available. " f"Latest: {latest_version_tag}, Current: {current_version}") return latest_version return None def is_localhost(comparison_hostname: str) -> bool: return comparison_hostname in ['localhost', '127.0.0.1', socket.gethostname()] def num_to_alphanumeric(num: int) -> str: return string.ascii_letters[num % 26] + str(num // 26) def get_gpu_info() -> List[Dict[str, Any]]: """Cross-platform GPU information retrieval""" def get_windows_gpu_info(): """Get GPU info on Windows""" try: result = subprocess.run( ['wmic', 'path', 'win32_videocontroller', 'get', 'name,AdapterRAM', '/format:list'], capture_output=True, text=True, timeout=5 ) # Virtual adapters to exclude virtual_adapters = [ 'virtual', 'rdp', 'hyper-v', 'microsoft basic', 'basic display', 'vga compatible', 'dummy', 'nvfbc', 'nvencode' ] gpus = [] current_gpu = None for line in result.stdout.strip().split('\n'): line = line.strip() if not line: continue if line.startswith('Name='): if current_gpu and current_gpu.get('name'): gpus.append(current_gpu) gpu_name = line.replace('Name=', '').strip() # Skip virtual adapters if any(virtual in gpu_name.lower() for virtual in virtual_adapters): current_gpu = None else: current_gpu = {'name': gpu_name, 'memory': 'Integrated'} elif line.startswith('AdapterRAM=') and current_gpu: vram_bytes_str = line.replace('AdapterRAM=', '').strip() if vram_bytes_str and vram_bytes_str != '0': try: vram_gb = int(vram_bytes_str) / (1024**3) current_gpu['memory'] = round(vram_gb, 2) except: pass if current_gpu and current_gpu.get('name'): gpus.append(current_gpu) return gpus if gpus else [{'name': 'Unknown GPU', 'memory': 'Unknown'}] except Exception as e: logger.error(f"Failed to get Windows GPU info: {e}") return [{'name': 'Unknown GPU', 'memory': 'Unknown'}] def get_macos_gpu_info(): """Get GPU info on macOS (works with Apple Silicon)""" try: if current_system_cpu() == "arm64": # don't bother with system_profiler with Apple ARM - we know its integrated return [{'name': current_system_cpu_brand(), 'memory': 'Integrated'}] result = subprocess.run(['system_profiler', 'SPDisplaysDataType', '-detailLevel', 'mini', '-json'], capture_output=True, text=True, timeout=5) data = json.loads(result.stdout) gpus = [] displays = data.get('SPDisplaysDataType', []) for display in displays: if 'sppci_model' in display: gpus.append({ 'name': display.get('sppci_model', 'Unknown GPU'), 'memory': display.get('sppci_vram', 'Integrated'), }) return gpus if gpus else [{'name': 'Apple GPU', 'memory': 'Integrated'}] except Exception as e: print(f"Failed to get macOS GPU info: {e}") return [{'name': 'Unknown GPU', 'memory': 'Unknown'}] def get_linux_gpu_info(): gpus = [] try: # Run plain lspci and filter for GPU-related lines output = subprocess.check_output( ["lspci"], universal_newlines=True, stderr=subprocess.DEVNULL ) for line in output.splitlines(): if any(keyword in line.lower() for keyword in ["vga", "3d", "display"]): # Extract the part after the colon (vendor + model) if ":" in line: name_part = line.split(":", 1)[1].strip() # Clean up common extras like (rev xx) or (prog-if ...) name = name_part.split("(")[0].split("controller:")[-1].strip() vendor = "Unknown" if "nvidia" in name.lower(): vendor = "NVIDIA" elif "amd" in name.lower() or "ati" in name.lower(): vendor = "AMD" elif "intel" in name.lower(): vendor = "Intel" gpus.append({ "name": name, "vendor": vendor, "memory": "Unknown" }) except FileNotFoundError: print("lspci not found. Install pciutils: sudo apt install pciutils") return [] except Exception as e: print(f"Error running lspci: {e}") return [] return gpus system = platform.system() if system == 'Darwin': # macOS return get_macos_gpu_info() elif system == 'Windows': return get_windows_gpu_info() else: # Assume Linux or other return get_linux_gpu_info() COMMON_RESOLUTIONS = { # SD "SD_480p": (640, 480), "NTSC_DVD": (720, 480), "PAL_DVD": (720, 576), # HD "HD_720p": (1280, 720), "HD_900p": (1600, 900), "HD_1080p": (1920, 1080), # Cinema / Film "2K_DCI": (2048, 1080), "4K_DCI": (4096, 2160), # UHD / Consumer "UHD_4K": (3840, 2160), "UHD_5K": (5120, 2880), "UHD_8K": (7680, 4320), # Ultrawide / Aspect Variants "UW_1080p": (2560, 1080), "UW_1440p": (3440, 1440), "UW_5K": (5120, 2160), # Mobile / Social "VERTICAL_1080x1920": (1080, 1920), "SQUARE_1080": (1080, 1080), # Classic / Legacy "VGA": (640, 480), "SVGA": (800, 600), "XGA": (1024, 768), "WXGA": (1280, 800), } COMMON_FRAME_RATES = { "23.976 (NTSC Film)": 23.976, "24 (Cinema)": 24.0, "25 (PAL)": 25.0, "29.97 (NTSC)": 29.97, "30": 30.0, "48 (HFR Film)": 48.0, "50 (PAL HFR)": 50.0, "59.94": 59.94, "60": 60.0, "72": 72.0, "90 (VR)": 90.0, "120": 120.0, "144 (Gaming)": 144.0, "240 (HFR)": 240.0, }