Files
Jackify/jackify/backend/handlers/shortcut_vdf_management.py
2026-02-07 18:26:54 +00:00

319 lines
12 KiB
Python

"""VDF backup/restore and modification methods for ShortcutHandler (Mixin)."""
import logging
import os
import shutil
import subprocess
import time
from pathlib import Path
import glob
import vdf
from .vdf_handler import VDFHandler
logger = logging.getLogger(__name__)
class ShortcutVDFManagementMixin:
"""Mixin providing VDF file management methods."""
def _check_and_restore_shortcuts_vdf(self):
"""
Check if shortcuts.vdf exists and restore from backup if missing.
Returns:
bool: True if file exists or was restored, False if unable to restore
"""
shortcuts_files = []
for user_dir in os.listdir(self.shortcuts_path):
shortcuts_file = os.path.join(self.shortcuts_path, user_dir, "config", "shortcuts.vdf")
if os.path.dirname(shortcuts_file):
shortcuts_files.append(shortcuts_file)
missing_files = []
for file_path in shortcuts_files:
if not os.path.exists(file_path):
self.logger.warning(f"shortcuts.vdf is missing at: {file_path}")
missing_files.append(file_path)
if not missing_files:
self.logger.debug("All shortcuts.vdf files are present")
return True
restored = 0
for file_path in missing_files:
backup_files = sorted(glob.glob(f"{file_path}.*.bak"), reverse=True)
if backup_files:
try:
shutil.copy2(backup_files[0], file_path)
self.logger.info(f"Restored {file_path} from {backup_files[0]}")
restored += 1
continue
except Exception as e:
self.logger.error(f"Failed to restore from timestamped backup: {e}")
simple_backup = f"{file_path}.bak"
if os.path.exists(simple_backup):
try:
shutil.copy2(simple_backup, file_path)
self.logger.info(f"Restored {file_path} from simple backup")
restored += 1
continue
except Exception as e:
self.logger.error(f"Failed to restore from simple backup: {e}")
if restored == len(missing_files):
self.logger.info("Successfully restored all missing shortcuts.vdf files")
return True
elif restored > 0:
self.logger.warning(f"Partially restored {restored}/{len(missing_files)} shortcuts.vdf files")
return True
else:
self.logger.error("Failed to restore any shortcuts.vdf files")
return False
def _modify_shortcuts_directly(self, shortcuts_file, modlist_name, mo2_path, mo2_dir):
"""
Directly modify shortcuts.vdf in a way that preserves Steam's exact binary format.
This is a fallback method when regular VDF handling might cause issues.
Args:
shortcuts_file (str): Path to shortcuts.vdf
modlist_name (str): Name for the modlist
mo2_path (str): Path to ModOrganizer.exe
mo2_dir (str): Directory containing ModOrganizer.exe
Returns:
bool: True if successful, False otherwise
"""
try:
backup_path = f"{shortcuts_file}.{int(time.time())}.bak"
shutil.copy2(shortcuts_file, backup_path)
self.logger.info(f"Created backup before direct modification: {backup_path}")
if not os.path.exists(shortcuts_file) or os.path.getsize(shortcuts_file) == 0:
with open(shortcuts_file, 'wb') as f:
f.write(b'\x00shortcuts\x00\x08\x08')
self.logger.info(f"Created new shortcuts.vdf file at {shortcuts_file}")
try:
import sys
import importlib.util
steam_vdf_spec = importlib.util.find_spec("steam_vdf")
if steam_vdf_spec is None:
from jackify.backend.handlers.subprocess_utils import get_safe_python_executable
python_exe = get_safe_python_executable()
subprocess.check_call([python_exe, "-m", "pip", "install", "steam-vdf", "--user"])
time.sleep(1)
import vdf as steam_vdf
with open(shortcuts_file, 'rb') as f:
shortcuts_data = steam_vdf.load(f)
max_id = -1
if 'shortcuts' in shortcuts_data:
for id_str in shortcuts_data['shortcuts']:
try:
id_num = int(id_str)
if id_num > max_id:
max_id = id_num
except ValueError:
pass
new_id = max_id + 1
if 'shortcuts' not in shortcuts_data:
shortcuts_data['shortcuts'] = {}
shortcuts_data['shortcuts'][str(new_id)] = {
'AppName': modlist_name,
'Exe': f'"{mo2_path}"',
'StartDir': mo2_dir,
'icon': '',
'ShortcutPath': '',
'LaunchOptions': '',
'IsHidden': 0,
'AllowDesktopConfig': 1,
'AllowOverlay': 1,
'OpenVR': 0,
'Devkit': 0,
'DevkitGameID': '',
'LastPlayTime': 0
}
with open(shortcuts_file, 'wb') as f:
steam_vdf.dump(shortcuts_data, f)
self.logger.info(f"Added shortcut for {modlist_name} using steam-vdf library")
return True
except Exception as e:
self.logger.warning(f"Failed to use steam-vdf library: {e}")
self.logger.info("Falling back to VDFHandler for shortcuts.vdf modification")
shortcuts_data = VDFHandler.load(shortcuts_file, binary=True)
if not shortcuts_data:
shortcuts_data = {'shortcuts': {}}
new_id = len(shortcuts_data.get('shortcuts', {}))
new_entry = {
'AppName': modlist_name,
'Exe': f'"{mo2_path}"',
'StartDir': mo2_dir,
'icon': '',
'ShortcutPath': '',
'LaunchOptions': '',
'IsHidden': 0,
'AllowDesktopConfig': 1,
'AllowOverlay': 1,
'OpenVR': 0,
'Devkit': 0,
'DevkitGameID': '',
'LastPlayTime': 0
}
if 'shortcuts' not in shortcuts_data:
shortcuts_data['shortcuts'] = {}
shortcuts_data['shortcuts'][str(new_id)] = new_entry
result = VDFHandler.save(shortcuts_file, shortcuts_data, binary=True)
self.logger.info(f"Added shortcut for {modlist_name} using VDFHandler")
return result
except Exception as e:
self.logger.error(f"Error in direct shortcut modification: {e}")
return False
def _add_steam_shortcut_safely(self, shortcuts_file, app_name, exe_path, start_dir, icon_path="", launch_options="", tags=None):
"""
Adds a new shortcut entry to the shortcuts.vdf file using the correct binary format.
This method is carefully designed to maintain file integrity.
Args:
shortcuts_file (str): Path to shortcuts.vdf
app_name (str): Name for the shortcut
exe_path (str): Path to the executable
start_dir (str): Start directory for the executable
icon_path (str): Path to icon file (optional)
launch_options (str): Command line options (optional)
tags (list): List of tags (optional)
Returns:
tuple: (bool success, str app_id) - Success status and calculated AppID
"""
if tags is None:
tags = []
data = {'shortcuts': {}}
try:
if os.path.exists(shortcuts_file):
with open(shortcuts_file, 'rb') as f:
file_data = f.read()
if file_data:
try:
data = vdf.binary_loads(file_data)
if 'shortcuts' not in data:
data['shortcuts'] = {}
except Exception as e:
self.logger.warning(f"Could not parse existing shortcuts.vdf: {e}")
data = {'shortcuts': {}}
else:
self.logger.info(f"shortcuts.vdf not found at {shortcuts_file}. A new file will be created.")
except Exception as e:
self.logger.warning(f"Error accessing shortcuts.vdf: {e}")
data = {'shortcuts': {}}
if 'shortcuts' not in data:
data['shortcuts'] = {}
next_index = 0
if data.get('shortcuts'):
shortcut_indices = [int(k) for k in data['shortcuts'].keys() if k.isdigit()]
if shortcut_indices:
next_index = max(shortcut_indices) + 1
new_shortcut = {
'AppName': app_name,
'Exe': f'"{exe_path}"',
'StartDir': f'"{start_dir}"',
'icon': icon_path,
'ShortcutPath': "",
'LaunchOptions': launch_options,
'IsHidden': 0,
'AllowDesktopConfig': 1,
'AllowOverlay': 1,
'OpenVR': 0,
'Devkit': 0,
'DevkitGameID': '',
'DevkitOverrideAppID': 0,
'LastPlayTime': 0,
'FlatpakAppID': '',
'IsInstalled': 1,
}
if tags:
new_shortcut['tags'] = {str(i): tag for i, tag in enumerate(tags)}
app_id = (0x80000000 + int(next_index)) % (2**32)
if app_id > 0x7FFFFFFF:
app_id = app_id - 0x100000000
new_shortcut['appid'] = app_id
data['shortcuts'][str(next_index)] = new_shortcut
self.logger.info(f"Adding shortcut '{app_name}' at index {next_index}")
try:
temp_file = f"{shortcuts_file}.temp"
with open(temp_file, 'wb') as f:
vdf_data = vdf.binary_dumps(data)
f.write(vdf_data)
shutil.move(temp_file, shortcuts_file)
self.logger.info(f"Successfully updated shortcuts.vdf! AppID: {app_id}")
return True, app_id
except Exception as e:
self.logger.error(f"Error: Failed to write updated shortcuts.vdf: {e}")
return False, None
def _verify_and_restore_shortcuts(self):
"""
Verify shortcuts.vdf exists after Steam restart and restore it if needed.
"""
shortcuts_file = getattr(self, '_shortcuts_file', None)
if not shortcuts_file:
self.logger.warning("No shortcuts file to verify")
return
if not os.path.exists(shortcuts_file) or os.path.getsize(shortcuts_file) == 0:
self.logger.warning(f"shortcuts.vdf missing or empty after restart: {shortcuts_file}")
safe_backup = getattr(self, '_safe_shortcuts_backup', None)
if safe_backup and os.path.exists(safe_backup):
try:
shutil.copy2(safe_backup, shortcuts_file)
self.logger.info(f"Restored shortcuts.vdf from pre-restart backup")
print("Restored shortcuts file after Steam restart")
return
except Exception as e:
self.logger.error(f"Failed to restore from pre-restart backup: {e}")
backup = getattr(self, '_last_shortcuts_backup', None)
if backup and os.path.exists(backup):
try:
shutil.copy2(backup, shortcuts_file)
self.logger.info(f"Restored shortcuts.vdf from regular backup")
print("Restored shortcuts file after Steam restart")
except Exception as e:
self.logger.error(f"Failed to restore from backup: {e}")
print("Failed to restore shortcuts file. You may need to recreate your shortcut.")
else:
self.logger.info(f"shortcuts.vdf verified intact after restart")