mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 00:17:58 +02:00
317 lines
12 KiB
Python
317 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")
|
|
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")
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to restore from backup: {e}")
|
|
self.logger.warning("shortcuts.vdf restore failed - shortcut may need to be recreated")
|
|
else:
|
|
self.logger.info(f"shortcuts.vdf verified intact after restart")
|