From 04d8302d6c1f5807a1b6043fec2e6e82f39df2cf Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sat, 20 Sep 2025 20:10:16 +0500 Subject: [PATCH] chore(logs): start translate Signed-off-by: Boris Yumankulov --- portprotonqt/cli.py | 6 +- portprotonqt/context_menu_manager.py | 42 +++--- portprotonqt/custom_widgets.py | 46 +++--- portprotonqt/steam_api.py | 214 +++++++++++++-------------- 4 files changed, 150 insertions(+), 158 deletions(-) diff --git a/portprotonqt/cli.py b/portprotonqt/cli.py index 4898100..44a93dc 100644 --- a/portprotonqt/cli.py +++ b/portprotonqt/cli.py @@ -1,17 +1,15 @@ import argparse -from portprotonqt.logger import get_logger -logger = get_logger(__name__) def parse_args(): """ - Парсит аргументы командной строки. + Parses command-line arguments. """ parser = argparse.ArgumentParser(description="PortProtonQt CLI") parser.add_argument( "--fullscreen", action="store_true", - help="Запустить приложение в полноэкранном режиме и сохранить эту настройку" + help="Launch the application in fullscreen mode and save this setting" ) parser.add_argument( "--debug-level", diff --git a/portprotonqt/context_menu_manager.py b/portprotonqt/context_menu_manager.py index 64d1fdc..97fb6a8 100644 --- a/portprotonqt/context_menu_manager.py +++ b/portprotonqt/context_menu_manager.py @@ -62,7 +62,7 @@ class ContextMenuManager: self.parent.statusBar().showMessage, Qt.ConnectionType.QueuedConnection ) - logger.debug("Connected show_status_message signal to statusBar") + logger.debug("Connected show_status_message signal to status bar") self.signals.show_warning_dialog.connect( self._show_warning_dialog, Qt.ConnectionType.QueuedConnection @@ -74,28 +74,28 @@ class ContextMenuManager: def _show_warning_dialog(self, title: str, message: str): """Show a warning dialog in the main thread.""" - logger.debug("Showing warning dialog: %s - %s", title, message) + logger.debug("Displaying warning dialog: %s - %s", title, message) QMessageBox.warning(self.parent, title, message) def _show_info_dialog(self, title: str, message: str): """Show an info dialog in the main thread.""" - logger.debug("Showing info dialog: %s - %s", title, message) + logger.debug("Displaying info dialog: %s - %s", title, message) QMessageBox.information(self.parent, title, message) def _show_status_message(self, message: str, timeout: int = 3000): """Show a status message on the status bar if available.""" if self.parent.statusBar(): self.parent.statusBar().showMessage(message, timeout) - logger.debug("Direct status message: %s", message) + logger.debug("Displayed status message: %s", message) else: - logger.warning("Status bar not available for message: %s", message) + logger.warning("Status bar unavailable for message: %s", message) def _check_portproton(self): """Check if PortProton is available.""" if self.portproton_location is None: self.signals.show_warning_dialog.emit( _("Error"), - _("PortProton is not found") + _("PortProton directory not found") ) return False return True @@ -119,7 +119,7 @@ class ContextMenuManager: installed_games = orjson.loads(f.read()) return app_name in installed_games except (OSError, orjson.JSONDecodeError) as e: - logger.error("Failed to read installed.json: %s", e) + logger.error("Error reading installed.json: %s", e) return False def _is_game_running(self, game_card) -> bool: @@ -155,7 +155,7 @@ class ContextMenuManager: try: item = file_explorer.file_list.itemAt(pos) if not item: - logger.debug("No item selected at position %s", pos) + logger.debug("No folder selected at position %s", pos) return selected = item.text() if not selected.endswith("/"): @@ -202,7 +202,7 @@ class ContextMenuManager: global_pos = file_explorer.file_list.mapToGlobal(pos) menu.exec(global_pos) except Exception as e: - logger.error("Error showing folder context menu: %s", e) + logger.error("Error displaying folder context menu: %s", e) def toggle_favorite_folder(self, file_explorer, folder_path, add): """Adds or removes a folder from favorites.""" @@ -211,12 +211,12 @@ class ContextMenuManager: if folder_path not in favorite_folders: favorite_folders.append(folder_path) save_favorite_folders(favorite_folders) - logger.info(f"Folder added to favorites: {folder_path}") + logger.info("Added folder to favorites: %s", folder_path) else: if folder_path in favorite_folders: favorite_folders.remove(folder_path) save_favorite_folders(favorite_folders) - logger.info(f"Folder removed from favorites: {folder_path}") + logger.info("Removed folder from favorites: %s", folder_path) file_explorer.update_drives_list() def _get_safe_icon(self, icon_name: str) -> QIcon: @@ -607,10 +607,10 @@ class ContextMenuManager: exe_path = get_egs_executable(app_name, self.legendary_config_path) if exe_path and os.path.exists(exe_path): if not generate_thumbnail(exe_path, icon_path, size=128): - logger.error(f"Failed to generate thumbnail from exe: {exe_path}") + logger.error("Failed to generate thumbnail for EGS game: %s", exe_path) icon_path = "" else: - logger.error(f"No executable found for EGS game: {app_name}") + logger.error("No executable found for EGS game: %s", app_name) icon_path = "" egs_desktop_dir = os.path.join(self.portproton_location, "egs_desktops") @@ -750,7 +750,7 @@ Icon={icon_path} if not exec_line: self.signals.show_warning_dialog.emit( _("Error"), - _("No executable command in .desktop file for '{game_name}'").format(game_name=game_name) + _("No executable command found in .desktop file for '{game_name}'").format(game_name=game_name) ) return None else: @@ -762,7 +762,7 @@ Icon={icon_path} except Exception as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to read .desktop file: {error}").format(error=str(e)) + _("Error reading .desktop file: {error}").format(error=str(e)) ) return None else: @@ -784,7 +784,7 @@ Icon={icon_path} try: entry_exec_split = shlex.split(exec_line) if not entry_exec_split: - logger.debug("Invalid executable command for '%s': %s", game_name, exec_line) + logger.debug("Invalid executable command for game '%s': %s", game_name, exec_line) return None if entry_exec_split[0] == "env" and len(entry_exec_split) >= 3: exe_path = entry_exec_split[2] @@ -793,11 +793,11 @@ Icon={icon_path} else: exe_path = entry_exec_split[-1] if not exe_path or not os.path.exists(exe_path): - logger.debug("Executable not found for '%s': %s", game_name, exe_path or "None") + logger.debug("Executable not found for game '%s': %s", game_name, exe_path or "None") return None return exe_path except Exception as e: - logger.debug("Failed to parse executable for '%s': %s", game_name, e) + logger.debug("Error parsing executable for game '%s': %s", game_name, e) return None def _remove_file(self, file_path, error_message, success_message, game_name, location=""): @@ -936,7 +936,7 @@ Icon={icon_path} icon_path = os.path.join(self.portproton_location, "data", "img", f"{game_name}.png") if not os.path.exists(icon_path): if not generate_thumbnail(exe_path, icon_path, size=128): - logger.error(f"Failed to generate thumbnail for {exe_path}") + logger.error("Failed to generate thumbnail for game: %s", exe_path) desktop_dir = subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode('utf-8').strip() os.makedirs(desktop_dir, exist_ok=True) @@ -1072,7 +1072,7 @@ Icon={icon_path} exe_path = self._parse_exe_path(exec_line, game_name) if not exe_path: return - logger.debug("Adding '%s' to Steam", game_name) + logger.debug("Adding game '%s' to Steam", game_name) try: success, message = add_to_steam(game_name, exec_line, cover_path) self.signals.show_info_dialog.emit( @@ -1115,7 +1115,7 @@ Icon={icon_path} exe_path = self._parse_exe_path(exec_line, game_name) if not exe_path: return - logger.debug("Removing non-EGS game '%s' from Steam", game_name) + logger.debug("Removing game '%s' from Steam", game_name) try: success, message = remove_from_steam(game_name, exec_line) self.signals.show_info_dialog.emit( diff --git a/portprotonqt/custom_widgets.py b/portprotonqt/custom_widgets.py index b1c697f..d8841ef 100644 --- a/portprotonqt/custom_widgets.py +++ b/portprotonqt/custom_widgets.py @@ -5,29 +5,29 @@ from PySide6.QtGui import QFont, QFontMetrics, QPainter def compute_layout(nat_sizes, rect_width, spacing, max_scale): """ - Вычисляет расположение элементов с учетом отступов и возможного увеличения карточек. - nat_sizes: массив (N, 2) с натуральными размерами элементов (ширина, высота). - rect_width: доступная ширина контейнера. - spacing: отступ между элементами (горизонтальный и вертикальный). - max_scale: максимальный коэффициент масштабирования (например, 1.0). + Computes the layout of elements considering spacing and potential scaling of cards. + nat_sizes: Array (N, 2) with natural sizes of elements (width, height). + rect_width: Available container width. + spacing: Spacing between elements (horizontal and vertical). + max_scale: Maximum scaling factor (e.g., 1.0). - Возвращает: - result: массив (N, 4), где каждая строка содержит [x, y, new_width, new_height]. - total_height: итоговая высота всех рядов. + Returns: + result: Array (N, 4), where each row contains [x, y, new_width, new_height]. + total_height: Total height of all rows. """ N = nat_sizes.shape[0] result = np.zeros((N, 4), dtype=np.int32) y = 0 i = 0 - min_margin = 20 # Минимальный отступ по краям + min_margin = 20 # Minimum margin on edges - # Определяем максимальное количество элементов в ряду и общий масштаб + # Determine the maximum number of items per row and overall scale max_items_per_row = 0 global_scale = 1.0 - max_row_x_start = min_margin # Начальная позиция x самого длинного ряда + max_row_x_start = min_margin # Starting x position of the widest row temp_i = 0 - # Первый проход: находим максимальное количество элементов в ряду + # First pass: Find the maximum number of items in a row while temp_i < N: sum_width = 0 count = 0 @@ -42,23 +42,23 @@ def compute_layout(nat_sizes, rect_width, spacing, max_scale): if count > max_items_per_row: max_items_per_row = count - # Вычисляем масштаб для самого заполненного ряда + # Calculate scale for the most populated row available_width = rect_width - spacing * (count - 1) - 2 * min_margin desired_scale = available_width / sum_width if sum_width > 0 else 1.0 global_scale = desired_scale if desired_scale < max_scale else max_scale - # Сохраняем начальную позицию x для самого длинного ряда + # Store starting x position for the widest row scaled_row_width = int(sum_width * global_scale) + spacing * (count - 1) max_row_x_start = max(min_margin, (rect_width - scaled_row_width) // 2) temp_i = temp_j - # Второй проход: размещаем элементы + # Second pass: Place elements while i < N: sum_width = 0 row_max_height = 0 count = 0 j = i - # Подбираем количество элементов для текущего ряда + # Determine the number of items for the current row while j < N: w = nat_sizes[j, 0] if count > 0 and (sum_width + spacing + w) > rect_width - 2 * min_margin: @@ -70,16 +70,16 @@ def compute_layout(nat_sizes, rect_width, spacing, max_scale): row_max_height = h j += 1 - # Используем глобальный масштаб для всех рядов + # Use global scale for all rows scale = global_scale scaled_row_width = int(sum_width * scale) + spacing * (count - 1) - # Определяем начальную координату x + # Determine starting x coordinate if count == max_items_per_row: - # Центрируем полный ряд + # Center the full row x = max(min_margin, (rect_width - scaled_row_width) // 2) else: - # Выравниваем неполный ряд по левому краю, совмещая с началом самого длинного ряда + # Align incomplete row to the left, matching the widest row's start x = max_row_x_start for k in range(i, j): @@ -99,9 +99,9 @@ class FlowLayout(QLayout): def __init__(self, parent=None): super().__init__(parent) self.itemList = [] - self.setContentsMargins(20, 20, 20, 20) # Отступы по краям - self._spacing = 20 # Отступ для анимации и предотвращения перекрытий - self._max_scale = 1.0 # Отключено масштабирование в layout + self.setContentsMargins(20, 20, 20, 20) # Margins around the layout + self._spacing = 20 # Spacing for animation and overlap prevention + self._max_scale = 1.0 # Scaling disabled in layout def addItem(self, item: QLayoutItem) -> None: self.itemList.append(item) diff --git a/portprotonqt/steam_api.py b/portprotonqt/steam_api.py index c92fc24..67694c6 100644 --- a/portprotonqt/steam_api.py +++ b/portprotonqt/steam_api.py @@ -45,14 +45,14 @@ def safe_vdf_load(path: str | Path) -> dict: def decode_text(text: str) -> str: """ - Декодирует HTML-сущности в строке. - Например, "&quot;" преобразуется в '"'. - Остальные символы и HTML-теги остаются без изменений. + Decodes HTML entities in a string. + For example, "&quot;" is converted to '"'. + Other characters and HTML tags remain unchanged. """ return html.unescape(text) def get_cache_dir(): - """Возвращает путь к каталогу кэша, создаёт его при необходимости.""" + """Returns the path to the cache directory, creating it if necessary.""" xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")) cache_dir = os.path.join(xdg_cache_home, "PortProtonQt") os.makedirs(cache_dir, exist_ok=True) @@ -65,7 +65,7 @@ STEAM_DATA_DIRS = ( ) def get_steam_home(): - """Возвращает путь к директории Steam, используя список возможных директорий.""" + """Returns the path to the Steam directory using a list of possible directories.""" for dir_path in STEAM_DATA_DIRS: expanded_path = Path(os.path.expanduser(dir_path)) if expanded_path.exists(): @@ -73,7 +73,7 @@ def get_steam_home(): return None def get_last_steam_user(steam_home: Path) -> dict | None: - """Возвращает данные последнего пользователя Steam из loginusers.vdf.""" + """Returns data for the last Steam user from loginusers.vdf.""" loginusers_path = steam_home / "config/loginusers.vdf" data = safe_vdf_load(loginusers_path) if not data: @@ -84,20 +84,20 @@ def get_last_steam_user(steam_home: Path) -> dict | None: try: return {'SteamID': int(user_id)} except ValueError: - logger.error(f"Неверный формат SteamID: {user_id}") + logger.error(f"Invalid SteamID format: {user_id}") return None - logger.info("Не найден пользователь с MostRecent=1") + logger.info("No user found with MostRecent=1") return None def convert_steam_id(steam_id: int) -> int: """ - Преобразует знаковое 32-битное целое число в беззнаковое 32-битное целое число. - Использует побитовое И с 0xFFFFFFFF, что корректно обрабатывает отрицательные значения. + Converts a signed 32-bit integer to an unsigned 32-bit integer. + Uses bitwise AND with 0xFFFFFFFF to correctly handle negative values. """ return steam_id & 0xFFFFFFFF def get_steam_libs(steam_dir: Path) -> set[Path]: - """Возвращает набор директорий Steam libraryfolders.""" + """Returns a set of Steam library folders.""" libs = set() libs_vdf = steam_dir / "steamapps/libraryfolders.vdf" data = safe_vdf_load(libs_vdf) @@ -113,7 +113,7 @@ def get_steam_libs(steam_dir: Path) -> set[Path]: return libs def get_playtime_data(steam_home: Path | None = None) -> dict[int, tuple[int, int]]: - """Возвращает данные о времени игры для последнего пользователя.""" + """Returns playtime data for the last user.""" play_data: dict[int, tuple[int, int]] = {} if steam_home is None: steam_home = get_steam_home() @@ -133,14 +133,14 @@ def get_playtime_data(steam_home: Path | None = None) -> dict[int, tuple[int, in return play_data if not last_user: - logger.info("Не удалось определить последнего пользователя Steam") + logger.info("Could not identify the last Steam user") return play_data user_id = last_user['SteamID'] unsigned_id = convert_steam_id(user_id) user_dir = userdata_dir / str(unsigned_id) if not user_dir.exists(): - logger.info(f"Директория пользователя {unsigned_id} не найдена") + logger.info(f"User directory {unsigned_id} not found") return play_data localconfig = user_dir / "config/localconfig.vdf" @@ -154,11 +154,11 @@ def get_playtime_data(steam_home: Path | None = None) -> dict[int, tuple[int, in playtime = int(info.get('Playtime', 0)) play_data[appid] = (last_played, playtime) except ValueError: - logger.warning(f"Некорректные данные playtime для app {appid_str}") + logger.warning(f"Invalid playtime data for app {appid_str}") return play_data def get_steam_installed_games() -> list[tuple[str, int, int, int]]: - """Возвращает список установленных Steam игр в формате (name, appid, last_played, playtime_sec).""" + """Returns a list of installed Steam games in the format (name, appid, last_played, playtime_sec).""" games: list[tuple[str, int, int, int]] = [] steam_home = get_steam_home() if steam_home is None or not steam_home.exists(): @@ -187,13 +187,13 @@ def get_steam_installed_games() -> list[tuple[str, int, int, int]]: def normalize_name(s): """ - Приведение строки к нормальному виду: - - перевод в нижний регистр, - - удаление символов ™ и ®, - - замена разделителей (-, :, ,) на пробел, - - удаление лишних пробелов, - - удаление суффиксов 'bin' или 'app' в конце строки, - - удаление ключевых слов типа 'ultimate', 'edition' и т.п. + Normalizes a string by: + - converting to lowercase, + - removing ™ and ® symbols, + - replacing separators (-, :, ,) with spaces, + - removing extra spaces, + - removing 'bin' or 'app' suffixes, + - removing keywords like 'ultimate', 'edition', etc. """ s = s.lower() for ch in ["™", "®"]: @@ -211,12 +211,12 @@ def normalize_name(s): def is_valid_candidate(candidate): """ - Проверяет, содержит ли кандидат запрещённые подстроки: + Checks if a candidate contains forbidden substrings: - win32 - win64 - gamelauncher - Для проверки дополнительно используется строка без пробелов. - Возвращает True, если кандидат допустим, иначе False. + Additionally checks the string without spaces. + Returns True if the candidate is valid, otherwise False. """ normalized_candidate = normalize_name(candidate) normalized_no_space = normalized_candidate.replace(" ", "") @@ -228,7 +228,7 @@ def is_valid_candidate(candidate): def filter_candidates(candidates): """ - Фильтрует список кандидатов, отбрасывая недопустимые. + Filters a list of candidates, discarding invalid ones. """ valid = [] dropped = [] @@ -238,18 +238,18 @@ def filter_candidates(candidates): else: dropped.append(cand) if dropped: - logger.info("Отбрасываю кандидатов: %s", dropped) + logger.info("Discarding candidates: %s", dropped) return valid def remove_duplicates(candidates): """ - Удаляет дубликаты из списка, сохраняя порядок. + Removes duplicates from a list while preserving order. """ return list(dict.fromkeys(candidates)) @functools.lru_cache(maxsize=256) def get_exiftool_data(game_exe): - """Получает метаданные через exiftool""" + """Retrieves metadata using exiftool.""" try: proc = subprocess.run( ["exiftool", "-j", game_exe], @@ -258,12 +258,12 @@ def get_exiftool_data(game_exe): check=False ) if proc.returncode != 0: - logger.error(f"exiftool error for {game_exe}: {proc.stderr.strip()}") + logger.error(f"exiftool failed for {game_exe}: {proc.stderr.strip()}") return {} meta_data_list = orjson.loads(proc.stdout.encode("utf-8")) return meta_data_list[0] if meta_data_list else {} except Exception as e: - logger.error(f"An unexpected error occurred in get_exiftool_data for {game_exe}: {e}") + logger.error(f"Unexpected error in get_exiftool_data for {game_exe}: {e}") return {} def delete_cached_app_files(cache_dir: str, pattern: str): @@ -305,14 +305,14 @@ def load_steam_apps_async(callback: Callable[[list], None]): f.write(orjson.dumps(data)) if os.path.exists(cache_tar): os.remove(cache_tar) - logger.info("Archive %s deleted after extraction", cache_tar) + logger.info("Deleted archive: %s", cache_tar) # Delete all cached app detail files (steam_app_*.json) delete_cached_app_files(cache_dir, "steam_app_*.json") steam_apps = data if isinstance(data, list) else [] logger.info("Loaded %d apps from archive", len(steam_apps)) callback(steam_apps) except Exception as e: - logger.error("Error extracting Steam apps archive: %s", e) + logger.error("Failed to extract Steam apps archive: %s", e) callback([]) if os.path.exists(cache_json) and (time.time() - os.path.getmtime(cache_json) < CACHE_DURATION): @@ -322,18 +322,18 @@ def load_steam_apps_async(callback: Callable[[list], None]): data = orjson.loads(f.read()) # Validate JSON structure if not isinstance(data, list): - logger.error("Cached JSON %s has invalid format (not a list), re-downloading", cache_json) + logger.error("Invalid JSON format in %s (not a list), re-downloading", cache_json) raise ValueError("Invalid JSON structure") # Validate each app entry for app in data: if not isinstance(app, dict) or "appid" not in app or "normalized_name" not in app: - logger.error("Cached JSON %s contains invalid app entry, re-downloading", cache_json) + logger.error("Invalid app entry in cached JSON %s, re-downloading", cache_json) raise ValueError("Invalid app entry structure") steam_apps = data logger.info("Loaded %d apps from cache", len(steam_apps)) callback(steam_apps) except Exception as e: - logger.error("Error reading or validating cached JSON %s: %s", cache_json, e) + logger.error("Failed to read or validate cached JSON %s: %s", cache_json, e) # Attempt to re-download if cache is invalid or corrupted app_list_url = ( "https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/games_appid.tar.xz" @@ -351,12 +351,12 @@ def load_steam_apps_async(callback: Callable[[list], None]): def build_index(steam_apps): """ - Строит индекс приложений по полю normalized_name. + Builds an index of applications by normalized_name field. """ steam_apps_index = {} if not steam_apps: return steam_apps_index - logger.info("Построение индекса Steam приложений:") + logger.info("Building Steam apps index") for app in steam_apps: normalized = app["normalized_name"] steam_apps_index[normalized] = app @@ -364,25 +364,24 @@ def build_index(steam_apps): def search_app(candidate, steam_apps_index): """ - Ищет приложение по кандидату: сначала пытается точное совпадение, затем ищет подстроку. + Searches for an application by candidate: tries exact match first, then substring match. """ candidate_norm = normalize_name(candidate) - logger.info("Поиск приложения для кандидата: '%s' -> '%s'", candidate, candidate_norm) + logger.info("Searching for app with candidate: '%s' -> '%s'", candidate, candidate_norm) if candidate_norm in steam_apps_index: - logger.info(" Найдено точное совпадение: '%s'", candidate_norm) + logger.info("Found exact match: '%s'", candidate_norm) return steam_apps_index[candidate_norm] for name_norm, app in steam_apps_index.items(): if candidate_norm in name_norm: ratio = len(candidate_norm) / len(name_norm) if ratio > 0.8: - logger.info(" Найдено частичное совпадение: кандидат '%s' в '%s' (ratio: %.2f)", - candidate_norm, name_norm, ratio) + logger.info("Found partial match: candidate '%s' in '%s' (ratio: %.2f)", candidate_norm, name_norm, ratio) return app - logger.info(" Приложение для кандидата '%s' не найдено", candidate_norm) + logger.info("No app found for candidate '%s'", candidate_norm) return None def load_app_details(app_id): - """Загружает кэшированные данные для игры по appid, если они не устарели.""" + """Loads cached game data by appid if not outdated.""" cache_dir = get_cache_dir() cache_file = os.path.join(cache_dir, f"steam_app_{app_id}.json") if os.path.exists(cache_file): @@ -392,7 +391,7 @@ def load_app_details(app_id): return None def save_app_details(app_id, data): - """Сохраняет данные по appid в файл кэша.""" + """Saves appid data to a cache file.""" cache_dir = get_cache_dir() cache_file = os.path.join(cache_dir, f"steam_app_{app_id}.json") with open(cache_file, "wb") as f: @@ -435,7 +434,7 @@ def fetch_app_info_async(app_id: int, callback: Callable[[dict | None], None]): save_app_details(app_id, app_data) callback(app_data) except Exception as e: - logger.error("Error processing Steam app info for appid %s: %s", app_id, e) + logger.error("Failed to process Steam app info for appid %s: %s", app_id, e) callback(None) downloader.download_async(url, cache_file, timeout=5, callback=process_response) @@ -470,12 +469,12 @@ def load_weanticheatyet_data_async(callback: Callable[[list], None]): f.write(orjson.dumps(data)) if os.path.exists(cache_tar): os.remove(cache_tar) - logger.info("Archive %s deleted after extraction", cache_tar) + logger.info("Deleted archive: %s", cache_tar) anti_cheat_data = data or [] logger.info("Loaded %d anti-cheat entries from archive", len(anti_cheat_data)) callback(anti_cheat_data) except Exception as e: - logger.error("Error extracting WeAntiCheatYet archive: %s", e) + logger.error("Failed to extract WeAntiCheatYet archive: %s", e) callback([]) if os.path.exists(cache_json) and (time.time() - os.path.getmtime(cache_json) < CACHE_DURATION): @@ -485,41 +484,37 @@ def load_weanticheatyet_data_async(callback: Callable[[list], None]): data = orjson.loads(f.read()) # Validate JSON structure if not isinstance(data, list): - logger.error("Cached JSON %s has invalid format (not a list), re-downloading", cache_json) + logger.error("Invalid JSON format in %s (not a list), re-downloading", cache_json) raise ValueError("Invalid JSON structure") # Validate each anti-cheat entry for entry in data: if not isinstance(entry, dict) or "normalized_name" not in entry or "status" not in entry: - logger.error("Cached JSON %s contains invalid anti-cheat entry, re-downloading", cache_json) + logger.error("Invalid anti-cheat entry in cached JSON %s, re-downloading", cache_json) raise ValueError("Invalid anti-cheat entry structure") anti_cheat_data = data logger.info("Loaded %d anti-cheat entries from cache", len(anti_cheat_data)) callback(anti_cheat_data) except Exception as e: - logger.error("Error reading or validating cached WeAntiCheatYet JSON %s: %s", cache_json, e) + logger.error("Failed to read or validate cached WeAntiCheatYet JSON %s: %s", cache_json, e) # Attempt to re-download if cache is invalid or corrupted app_list_url = ( "https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/anticheat_games.tar.xz" ) - # Delete cached anti-cheat files before re-downloading - delete_cached_app_files(cache_dir, "anticheat_*.json") # Adjust pattern if app-specific files are added downloader.download_async(app_list_url, cache_tar, timeout=5, callback=process_tar) else: app_list_url = ( "https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/anticheat_games.tar.xz" ) - # Delete cached anti-cheat files before downloading - delete_cached_app_files(cache_dir, "anticheat_*.json") # Adjust pattern if app-specific files are added downloader.download_async(app_list_url, cache_tar, timeout=5, callback=process_tar) def build_weanticheatyet_index(anti_cheat_data): """ - Строит индекс античит-данных по полю normalized_name. + Builds an index of anti-cheat data by normalized_name field. """ anti_cheat_index = {} if not anti_cheat_data: return anti_cheat_index - logger.info("Построение индекса WeAntiCheatYet данных:") + logger.info("Building WeAntiCheatYet data index") for entry in anti_cheat_data: normalized = entry["normalized_name"] anti_cheat_index[normalized] = entry @@ -527,20 +522,19 @@ def build_weanticheatyet_index(anti_cheat_data): def search_anticheat_status(candidate, anti_cheat_index): candidate_norm = normalize_name(candidate) - logger.info("Поиск античит-статуса для кандидата: '%s' -> '%s'", candidate, candidate_norm) + logger.info("Searching for anti-cheat status for candidate: '%s' -> '%s'", candidate, candidate_norm) if candidate_norm in anti_cheat_index: status = anti_cheat_index[candidate_norm]["status"] - logger.info(" Найдено точное совпадение: '%s', статус: '%s'", candidate_norm, status) + logger.info("Found exact match: '%s', status: '%s'", candidate_norm, status) return status for name_norm, entry in anti_cheat_index.items(): if candidate_norm in name_norm: ratio = len(candidate_norm) / len(name_norm) if ratio > 0.8: status = entry["status"] - logger.info(" Найдено частичное совпадение: кандидат '%s' в '%s' (ratio: %.2f), статус: '%s'", - candidate_norm, name_norm, ratio, status) + logger.info("Found partial match: candidate '%s' in '%s' (ratio: %.2f), status: '%s'", candidate_norm, name_norm, ratio, status) return status - logger.info(" Античит-статус для кандидата '%s' не найден", candidate_norm) + logger.info("No anti-cheat status found for candidate '%s'", candidate_norm) return "" def get_weanticheatyet_status_async(game_name: str, callback: Callable[[str], None]): @@ -556,7 +550,7 @@ def get_weanticheatyet_status_async(game_name: str, callback: Callable[[str], No load_weanticheatyet_data_async(on_anticheat_data) def load_protondb_status(appid): - """Загружает закешированные данные ProtonDB для игры по appid, если они не устарели.""" + """Loads cached ProtonDB data for a game by appid if not outdated.""" cache_dir = get_cache_dir() cache_file = os.path.join(cache_dir, f"protondb_{appid}.json") if os.path.exists(cache_file): @@ -565,18 +559,18 @@ def load_protondb_status(appid): with open(cache_file, "rb") as f: return orjson.loads(f.read()) except Exception as e: - logger.error("Ошибка загрузки кеша ProtonDB для appid %s: %s", appid, e) + logger.error("Failed to load ProtonDB cache for appid %s: %s", appid, e) return None def save_protondb_status(appid, data): - """Сохраняет данные ProtonDB для игры по appid в файл кэша.""" + """Saves ProtonDB data for a game by appid to a cache file.""" cache_dir = get_cache_dir() cache_file = os.path.join(cache_dir, f"protondb_{appid}.json") try: with open(cache_file, "wb") as f: f.write(orjson.dumps(data)) except Exception as e: - logger.error("Ошибка сохранения кеша ProtonDB для appid %s: %s", appid, e) + logger.error("Failed to save ProtonDB cache for appid %s: %s", appid, e) def get_protondb_tier_async(appid: int, callback: Callable[[str], None]): """ @@ -664,7 +658,7 @@ def get_steam_game_info_async(desktop_name: str, exec_line: str, callback: Calla if game_exe.lower().endswith('.exe'): break except Exception as e: - logger.error("Error processing bat file %s: %s", game_exe, e) + logger.error("Failed to process bat file %s: %s", game_exe, e) else: logger.error("Bat file not found: %s", game_exe) @@ -799,55 +793,55 @@ def get_steam_apps_and_index_async(callback: Callable[[tuple[list, dict]], None] def enable_steam_cef() -> tuple[bool, str]: """ - Проверяет и при необходимости активирует режим удаленной отладки Steam CEF. + Checks and enables Steam CEF remote debugging if necessary. - Создает файл .cef-enable-remote-debugging в директории Steam. - Steam необходимо перезапустить после первого создания этого файла. + Creates a .cef-enable-remote-debugging file in the Steam directory. + Steam must be restarted after the file is first created. - Возвращает кортеж: - - (True, "already_enabled") если уже было активно. - - (True, "restart_needed") если было только что активировано и нужен перезапуск Steam. - - (False, "steam_not_found") если директория Steam не найдена. + Returns a tuple: + - (True, "already_enabled") if already enabled. + - (True, "restart_needed") if just enabled and Steam restart is needed. + - (False, "steam_not_found") if Steam directory is not found. """ steam_home = get_steam_home() if not steam_home: return (False, "steam_not_found") cef_flag_file = steam_home / ".cef-enable-remote-debugging" - logger.info(f"Проверка CEF флага: {cef_flag_file}") + logger.info(f"Checking CEF flag: {cef_flag_file}") if cef_flag_file.exists(): - logger.info("CEF Remote Debugging уже активирован.") + logger.info("CEF Remote Debugging is already enabled") return (True, "already_enabled") else: try: os.makedirs(cef_flag_file.parent, exist_ok=True) cef_flag_file.touch() - logger.info("CEF Remote Debugging активирован. Steam необходимо перезапустить.") + logger.info("Enabled CEF Remote Debugging. Steam restart required") return (True, "restart_needed") except Exception as e: - logger.error(f"Не удалось создать CEF флаг {cef_flag_file}: {e}") + logger.error(f"Failed to create CEF flag {cef_flag_file}: {e}") return (False, str(e)) def call_steam_api(js_cmd: str, *args) -> dict | None: """ - Выполняет JavaScript функцию в контексте Steam через CEF Remote Debugging. + Executes a JavaScript function in the Steam context via CEF Remote Debugging. Args: - js_cmd: Имя JS функции для вызова (напр. 'createShortcut'). - *args: Аргументы для передачи в JS функцию. + js_cmd: Name of the JS function to call (e.g., 'createShortcut'). + *args: Arguments to pass to the JS function. Returns: - Словарь с результатом выполнения или None в случае ошибки. + Dictionary with the result or None if an error occurs. """ status, message = enable_steam_cef() if not (status is True and message == "already_enabled"): if message == "restart_needed": - logger.warning("Steam CEF API доступен, но требует перезапуска Steam для полной активации.") + logger.warning("Steam CEF API is available but requires Steam restart for full activation") elif message == "steam_not_found": - logger.error("Не удалось найти директорию Steam для проверки CEF API.") + logger.error("Could not find Steam directory to check CEF API") else: - logger.error(f"Steam CEF API недоступен или не готов: {message}") + logger.error(f"Steam CEF API is unavailable or not ready: {message}") return None steam_debug_url = "http://localhost:8080/json" @@ -858,10 +852,10 @@ def call_steam_api(js_cmd: str, *args) -> dict | None: contexts = response.json() ws_url = next((ctx['webSocketDebuggerUrl'] for ctx in contexts if ctx['title'] == 'SharedJSContext'), None) if not ws_url: - logger.warning("Не удалось найти SharedJSContext. Steam запущен с флагом -cef-enable-remote-debugging?") + logger.warning("SharedJSContext not found. Is Steam running with -cef-enable-remote-debugging?") return None except Exception as e: - logger.warning(f"Не удалось подключиться к Steam CEF API по адресу {steam_debug_url}. Steam запущен? {e}") + logger.warning(f"Failed to connect to Steam CEF API at {steam_debug_url}. Is Steam running? {e}") return None js_code = """ @@ -906,15 +900,15 @@ def call_steam_api(js_cmd: str, *args) -> dict | None: response_data = orjson.loads(response_str) if "error" in response_data: - logger.error(f"Ошибка выполнения JS в Steam: {response_data['error']['message']}") + logger.error(f"JavaScript execution error in Steam: {response_data['error']['message']}") return None result = response_data.get('result', {}).get('result', {}) if result.get('type') == 'object' and result.get('subtype') == 'error': - logger.error(f"Ошибка выполнения JS в Steam: {result.get('description')}") + logger.error(f"JavaScript execution error in Steam: {result.get('description')}") return None return result.get('value') except Exception as e: - logger.error(f"Ошибка при взаимодействии с WebSocket Steam: {e}") + logger.error(f"WebSocket interaction error with Steam: {e}") return None def add_to_steam(game_name: str, exec_line: str, cover_path: str) -> tuple[bool, str]: @@ -991,24 +985,24 @@ export START_FROM_STEAM=1 else: success = generate_thumbnail(exe_path, generated_icon_path, size=128, force_resize=True) if not success or not os.path.exists(generated_icon_path): - logger.warning(f"generate_thumbnail failed to create icon for {exe_path}") + logger.warning(f"Failed to generate thumbnail for {exe_path}") icon_path = "" else: logger.info(f"Generated thumbnail: {generated_icon_path}") icon_path = generated_icon_path except Exception as e: - logger.error(f"Error generating thumbnail for {exe_path}: {e}") + logger.error(f"Failed to generate thumbnail for {exe_path}: {e}") icon_path = "" steam_home = get_steam_home() if not steam_home: logger.error("Steam home directory not found") - return (False, "Steam directory not found.") + return (False, "Steam directory not found") last_user = get_last_steam_user(steam_home) if not last_user or 'SteamID' not in last_user: logger.error("Failed to retrieve Steam user ID") - return (False, "Failed to get Steam user ID.") + return (False, "Failed to get Steam user ID") userdata_dir = steam_home / "userdata" user_id = last_user['SteamID'] @@ -1021,7 +1015,7 @@ export START_FROM_STEAM=1 appid = None was_api_used = False - logger.info("Попытка добавления ярлыка через Steam CEF API...") + logger.info("Attempting to add shortcut via Steam CEF API") api_response = call_steam_api( "createShortcut", game_name, @@ -1034,9 +1028,9 @@ export START_FROM_STEAM=1 if api_response and isinstance(api_response, dict) and 'id' in api_response: appid = api_response['id'] was_api_used = True - logger.info(f"Ярлык успешно добавлен через API. AppID: {appid}") + logger.info(f"Shortcut successfully added via API. AppID: {appid}") else: - logger.warning("Не удалось добавить ярлык через API. Используется запасной метод (запись в shortcuts.vdf).") + logger.warning("Failed to add shortcut via API. Falling back to shortcuts.vdf") backup_path = f"{steam_shortcuts_path}.backup" if os.path.exists(steam_shortcuts_path): try: @@ -1110,7 +1104,7 @@ export START_FROM_STEAM=1 appid = None if not appid: - return (False, "Не удалось создать ярлык ни одним из способов.") + return (False, "Failed to create shortcut using any method") steam_appid = None @@ -1120,7 +1114,7 @@ export START_FROM_STEAM=1 if not steam_appid or not isinstance(steam_appid, int): logger.info("No valid Steam appid found, skipping cover download") return - logger.info(f"Найден Steam AppID {steam_appid} для загрузки обложек.") + logger.info(f"Found Steam AppID {steam_appid} for cover download") cover_types = [ ("p.jpg", "library_600x900_2x.jpg"), @@ -1137,15 +1131,15 @@ export START_FROM_STEAM=1 try: with open(result_path, 'rb') as f: img_b64 = base64.b64encode(f.read()).decode('utf-8') - logger.info(f"Применение обложки типа '{steam_name}' через API для AppID {appid}") + logger.info(f"Applying cover type '{steam_name}' via API for AppID {appid}") ext = Path(steam_name).suffix.lstrip('.') call_steam_api("setGrid", appid, index, ext, img_b64) except Exception as e: - logger.error(f"Ошибка при применении обложки '{steam_name}' через API: {e}") + logger.error(f"Failed to apply cover '{steam_name}' via API: {e}") else: logger.warning(f"Failed to download cover {steam_name} for appid {steam_appid}") except Exception as e: - logger.error(f"Error processing cover {steam_name} for appid {steam_appid}: {e}") + logger.error(f"Failed to process cover {steam_name} for appid {steam_appid}: {e}") for i, (suffix, steam_name) in enumerate(cover_types): cover_file = os.path.join(grid_dir, f"{appid}{suffix}") @@ -1186,13 +1180,13 @@ def remove_from_steam(game_name: str, exec_line: str) -> tuple[bool, str]: steam_home = get_steam_home() if not steam_home: logger.error("Steam home directory not found") - return (False, "Steam directory not found.") + return (False, "Steam directory not found") # Get current Steam user ID last_user = get_last_steam_user(steam_home) if not last_user or 'SteamID' not in last_user: logger.error("Failed to retrieve Steam user ID") - return (False, "Failed to get Steam user ID.") + return (False, "Failed to get Steam user ID") userdata_dir = steam_home / "userdata" user_id = last_user['SteamID'] unsigned_id = convert_steam_id(user_id) @@ -1238,10 +1232,10 @@ def remove_from_steam(game_name: str, exec_line: str) -> tuple[bool, str]: return (False, f"Game '{game_name}' not found in Steam") api_response = call_steam_api("removeShortcut", appid) - if api_response is not None: # API ответил, даже если ответ пустой - logger.info(f"Ярлык для AppID {appid} успешно удален через API.") + if api_response is not None: # API responded, even if response is empty + logger.info(f"Shortcut for AppID {appid} successfully removed via API") else: - logger.warning("Не удалось удалить ярлык через API. Используется запасной метод (редактирование shortcuts.vdf).") + logger.warning("Failed to remove shortcut via API. Falling back to editing shortcuts.vdf") # Create backup of shortcuts.vdf backup_path = f"{steam_shortcuts_path}.backup" @@ -1320,5 +1314,5 @@ def is_game_in_steam(game_name: str) -> bool: if entry.get("AppName") == game_name: return True except Exception as e: - logger.error(f"Error checking if game {game_name} is in Steam: {e}") + logger.error(f"Failed to check if game {game_name} is in Steam: {e}") return False