mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-17 21:17:46 +02:00
Sync from development - prepare for v0.3.0
This commit is contained in:
318
jackify/backend/handlers/shortcut_vdf_management.py
Normal file
318
jackify/backend/handlers/shortcut_vdf_management.py
Normal file
@@ -0,0 +1,318 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user