mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-01-24 12:22:49 -05:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b77cea8d0b | ||
|
|
fbf04d0020 | ||
|
|
d26e1d154e | ||
|
|
0a74ba0f4c | ||
|
|
e000afacbc | ||
|
|
25c1c46e5d | ||
|
|
b9d418805f | ||
|
|
c99d747642 | ||
|
|
2a94f19719 | ||
|
|
642b68ce91 | ||
|
|
eb9b3a1368 | ||
|
|
068e52c529 | ||
|
|
7e19ad1ab9 | ||
|
|
5b9e3fd350 | ||
|
|
dab8787f1d | ||
|
|
723c5873b5 | ||
|
|
649abe709a | ||
|
|
41f8e80df1 | ||
|
|
5a3fafac49 | ||
|
|
a2d081d3b6 | ||
|
|
a13a56cbc3 | ||
|
|
6c2306eac4 | ||
|
|
41680b8c15 | ||
|
|
e580645892 | ||
|
|
4648a1ad65 |
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: "[ Feature Request ]"
|
||||||
|
labels: enhancement
|
||||||
|
assignees: Novattz
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
29
.github/ISSUE_TEMPLATE/report.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/report.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: Report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
assignees: Novattz
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
**Before submitting, have you tried:**
|
||||||
|
- Using smokeapi with Proton? [ ] Yes [ ] No
|
||||||
|
- Checking if `LD_PRELOAD` is blocked on your system? [ ] Yes [ ] No
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
- A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**Terminal output**
|
||||||
|
- Copy and paste the entire terminal output when running the script.
|
||||||
|
|
||||||
|
**Log File Output**
|
||||||
|
- If the script logged any errors, attach the log file **`script.log`** to this issue.
|
||||||
|
|
||||||
|
**Debug Output**
|
||||||
|
- Run the script with the **`--debug`** argument and attach the log file **`debug_script.log`** to this issue.
|
||||||
|
|
||||||
|
**Steam library path**
|
||||||
|
- Provide the path where your steam library is.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
54
README.md
54
README.md
@@ -1,36 +1,44 @@
|
|||||||
|
|
||||||
# Steam DLC Fetcher and installer for Linux
|
# Steam DLC Fetcher and installer for Linux
|
||||||
- Python script designed for linux to automate fetching of DLC id's for steam games and the installation of creamlinux automatically.
|
- A user-friendly tool for managing DLC for Steam games on Linux systems.
|
||||||
|
|
||||||
# Features
|
[Demo/Tutorial](https://www.youtube.com/watch?v=Y1E15rUsdDw) - [OUTDATED]
|
||||||
- Automatically fetches and lists DLC's for selected steam game(s) installed on the computer.
|
|
||||||
- Automatically installs creamlinux and its components into selected steam games, excluding and handling specific config files.
|
|
||||||
- Provides a simple cli to navigate and operate the entire process.
|
|
||||||
|
|
||||||
# Demo
|
### Features
|
||||||
https://www.youtube.com/watch?v=22LDDUoBvus&ab_channel=Nova
|
- Automatic Steam library detection
|
||||||
|
- Support for Linux and Proton
|
||||||
|
- Automatic updates (Soon)
|
||||||
|
- DLC detection and installation
|
||||||
|
|
||||||
# To do
|
### Prerequisites
|
||||||
- Cross reference dlc files and dlc id's. Incase dlc id's and dlc files differ in terms of quantity it will notify the user.
|
- Python 3.7 or higher
|
||||||
- Possibly add functionality to search for dlc files/automatically installing them.
|
- requests
|
||||||
- Add the possibility to install cream/smokeapi for games running proton.
|
- rich
|
||||||
- Check if the game already has dlc files installed
|
- argparse
|
||||||
- Gui?
|
- json
|
||||||
|
|
||||||
# Prerequisites
|
### Installation
|
||||||
- `python 3.x`
|
|
||||||
- `requests` library
|
|
||||||
- `zipfile` library
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
- Clone the repo or download the script.
|
- Clone the repo or download the script.
|
||||||
- Navigate to the directory containing the script.
|
- Navigate to the directory containing the script.
|
||||||
- Run the script using python.
|
- Run the script using python:
|
||||||
```bash
|
```bash
|
||||||
python dlc_fetcher.py
|
python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
# Issues?
|
### Basic Usage
|
||||||
|
- `--manual <path>`: Specify steam library path manually
|
||||||
|
```bash
|
||||||
|
python main.py --manual "/path/to/steamapps"
|
||||||
|
```
|
||||||
|
- `--debug`: Enable debug logging
|
||||||
|
```bash
|
||||||
|
python main.py --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issues?
|
||||||
- Open a issue and attach all relevant errors/logs.
|
- Open a issue and attach all relevant errors/logs.
|
||||||
|
|
||||||
# Credits
|
## Credits
|
||||||
- [All credits for creamlinux go to its original author and contributors.](https://github.com/anticitizn/creamlinux)
|
- [Creamlinux](https://github.com/anticitizn/creamlinux) by anticitizn
|
||||||
|
- [SmokeAPI](https://github.com/acidicoala/SmokeAPI) by acidicoala
|
||||||
|
|||||||
222
dlc_fetcher.py
222
dlc_fetcher.py
@@ -1,222 +0,0 @@
|
|||||||
import os
|
|
||||||
import re
|
|
||||||
import requests
|
|
||||||
import zipfile
|
|
||||||
import time
|
|
||||||
import stat
|
|
||||||
|
|
||||||
LOG_FILE = 'script.log'
|
|
||||||
|
|
||||||
def clear_screen():
|
|
||||||
os.system('cls' if os.name == 'nt' else 'clear')
|
|
||||||
|
|
||||||
def fetch_latest_version():
|
|
||||||
try:
|
|
||||||
response = requests.get("https://api.github.com/repos/Novattz/creamlinux-installer/releases/latest")
|
|
||||||
data = response.json()
|
|
||||||
return data['tag_name']
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
log_message(f"Failed to fetch latest version: {str(e)}")
|
|
||||||
return "Unknown"
|
|
||||||
|
|
||||||
def show_header(app_version):
|
|
||||||
clear_screen()
|
|
||||||
cyan = '\033[96m'
|
|
||||||
reset = '\033[0m'
|
|
||||||
print(f"{cyan}")
|
|
||||||
print(r"""
|
|
||||||
_ _ _ _ _ ____ _____ _ ________ _____
|
|
||||||
| | | | \ | | | / __ \ / ____| |/ / ____| __ \
|
|
||||||
| | | | \| | | | | | | | | ' /| |__ | |__) |
|
|
||||||
| | | | . ` | | | | | | | | < | __| | _ /
|
|
||||||
| |__| | |\ | |___| |__| | |____| . \| |____| | \ \
|
|
||||||
\____/|_| \_|______\____/ \_____|_|\_\______|_| \_\
|
|
||||||
|
|
||||||
""")
|
|
||||||
print(f"""
|
|
||||||
> Made by Tickbase
|
|
||||||
> GitHub: https://github.com/Novattz/creamlinux-installer
|
|
||||||
> Version: {app_version}
|
|
||||||
{reset}
|
|
||||||
""")
|
|
||||||
|
|
||||||
app_version = fetch_latest_version()
|
|
||||||
|
|
||||||
def log_message(message):
|
|
||||||
with open(LOG_FILE, 'a') as log_file:
|
|
||||||
log_file.write(f"{message}\n")
|
|
||||||
print(message)
|
|
||||||
|
|
||||||
def parse_vdf(file_path):
|
|
||||||
library_paths = []
|
|
||||||
try:
|
|
||||||
with open(file_path, 'r', encoding='utf-8') as file:
|
|
||||||
content = file.read()
|
|
||||||
paths = re.findall(r'"path"\s*"(.*?)"', content, re.IGNORECASE)
|
|
||||||
library_paths.extend([os.path.normpath(path) for path in paths])
|
|
||||||
except Exception as e:
|
|
||||||
log_message(f"Failed to read {file_path}: {str(e)}")
|
|
||||||
return library_paths
|
|
||||||
|
|
||||||
def find_steam_library_folders():
|
|
||||||
base_paths = [
|
|
||||||
os.path.expanduser('~/.steam/steam'),
|
|
||||||
os.path.expanduser('~/.local/share/Steam'),
|
|
||||||
os.path.expanduser('~/home/deck/.steam/steam'),
|
|
||||||
os.path.expanduser('~/home/deck/.local/share/Steam'),
|
|
||||||
'/mnt', '/media',
|
|
||||||
'/run/media/mmcblk0p1/steamapps'
|
|
||||||
]
|
|
||||||
library_folders = []
|
|
||||||
try:
|
|
||||||
for base_path in base_paths:
|
|
||||||
if os.path.exists(base_path):
|
|
||||||
for root, dirs, files in os.walk(base_path, topdown=True):
|
|
||||||
if 'steamapps' in dirs:
|
|
||||||
steamapps_path = os.path.join(root, 'steamapps')
|
|
||||||
library_folders.append(steamapps_path)
|
|
||||||
vdf_path = os.path.join(steamapps_path, 'libraryfolders.vdf')
|
|
||||||
if os.path.exists(vdf_path):
|
|
||||||
additional_paths = parse_vdf(vdf_path)
|
|
||||||
for path in additional_paths:
|
|
||||||
new_steamapps_path = os.path.join(path, 'steamapps')
|
|
||||||
if os.path.exists(new_steamapps_path):
|
|
||||||
library_folders.append(new_steamapps_path)
|
|
||||||
dirs[:] = [] # Prevent further scanning into subdirectories
|
|
||||||
if not library_folders:
|
|
||||||
raise FileNotFoundError("No Steam library folders found.")
|
|
||||||
except Exception as e:
|
|
||||||
log_message(f"Error finding Steam library folders: {e}")
|
|
||||||
return library_folders
|
|
||||||
|
|
||||||
def find_steam_apps(library_folders):
|
|
||||||
acf_pattern = re.compile(r'^appmanifest_(\d+)\.acf$')
|
|
||||||
games = {}
|
|
||||||
try:
|
|
||||||
for folder in library_folders:
|
|
||||||
if os.path.exists(folder):
|
|
||||||
for item in os.listdir(folder):
|
|
||||||
if acf_pattern.match(item):
|
|
||||||
try:
|
|
||||||
app_id, game_name, install_dir = parse_acf(os.path.join(folder, item))
|
|
||||||
if app_id and game_name:
|
|
||||||
install_path = os.path.join(folder, 'common', install_dir)
|
|
||||||
if os.path.exists(install_path):
|
|
||||||
cream_installed = 'Cream installed' if 'cream.sh' in os.listdir(install_path) else ''
|
|
||||||
games[app_id] = (game_name, cream_installed, install_path)
|
|
||||||
except Exception as e:
|
|
||||||
log_message(f"Error parsing {item}: {e}")
|
|
||||||
if not games:
|
|
||||||
raise FileNotFoundError("No Steam games found.")
|
|
||||||
except Exception as e:
|
|
||||||
log_message(f"Error finding Steam apps: {e}")
|
|
||||||
return games
|
|
||||||
|
|
||||||
def parse_acf(file_path):
|
|
||||||
try:
|
|
||||||
with open(file_path, 'r', encoding='utf-8') as file:
|
|
||||||
data = file.read()
|
|
||||||
app_id = re.search(r'"appid"\s+"(\d+)"', data)
|
|
||||||
name = re.search(r'"name"\s+"([^"]+)"', data)
|
|
||||||
install_dir = re.search(r'"installdir"\s+"([^"]+)"', data)
|
|
||||||
return app_id.group(1), name.group(1), install_dir.group(1)
|
|
||||||
except Exception as e:
|
|
||||||
log_message(f"Error reading ACF file {file_path}: {e}")
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
def fetch_dlc_details(app_id):
|
|
||||||
base_url = f"https://store.steampowered.com/api/appdetails?appids={app_id}"
|
|
||||||
try:
|
|
||||||
response = requests.get(base_url)
|
|
||||||
data = response.json()
|
|
||||||
if app_id not in data or "data" not in data[app_id]:
|
|
||||||
log_message("Error: Unable to fetch game details.")
|
|
||||||
return []
|
|
||||||
game_data = data[app_id]["data"]
|
|
||||||
dlcs = game_data.get("dlc", [])
|
|
||||||
dlc_details = []
|
|
||||||
for dlc_id in dlcs:
|
|
||||||
try:
|
|
||||||
time.sleep(0.3)
|
|
||||||
dlc_url = f"https://store.steampowered.com/api/appdetails?appids={dlc_id}"
|
|
||||||
dlc_response = requests.get(dlc_url)
|
|
||||||
if dlc_response.status_code == 200:
|
|
||||||
dlc_data = dlc_response.json()
|
|
||||||
if str(dlc_id) in dlc_data and "data" in dlc_data[str(dlc_id)]:
|
|
||||||
dlc_name = dlc_data[str(dlc_id)]["data"].get("name", "Unknown DLC")
|
|
||||||
dlc_details.append({"appid": dlc_id, "name": dlc_name})
|
|
||||||
else:
|
|
||||||
log_message(f"Data missing for DLC {dlc_id}")
|
|
||||||
elif dlc_response.status_code == 429:
|
|
||||||
log_message("Rate limited! Please wait before trying again.")
|
|
||||||
time.sleep(10)
|
|
||||||
else:
|
|
||||||
log_message(f"Failed to fetch details for DLC {dlc_id}, Status Code: {dlc_response.status_code}")
|
|
||||||
except Exception as e:
|
|
||||||
log_message(f"Exception for DLC {dlc_id}: {str(e)}")
|
|
||||||
return dlc_details
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
log_message(f"Failed to fetch DLC details for {app_id}: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def install_files(app_id, game_install_dir, dlcs, game_name):
|
|
||||||
zip_url = "https://github.com/anticitizn/creamlinux/releases/latest/download/creamlinux.zip"
|
|
||||||
zip_path = os.path.join(game_install_dir, 'creamlinux.zip')
|
|
||||||
try:
|
|
||||||
response = requests.get(zip_url)
|
|
||||||
if response.status_code == 200:
|
|
||||||
with open(zip_path, 'wb') as f:
|
|
||||||
f.write(response.content)
|
|
||||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
|
||||||
zip_ref.extractall(game_install_dir)
|
|
||||||
os.remove(zip_path)
|
|
||||||
|
|
||||||
cream_sh_path = os.path.join(game_install_dir, 'cream.sh')
|
|
||||||
os.chmod(cream_sh_path, os.stat(cream_sh_path).st_mode | stat.S_IEXEC)
|
|
||||||
|
|
||||||
dlc_list = "\n".join([f"{dlc['appid']} = {dlc['name']}" for dlc in dlcs])
|
|
||||||
cream_api_path = os.path.join(game_install_dir, 'cream_api.ini')
|
|
||||||
with open(cream_api_path, 'w') as f:
|
|
||||||
f.write(f"APPID = {app_id}\n[config]\nissubscribedapp_on_false_use_real = true\n[methods]\ndisable_steamapps_issubscribedapp = false\n[dlc]\n{dlc_list}")
|
|
||||||
print(f"Custom cream_api.ini has been written to {game_install_dir}.")
|
|
||||||
print(f"Installation complete. Set launch options in Steam: 'sh ./cream.sh %command%' for {game_name}.")
|
|
||||||
else:
|
|
||||||
log_message("Failed to download the files needed for installation.")
|
|
||||||
except Exception as e:
|
|
||||||
log_message(f"Failed to install files for {game_name}: {e}")
|
|
||||||
|
|
||||||
def main():
|
|
||||||
show_header(app_version)
|
|
||||||
try:
|
|
||||||
library_folders = find_steam_library_folders()
|
|
||||||
games = find_steam_apps(library_folders)
|
|
||||||
if games:
|
|
||||||
print("Select the game for which you want to fetch DLCs:")
|
|
||||||
games_list = list(games.items())
|
|
||||||
GREEN = '\033[92m'
|
|
||||||
RESET = '\033[0m'
|
|
||||||
for idx, (app_id, (name, cream_status, _)) in enumerate(games_list, 1):
|
|
||||||
if cream_status:
|
|
||||||
print(f"{idx}. {GREEN}{name} (App ID: {app_id}) - Cream installed{RESET}")
|
|
||||||
else:
|
|
||||||
print(f"{idx}. {name} (App ID: {app_id})")
|
|
||||||
|
|
||||||
choice = int(input("Enter the number of the game: ")) - 1
|
|
||||||
if choice < 0 or choice >= len(games_list):
|
|
||||||
raise ValueError("Invalid selection.")
|
|
||||||
selected_app_id, (selected_game_name, _, selected_install_dir) = games_list[choice]
|
|
||||||
print(f"You selected: {selected_game_name} (App ID: {selected_app_id})")
|
|
||||||
|
|
||||||
dlcs = fetch_dlc_details(selected_app_id)
|
|
||||||
if dlcs:
|
|
||||||
print("DLC IDs found:", [dlc['appid'] for dlc in dlcs]) # Only print app IDs for clarity
|
|
||||||
install_files(selected_app_id, selected_install_dir, dlcs, selected_game_name)
|
|
||||||
else:
|
|
||||||
print("No DLCs found for the selected game.")
|
|
||||||
else:
|
|
||||||
print("No Steam games found on this computer or connected drives.")
|
|
||||||
except Exception as e:
|
|
||||||
log_message(f"An error occurred: {e}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
566
helper.py
Normal file
566
helper.py
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
import zipfile
|
||||||
|
import time
|
||||||
|
import shutil
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class SteamHelper:
|
||||||
|
def __init__(self, debug=False):
|
||||||
|
self.debug = debug
|
||||||
|
self.logger = None
|
||||||
|
self.config = None
|
||||||
|
# Only setup logging if debug is enabled - errors will setup logging on-demand
|
||||||
|
if debug:
|
||||||
|
self._setup_logging()
|
||||||
|
self.load_config()
|
||||||
|
|
||||||
|
def load_config(self):
|
||||||
|
"""Load configuration from config.json"""
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
config_path = os.path.join(script_dir, 'config.json')
|
||||||
|
|
||||||
|
# Create default config if it doesn't exist
|
||||||
|
if not os.path.exists(config_path):
|
||||||
|
default_config = {
|
||||||
|
"version": "v1.0.8",
|
||||||
|
"github_repo": "Novattz/creamlinux-installer",
|
||||||
|
"github_api": "https://api.github.com/repos/",
|
||||||
|
"creamlinux_release": "https://github.com/anticitizn/creamlinux/releases/latest/download/creamlinux.zip",
|
||||||
|
"smokeapi_release": "acidicoala/SmokeAPI",
|
||||||
|
}
|
||||||
|
with open(config_path, 'w') as f:
|
||||||
|
json.dump(default_config, f, indent=4)
|
||||||
|
self.config = default_config
|
||||||
|
if self.debug:
|
||||||
|
self._log_debug("Created default config.json")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
self.config = json.load(f)
|
||||||
|
if self.debug:
|
||||||
|
self._log_debug(f"Loaded config: {self.config}")
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"Failed to load config: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _cleanup_old_logs(self, log_dir, keep_logs=5):
|
||||||
|
"""Clean up old log files, keeping only the most recent ones"""
|
||||||
|
try:
|
||||||
|
log_files = [os.path.join(log_dir, f) for f in os.listdir(log_dir)
|
||||||
|
if f.endswith('.log')]
|
||||||
|
if len(log_files) > keep_logs:
|
||||||
|
log_files.sort(key=lambda x: os.path.getmtime(x))
|
||||||
|
for f in log_files[:-keep_logs]:
|
||||||
|
os.remove(f)
|
||||||
|
except Exception:
|
||||||
|
pass # Silently fail cleanup since this is not critical
|
||||||
|
|
||||||
|
def _setup_logging(self):
|
||||||
|
"""Setup logging to file with detailed formatting"""
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
log_dir = os.path.join(script_dir, 'logs')
|
||||||
|
os.makedirs(log_dir, exist_ok=True)
|
||||||
|
|
||||||
|
self._cleanup_old_logs(log_dir)
|
||||||
|
|
||||||
|
log_file = os.path.join(log_dir, f'cream_installer_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log')
|
||||||
|
|
||||||
|
file_handler = logging.FileHandler(log_file)
|
||||||
|
file_handler.setLevel(logging.DEBUG if self.debug else logging.ERROR)
|
||||||
|
file_handler.setFormatter(logging.Formatter(
|
||||||
|
'%(asctime)s - %(levelname)s - %(message)s'
|
||||||
|
))
|
||||||
|
|
||||||
|
logger = logging.getLogger('cream_installer')
|
||||||
|
logger.handlers = []
|
||||||
|
logger.propagate = False
|
||||||
|
logger.setLevel(logging.DEBUG if self.debug else logging.ERROR)
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
if self.debug:
|
||||||
|
self.logger.debug("=== Session Started ===")
|
||||||
|
self.logger.debug(f"Debug mode enabled - Log file: {log_file}")
|
||||||
|
self.logger.debug(f"System: {os.uname().sysname if hasattr(os, 'uname') else os.name}")
|
||||||
|
self.logger.debug(f"Python version: {subprocess.check_output(['python', '--version']).decode().strip()}")
|
||||||
|
self.logger.debug("Checking for Steam installation...")
|
||||||
|
|
||||||
|
def _log_debug(self, message):
|
||||||
|
"""Log debug message if debug mode is enabled"""
|
||||||
|
if self.debug and not self.logger:
|
||||||
|
self._setup_logging()
|
||||||
|
if self.logger:
|
||||||
|
self.logger.debug(message)
|
||||||
|
|
||||||
|
def _log_error(self, message):
|
||||||
|
"""Log error message, setting up logging if needed"""
|
||||||
|
if not self.logger:
|
||||||
|
self._setup_logging()
|
||||||
|
self.logger.error(message)
|
||||||
|
|
||||||
|
def check_requirements(self):
|
||||||
|
"""Check if all required commands and packages are available"""
|
||||||
|
missing_commands = []
|
||||||
|
missing_packages = []
|
||||||
|
|
||||||
|
# Check commands
|
||||||
|
required_commands = ['which', 'steam']
|
||||||
|
for cmd in required_commands:
|
||||||
|
if not subprocess.run(['which', cmd], capture_output=True).returncode == 0:
|
||||||
|
missing_commands.append(cmd)
|
||||||
|
self._log_error(f"Required command not found: {cmd}")
|
||||||
|
|
||||||
|
# Check packages
|
||||||
|
required_packages = ['requests', 'argparse', 'rich', 'json']
|
||||||
|
for package in required_packages:
|
||||||
|
try:
|
||||||
|
__import__(package)
|
||||||
|
except ImportError:
|
||||||
|
missing_packages.append(package)
|
||||||
|
self._log_error(f"Required Python package not found: {package}")
|
||||||
|
|
||||||
|
if missing_commands or missing_packages:
|
||||||
|
error_details = []
|
||||||
|
if missing_commands:
|
||||||
|
cmd_list = ', '.join(missing_commands)
|
||||||
|
error_details.append(f"Missing commands: {cmd_list}")
|
||||||
|
if missing_packages:
|
||||||
|
pkg_list = ', '.join(missing_packages)
|
||||||
|
error_details.append(f"Missing Python packages: {pkg_list}")
|
||||||
|
error_details.append("Install them using: pip install " + ' '.join(missing_packages))
|
||||||
|
|
||||||
|
raise RequirementsError("\n".join(error_details))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _is_excluded_app(self, app_id, name):
|
||||||
|
"""Check if the app should be excluded from the game list"""
|
||||||
|
excluded_ids = {
|
||||||
|
'228980', # Steamworks Common Redistributables
|
||||||
|
'1070560', # Steam Linux Runtime
|
||||||
|
'1391110', # Steam Linux Runtime - Soldier
|
||||||
|
'1628350', # Steam Linux Runtime - Sniper
|
||||||
|
'1493710', # Proton Experimental
|
||||||
|
'1826330' # Steam Linux Runtime - Scout
|
||||||
|
}
|
||||||
|
excluded_patterns = [
|
||||||
|
r'Proton \d+\.\d+',
|
||||||
|
r'Steam Linux Runtime',
|
||||||
|
r'Steamworks Common'
|
||||||
|
]
|
||||||
|
|
||||||
|
if app_id in excluded_ids:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for pattern in excluded_patterns:
|
||||||
|
if re.match(pattern, name, re.IGNORECASE):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _read_steam_registry(self):
|
||||||
|
"""Read Steam registry file"""
|
||||||
|
registry_path = os.path.expanduser('~/.steam/registry.vdf')
|
||||||
|
if os.path.exists(registry_path):
|
||||||
|
self._log_debug(f"Found Steam registry file: {registry_path}")
|
||||||
|
with open(registry_path, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
install_path = re.search(r'"InstallPath"\s*"([^"]+)"', content)
|
||||||
|
if install_path:
|
||||||
|
return install_path.group(1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _parse_vdf(self, file_path):
|
||||||
|
"""Parse Steam library folders VDF file"""
|
||||||
|
library_paths = []
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
content = file.read()
|
||||||
|
paths = re.findall(r'"path"\s*"(.*?)"', content, re.IGNORECASE)
|
||||||
|
library_paths.extend([os.path.normpath(path) for path in paths])
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"Failed to read {file_path}: {str(e)}")
|
||||||
|
return library_paths
|
||||||
|
|
||||||
|
def _find_steam_binary(self):
|
||||||
|
"""Find Steam binary location"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['which', 'steam'], stdout=subprocess.PIPE)
|
||||||
|
steam_path = result.stdout.decode('utf-8').strip()
|
||||||
|
if steam_path:
|
||||||
|
return os.path.dirname(steam_path)
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"Failed to locate steam binary: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_steam_library_folders(self, manual_path=""):
|
||||||
|
"""Find all Steam library folders"""
|
||||||
|
self._log_debug("Starting Steam library folder search")
|
||||||
|
|
||||||
|
search_list = [
|
||||||
|
os.path.expanduser('~/.steam/steam'),
|
||||||
|
os.path.expanduser('~/.local/share/Steam'),
|
||||||
|
os.path.expanduser('/home/deck/.steam/steam'),
|
||||||
|
os.path.expanduser('/home/deck/.local/share/Steam'),
|
||||||
|
'/mnt/Jogos/Steam',
|
||||||
|
'/run/media/mmcblk0p1',
|
||||||
|
os.path.expanduser('~/.var/app/com.valvesoftware.Steam/.local/share/Steam'),
|
||||||
|
os.path.expanduser('~/.var/app/com.valvesoftware.Steam/data/Steam/steamapps/common')
|
||||||
|
]
|
||||||
|
|
||||||
|
library_folders = []
|
||||||
|
try:
|
||||||
|
if manual_path:
|
||||||
|
self._log_debug(f"Manual game path provided: {manual_path}")
|
||||||
|
if os.path.exists(manual_path):
|
||||||
|
self._log_debug("Manual path exists, adding to library folders")
|
||||||
|
library_folders.append(manual_path)
|
||||||
|
else:
|
||||||
|
self._log_debug(f"Manual path does not exist: {manual_path}")
|
||||||
|
return library_folders
|
||||||
|
|
||||||
|
steam_binary_path = self._find_steam_binary()
|
||||||
|
if steam_binary_path:
|
||||||
|
self._log_debug(f"Found Steam binary at: {steam_binary_path}")
|
||||||
|
if steam_binary_path not in search_list:
|
||||||
|
search_list.append(steam_binary_path)
|
||||||
|
|
||||||
|
steam_install_path = self._read_steam_registry()
|
||||||
|
if steam_install_path:
|
||||||
|
self._log_debug(f"Found Steam installation path in registry: {steam_install_path}")
|
||||||
|
if steam_install_path not in search_list:
|
||||||
|
search_list.append(steam_install_path)
|
||||||
|
|
||||||
|
self._log_debug("Searching for Steam library folders in all potential locations")
|
||||||
|
for search_path in search_list:
|
||||||
|
self._log_debug(f"Checking path: {search_path}")
|
||||||
|
if os.path.exists(search_path):
|
||||||
|
steamapps_path = str(os.path.normpath(f"{search_path}/steamapps"))
|
||||||
|
if os.path.exists(steamapps_path):
|
||||||
|
self._log_debug(f"Found valid steamapps folder: {steamapps_path}")
|
||||||
|
library_folders.append(steamapps_path)
|
||||||
|
|
||||||
|
vdf_path = os.path.join(steamapps_path, 'libraryfolders.vdf')
|
||||||
|
if os.path.exists(vdf_path):
|
||||||
|
self._log_debug(f"Found libraryfolders.vdf at: {vdf_path}")
|
||||||
|
additional_paths = self._parse_vdf(vdf_path)
|
||||||
|
for path in additional_paths:
|
||||||
|
new_steamapps_path = os.path.join(path, 'steamapps')
|
||||||
|
if os.path.exists(new_steamapps_path):
|
||||||
|
self._log_debug(f"Found additional library folder: {new_steamapps_path}")
|
||||||
|
library_folders.append(new_steamapps_path)
|
||||||
|
|
||||||
|
self._log_debug(f"Found {len(library_folders)} total library folders")
|
||||||
|
for folder in library_folders:
|
||||||
|
self._log_debug(f"Library folder: {folder}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"Error finding Steam library folders: {e}")
|
||||||
|
self._log_debug(f"Stack trace:", exc_info=True)
|
||||||
|
return library_folders
|
||||||
|
|
||||||
|
def _parse_acf(self, file_path):
|
||||||
|
"""Parse Steam ACF file"""
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
data = file.read()
|
||||||
|
app_id = re.search(r'"appid"\s+"(\d+)"', data)
|
||||||
|
name = re.search(r'"name"\s+"([^"]+)"', data)
|
||||||
|
install_dir = re.search(r'"installdir"\s+"([^"]+)"', data)
|
||||||
|
return app_id.group(1), name.group(1), install_dir.group(1)
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"Error reading ACF file {file_path}: {e}")
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
def _check_proton_status(self, install_path):
|
||||||
|
"""
|
||||||
|
Check if a game requires Proton by looking for .exe files and Steam API DLLs
|
||||||
|
Returns: (needs_proton, steam_api_files)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
has_exe = False
|
||||||
|
steam_api_files = []
|
||||||
|
steam_api_patterns = ['steam_api.dll', 'steam_api64.dll']
|
||||||
|
|
||||||
|
for root, _, files in os.walk(install_path):
|
||||||
|
# Check for .exe files
|
||||||
|
if not has_exe and any(file.lower().endswith('.exe') for file in files):
|
||||||
|
has_exe = True
|
||||||
|
|
||||||
|
# Check for Steam API files
|
||||||
|
for file in files:
|
||||||
|
if file.lower() in steam_api_patterns:
|
||||||
|
steam_api_files.append(os.path.relpath(os.path.join(root, file), install_path))
|
||||||
|
|
||||||
|
# If we found both, we can stop searching
|
||||||
|
if has_exe and steam_api_files:
|
||||||
|
break
|
||||||
|
|
||||||
|
return has_exe, steam_api_files
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"Error checking Proton status: {e}")
|
||||||
|
return False, []
|
||||||
|
|
||||||
|
def find_steam_apps(self, library_folders):
|
||||||
|
"""Find all Steam apps in library folders"""
|
||||||
|
self._log_debug("Starting Steam apps search")
|
||||||
|
acf_pattern = re.compile(r'^appmanifest_(\d+)\.acf$')
|
||||||
|
games = {}
|
||||||
|
|
||||||
|
for folder in library_folders:
|
||||||
|
self._log_debug(f"Searching for games in: {folder}")
|
||||||
|
if os.path.exists(folder):
|
||||||
|
for item in os.listdir(folder):
|
||||||
|
if acf_pattern.match(item):
|
||||||
|
app_id, game_name, install_dir = self._parse_acf(os.path.join(folder, item))
|
||||||
|
if app_id and game_name and not self._is_excluded_app(app_id, game_name):
|
||||||
|
install_path = os.path.join(folder, 'common', install_dir)
|
||||||
|
if os.path.exists(install_path):
|
||||||
|
cream_installed = os.path.exists(os.path.join(install_path, 'cream.sh'))
|
||||||
|
needs_proton, steam_api_files = self._check_proton_status(install_path)
|
||||||
|
smoke_installed = self.check_smokeapi_status(install_path, steam_api_files) if needs_proton else False
|
||||||
|
|
||||||
|
games[app_id] = (
|
||||||
|
game_name, # [0] Name
|
||||||
|
cream_installed, # [1] CreamLinux status
|
||||||
|
install_path, # [2] Install path
|
||||||
|
needs_proton, # [3] Proton status
|
||||||
|
steam_api_files, # [4] Steam API files
|
||||||
|
smoke_installed # [5] SmokeAPI status
|
||||||
|
)
|
||||||
|
|
||||||
|
self._log_debug(f"Found game: {game_name} (App ID: {app_id})")
|
||||||
|
self._log_debug(f" Path: {install_path}")
|
||||||
|
self._log_debug(f" Status: Cream={cream_installed}, Proton={needs_proton}, Smoke={smoke_installed}")
|
||||||
|
if steam_api_files:
|
||||||
|
self._log_debug(f" Steam API files: {', '.join(steam_api_files)}")
|
||||||
|
|
||||||
|
self._log_debug(f"Found {len(games)} total games")
|
||||||
|
return games
|
||||||
|
|
||||||
|
def fetch_dlc_details(self, app_id, progress_callback=None):
|
||||||
|
"""Fetch DLC details for a game"""
|
||||||
|
base_url = f"https://store.steampowered.com/api/appdetails?appids={app_id}"
|
||||||
|
try:
|
||||||
|
response = requests.get(base_url)
|
||||||
|
data = response.json()
|
||||||
|
if str(app_id) not in data:
|
||||||
|
return []
|
||||||
|
|
||||||
|
app_data = data[str(app_id)]
|
||||||
|
if not app_data.get('success') or 'data' not in app_data:
|
||||||
|
return []
|
||||||
|
|
||||||
|
game_data = app_data['data']
|
||||||
|
dlcs = game_data.get("dlc", [])
|
||||||
|
dlc_details = []
|
||||||
|
|
||||||
|
total_dlcs = len(dlcs)
|
||||||
|
for index, dlc_id in enumerate(dlcs):
|
||||||
|
try:
|
||||||
|
time.sleep(0.3)
|
||||||
|
dlc_url = f"https://store.steampowered.com/api/appdetails?appids={dlc_id}"
|
||||||
|
dlc_response = requests.get(dlc_url)
|
||||||
|
|
||||||
|
if dlc_response.status_code == 200:
|
||||||
|
dlc_data = dlc_response.json()
|
||||||
|
if str(dlc_id) in dlc_data and "data" in dlc_data[str(dlc_id)]:
|
||||||
|
dlc_name = dlc_data[str(dlc_id)]["data"].get("name", "Unknown DLC")
|
||||||
|
dlc_details.append({"appid": dlc_id, "name": dlc_name})
|
||||||
|
elif dlc_response.status_code == 429:
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(index + 1, total_dlcs)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"Error fetching DLC {dlc_id}: {str(e)}")
|
||||||
|
|
||||||
|
return dlc_details
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self._log_error(f"Failed to fetch DLC details: {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def install_creamlinux(self, app_id, game_install_dir, dlcs):
|
||||||
|
"""Install CreamLinux for a game"""
|
||||||
|
try:
|
||||||
|
zip_url = self.config['creamlinux_release']
|
||||||
|
zip_path = os.path.join(game_install_dir, 'creamlinux.zip')
|
||||||
|
|
||||||
|
self._log_debug(f"Downloading CreamLinux from {zip_url}")
|
||||||
|
response = requests.get(zip_url)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise InstallationError(f"Failed to download CreamLinux (HTTP {response.status_code})")
|
||||||
|
|
||||||
|
self._log_debug(f"Writing zip file to {zip_path}")
|
||||||
|
with open(zip_path, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
self._log_debug("Extracting CreamLinux files")
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
|
zip_ref.extractall(game_install_dir)
|
||||||
|
except zipfile.BadZipFile:
|
||||||
|
raise InstallationError("Downloaded file is corrupted. Please try again.")
|
||||||
|
|
||||||
|
os.remove(zip_path)
|
||||||
|
|
||||||
|
cream_sh_path = os.path.join(game_install_dir, 'cream.sh')
|
||||||
|
self._log_debug(f"Setting permissions for {cream_sh_path}")
|
||||||
|
try:
|
||||||
|
os.chmod(cream_sh_path, os.stat(cream_sh_path).st_mode | stat.S_IEXEC)
|
||||||
|
except OSError as e:
|
||||||
|
raise InstallationError(f"Failed to set execute permissions: {str(e)}")
|
||||||
|
|
||||||
|
cream_api_path = os.path.join(game_install_dir, 'cream_api.ini')
|
||||||
|
self._log_debug(f"Creating config at {cream_api_path}")
|
||||||
|
try:
|
||||||
|
dlc_list = "\n".join([f"{dlc['appid']} = {dlc['name']}" for dlc in dlcs])
|
||||||
|
with open(cream_api_path, 'w') as f:
|
||||||
|
f.write(f"APPID = {app_id}\n[config]\nissubscribedapp_on_false_use_real = true\n[methods]\ndisable_steamapps_issubscribedapp = false\n[dlc]\n{dlc_list}")
|
||||||
|
except IOError as e:
|
||||||
|
raise InstallationError(f"Failed to create config file: {str(e)}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"Installation failed: {str(e)}")
|
||||||
|
if isinstance(e, InstallationError):
|
||||||
|
raise
|
||||||
|
raise InstallationError(f"Installation failed: {str(e)}")
|
||||||
|
|
||||||
|
def uninstall_creamlinux(self, install_path):
|
||||||
|
"""Uninstall CreamLinux from a game"""
|
||||||
|
try:
|
||||||
|
files_to_remove = ['cream.sh', 'cream_api.ini', 'cream_api.so', 'lib32Creamlinux.so', 'lib64Creamlinux.so']
|
||||||
|
for file in files_to_remove:
|
||||||
|
file_path = os.path.join(install_path, file)
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"Uninstallation failed: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def install_smokeapi(self, install_path, steam_api_files):
|
||||||
|
"""Install SmokeAPI for a Proton game"""
|
||||||
|
try:
|
||||||
|
# Construct the correct URL using latest version
|
||||||
|
response = requests.get(
|
||||||
|
f"{self.config['github_api']}{self.config['smokeapi_release']}/releases/latest"
|
||||||
|
)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise InstallationError("Failed to fetch latest SmokeAPI version")
|
||||||
|
|
||||||
|
latest_release = response.json()
|
||||||
|
latest_version = latest_release['tag_name']
|
||||||
|
zip_url = (
|
||||||
|
f"https://github.com/{self.config['smokeapi_release']}/releases/download/"
|
||||||
|
f"{latest_version}/SmokeAPI-{latest_version}.zip"
|
||||||
|
)
|
||||||
|
|
||||||
|
zip_path = os.path.join(install_path, 'smokeapi.zip')
|
||||||
|
|
||||||
|
self._log_debug(f"Downloading SmokeAPI from {zip_url}")
|
||||||
|
response = requests.get(zip_url)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise InstallationError(f"Failed to download SmokeAPI (HTTP {response.status_code})")
|
||||||
|
|
||||||
|
self._log_debug(f"Writing zip file to {zip_path}")
|
||||||
|
with open(zip_path, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
self._log_debug("Extracting SmokeAPI files")
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
|
for api_file in steam_api_files:
|
||||||
|
api_dir = os.path.dirname(os.path.join(install_path, api_file))
|
||||||
|
api_name = os.path.basename(api_file)
|
||||||
|
|
||||||
|
# Backup original file
|
||||||
|
original_path = os.path.join(api_dir, api_name)
|
||||||
|
backup_path = os.path.join(api_dir, api_name.replace('.dll', '_o.dll'))
|
||||||
|
|
||||||
|
self._log_debug(f"Processing {api_file}:")
|
||||||
|
self._log_debug(f" Original: {original_path}")
|
||||||
|
self._log_debug(f" Backup: {backup_path}")
|
||||||
|
|
||||||
|
# Only backup if not already backed up
|
||||||
|
if not os.path.exists(backup_path):
|
||||||
|
shutil.move(original_path, backup_path)
|
||||||
|
|
||||||
|
# Extract the appropriate DLL directly to the game directory
|
||||||
|
zip_ref.extract(api_name, api_dir)
|
||||||
|
|
||||||
|
self._log_debug(f" Installed SmokeAPI as: {original_path}")
|
||||||
|
|
||||||
|
except zipfile.BadZipFile:
|
||||||
|
raise InstallationError("Downloaded file is corrupted. Please try again.")
|
||||||
|
|
||||||
|
os.remove(zip_path)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"SmokeAPI installation failed: {str(e)}")
|
||||||
|
raise InstallationError(f"Failed to install SmokeAPI: {str(e)}")
|
||||||
|
|
||||||
|
def uninstall_smokeapi(self, install_path, steam_api_files):
|
||||||
|
"""Uninstall SmokeAPI and restore original files"""
|
||||||
|
try:
|
||||||
|
for api_file in steam_api_files:
|
||||||
|
api_dir = os.path.dirname(os.path.join(install_path, api_file))
|
||||||
|
api_name = os.path.basename(api_file)
|
||||||
|
|
||||||
|
original_path = os.path.join(api_dir, api_name)
|
||||||
|
backup_path = os.path.join(api_dir, api_name.replace('.dll', '_o.dll'))
|
||||||
|
|
||||||
|
if os.path.exists(backup_path):
|
||||||
|
if os.path.exists(original_path):
|
||||||
|
os.remove(original_path)
|
||||||
|
shutil.move(backup_path, original_path)
|
||||||
|
self._log_debug(f"Restored original file: {original_path}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"SmokeAPI uninstallation failed: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_smokeapi_status(self, install_path, steam_api_files):
|
||||||
|
"""Check if SmokeAPI is installed"""
|
||||||
|
try:
|
||||||
|
for api_file in steam_api_files:
|
||||||
|
backup_path = os.path.join(
|
||||||
|
install_path,
|
||||||
|
os.path.dirname(api_file),
|
||||||
|
os.path.basename(api_file).replace('.dll', '_o.dll')
|
||||||
|
)
|
||||||
|
if os.path.exists(backup_path):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"Error checking SmokeAPI status: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
class RequirementsError(Exception):
|
||||||
|
"""Raised when system requirements are not met"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NetworkError(Exception):
|
||||||
|
"""Raised when network-related operations fail"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class InstallationError(Exception):
|
||||||
|
"""Raised when installation operations fail"""
|
||||||
|
pass
|
||||||
201
main.py
Executable file
201
main.py
Executable file
@@ -0,0 +1,201 @@
|
|||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
from helper import SteamHelper, RequirementsError, NetworkError, InstallationError
|
||||||
|
from ui_handler import UIHandler
|
||||||
|
from updater import check_for_updates, UpdateError
|
||||||
|
|
||||||
|
def handle_dlc_operation(ui, helper, app_id, game_name, install_dir):
|
||||||
|
"""Handle DLC fetching and installation"""
|
||||||
|
ui.show_info(f"\nSelected: {game_name} (App ID: {app_id})")
|
||||||
|
|
||||||
|
with ui.create_progress_context() as progress:
|
||||||
|
progress_task = progress.add_task("🔍 Fetching DLC details...", total=None)
|
||||||
|
def update_progress(current, total):
|
||||||
|
progress.update(progress_task, completed=current, total=total)
|
||||||
|
dlcs = helper.fetch_dlc_details(app_id, update_progress)
|
||||||
|
|
||||||
|
if dlcs:
|
||||||
|
ui.show_dlc_table(dlcs)
|
||||||
|
if ui.get_user_confirmation("\nProceed with installation?"):
|
||||||
|
with ui.create_status_context("Installing CreamLinux..."):
|
||||||
|
success = helper.install_creamlinux(app_id, install_dir, dlcs)
|
||||||
|
if success:
|
||||||
|
ui.show_success("Installation complete!")
|
||||||
|
ui.show_launch_options(game_name)
|
||||||
|
else:
|
||||||
|
ui.show_warning("No DLCs found for this game.")
|
||||||
|
|
||||||
|
def handle_smokeapi_operation(ui, helper, install_path, steam_api_files, game_name, is_install=True):
|
||||||
|
"""Handle SmokeAPI installation/uninstallation"""
|
||||||
|
operation = "installation" if is_install else "uninstallation"
|
||||||
|
|
||||||
|
ui.show_info(f"\nProceeding with SmokeAPI {operation} for {game_name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with ui.create_status_context(f"{'Installing' if is_install else 'Uninstalling'} SmokeAPI..."):
|
||||||
|
if is_install:
|
||||||
|
success = helper.install_smokeapi(install_path, steam_api_files)
|
||||||
|
else:
|
||||||
|
success = helper.uninstall_smokeapi(install_path, steam_api_files)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
ui.show_success(f"Successfully {'installed' if is_install else 'uninstalled'} SmokeAPI!")
|
||||||
|
else:
|
||||||
|
ui.show_error(f"Failed to {'install' if is_install else 'uninstall'} SmokeAPI")
|
||||||
|
except Exception as e:
|
||||||
|
ui.show_error(str(e))
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Steam DLC Fetcher")
|
||||||
|
parser.add_argument("--manual", metavar='steamapps_path', help="Sets the steamapps path for faster operation", required=False)
|
||||||
|
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
|
||||||
|
parser.add_argument("--no-update", action="store_true", help="Skip update check")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
ui = UIHandler(debug=args.debug)
|
||||||
|
helper = SteamHelper(debug=args.debug)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not args.no_update:
|
||||||
|
try:
|
||||||
|
if check_for_updates(ui, helper):
|
||||||
|
return # Exit if update was performed
|
||||||
|
except UpdateError as e:
|
||||||
|
ui.show_error(f"Update failed: {str(e)}")
|
||||||
|
if not ui.get_user_confirmation("Would you like to continue anyway?"):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Use version from config instead of fetching
|
||||||
|
app_version = helper.config['version']
|
||||||
|
ui.show_header(app_version, args.debug)
|
||||||
|
|
||||||
|
helper.check_requirements()
|
||||||
|
except RequirementsError as e:
|
||||||
|
ui.show_error("Missing dependencies:", show_details=str(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with ui.create_status_context("Finding Steam library folders..."):
|
||||||
|
library_folders = helper.find_steam_library_folders(args.manual)
|
||||||
|
|
||||||
|
if not library_folders:
|
||||||
|
if args.manual:
|
||||||
|
ui.show_error(f"Could not find Steam library at specified path: {args.manual}")
|
||||||
|
else:
|
||||||
|
ui.show_warning("No Steam library folders found. Please enter the path manually.")
|
||||||
|
steamapps_path = ui.get_user_input("Enter Steamapps Path")
|
||||||
|
if len(steamapps_path) > 3 and os.path.exists(steamapps_path):
|
||||||
|
library_folders = [steamapps_path]
|
||||||
|
else:
|
||||||
|
ui.show_error("Invalid path or path does not exist!")
|
||||||
|
return
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Refresh games list at the start of each loop
|
||||||
|
with ui.create_status_context("Scanning for games..."):
|
||||||
|
games = helper.find_steam_apps(library_folders)
|
||||||
|
|
||||||
|
if not games:
|
||||||
|
ui.show_error("No Steam games found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
games_list = list(games.items())
|
||||||
|
ui.show_games_table(games_list)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ui.console.print("\n[dim]Enter game number or 'q' to quit[/dim]")
|
||||||
|
user_input = ui.get_user_input("Select game number")
|
||||||
|
|
||||||
|
if user_input.lower() == 'q':
|
||||||
|
return
|
||||||
|
|
||||||
|
choice = int(user_input) - 1
|
||||||
|
if not (0 <= choice < len(games_list)):
|
||||||
|
ui.show_error("Invalid selection.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Show the selected game and options
|
||||||
|
ui.clear_screen()
|
||||||
|
ui.show_header(app_version, args.debug)
|
||||||
|
ui.show_games_table(games_list, choice)
|
||||||
|
|
||||||
|
# Get game's status
|
||||||
|
is_installed = games_list[choice][1][1] # cream_status
|
||||||
|
needs_proton = games_list[choice][1][3] # needs_proton
|
||||||
|
steam_api_files = games_list[choice][1][4] # steam_api_files
|
||||||
|
smoke_status = games_list[choice][1][5] # smoke_status
|
||||||
|
game_info = (games_list[choice][0], games_list[choice][1])
|
||||||
|
|
||||||
|
# Different choices based on installation status and game type
|
||||||
|
if needs_proton and steam_api_files:
|
||||||
|
if smoke_status:
|
||||||
|
max_options = 2 # Uninstall and Go Back
|
||||||
|
action = ui.get_user_input("\nChoose action", choices=["1", "2"])
|
||||||
|
|
||||||
|
if action == "2": # Go back
|
||||||
|
ui.clear_screen()
|
||||||
|
ui.show_header(app_version, args.debug)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if action == "1": # Uninstall SmokeAPI
|
||||||
|
handle_smokeapi_operation(ui, helper, game_info[1][2], steam_api_files, game_info[1][0], False)
|
||||||
|
else:
|
||||||
|
max_options = 2 # Install and Go Back
|
||||||
|
action = ui.get_user_input("\nChoose action", choices=["1", "2"])
|
||||||
|
|
||||||
|
if action == "2": # Go back
|
||||||
|
ui.clear_screen()
|
||||||
|
ui.show_header(app_version, args.debug)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if action == "1": # Install SmokeAPI
|
||||||
|
handle_smokeapi_operation(ui, helper, game_info[1][2], steam_api_files, game_info[1][0], True)
|
||||||
|
else:
|
||||||
|
# Handle non-Proton games (original logic)
|
||||||
|
if is_installed:
|
||||||
|
action = ui.get_user_input("\nChoose action", choices=["1", "2", "3"])
|
||||||
|
if action == "3": # Go back
|
||||||
|
ui.clear_screen()
|
||||||
|
ui.show_header(app_version, args.debug)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if action == "1": # Fetch DLCs
|
||||||
|
handle_dlc_operation(ui, helper, game_info[0], game_info[1][0], game_info[1][2])
|
||||||
|
else: # Uninstall
|
||||||
|
if ui.get_user_confirmation("\nAre you sure you want to uninstall CreamLinux?"):
|
||||||
|
with ui.create_status_context("Uninstalling CreamLinux..."):
|
||||||
|
success = helper.uninstall_creamlinux(game_info[1][2])
|
||||||
|
if success:
|
||||||
|
ui.show_success(f"Successfully uninstalled CreamLinux from {game_info[1][0]}")
|
||||||
|
ui.show_uninstall_reminder()
|
||||||
|
else:
|
||||||
|
action = ui.get_user_input("\nChoose action", choices=["1", "2"])
|
||||||
|
if action == "2": # Go back
|
||||||
|
ui.clear_screen()
|
||||||
|
ui.show_header(app_version, args.debug)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Proceed with DLC operation
|
||||||
|
handle_dlc_operation(ui, helper, game_info[0], game_info[1][0], game_info[1][2])
|
||||||
|
|
||||||
|
# After any operation, ask if user wants to continue
|
||||||
|
if ui.get_user_confirmation("\nWould you like to perform another operation?"):
|
||||||
|
ui.clear_screen()
|
||||||
|
ui.show_header(app_version, args.debug)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
ui.show_error("Invalid input. Please enter a number.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, (RequirementsError, NetworkError, InstallationError)):
|
||||||
|
ui.show_error(str(e))
|
||||||
|
else:
|
||||||
|
helper._log_error(f"Unexpected error: {str(e)}")
|
||||||
|
ui.show_error(f"An unexpected error occurred: {str(e)}", show_exception=args.debug)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
183
ui_handler.py
Normal file
183
ui_handler.py
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
|
||||||
|
from rich.table import Table
|
||||||
|
from rich.prompt import Prompt, Confirm
|
||||||
|
from rich.text import Text
|
||||||
|
from rich import box
|
||||||
|
|
||||||
|
class UIHandler:
|
||||||
|
def __init__(self, debug=False):
|
||||||
|
self.console = Console()
|
||||||
|
self.debug = debug
|
||||||
|
|
||||||
|
def clear_screen(self):
|
||||||
|
"""Clear the console screen"""
|
||||||
|
import os
|
||||||
|
os.system('cls' if os.name == 'nt' else 'clear')
|
||||||
|
|
||||||
|
def show_header(self, app_version, debug_mode):
|
||||||
|
"""Display the application header"""
|
||||||
|
self.clear_screen()
|
||||||
|
logo = r"""
|
||||||
|
_ _ _ _ _ ____ _____ _ ________ _____
|
||||||
|
| | | | \ | | | / __ \ / ____| |/ / ____| __ \
|
||||||
|
| | | | \| | | | | | | | | ' /| |__ | |__) |
|
||||||
|
| | | | . ` | | | | | | | | < | __| | _ /
|
||||||
|
| |__| | |\ | |___| |__| | |____| . \| |____| | \ \
|
||||||
|
\____/|_| \_|______\____/ \_____|_|\_\______|_| \_\\
|
||||||
|
"""
|
||||||
|
|
||||||
|
info_text = f"\n> Made by Tickbase\n> GitHub: https://github.com/Novattz/creamlinux-installer\n> Version: {app_version}"
|
||||||
|
|
||||||
|
if debug_mode:
|
||||||
|
info_text += "\n[red][Running in DEBUG mode][/red]"
|
||||||
|
|
||||||
|
self.console.print(
|
||||||
|
Panel.fit(
|
||||||
|
Text.from_markup(f"{logo}\n{info_text}"),
|
||||||
|
style="cyan",
|
||||||
|
border_style="cyan",
|
||||||
|
box=box.ROUNDED,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def show_games_table(self, games_list, selected_idx=None):
|
||||||
|
"""Display the table of available games"""
|
||||||
|
table = Table(show_header=True, header_style="cyan", box=box.ROUNDED)
|
||||||
|
table.add_column("#", style="dim")
|
||||||
|
table.add_column("Game Name")
|
||||||
|
table.add_column("Type", style="dim")
|
||||||
|
table.add_column("Status")
|
||||||
|
|
||||||
|
for idx, (app_id, (name, cream_status, _, needs_proton, _, smoke_status)) in enumerate(games_list, 1):
|
||||||
|
if needs_proton:
|
||||||
|
status = "[green]✓ Smoke installed[/green]" if smoke_status else "[yellow]Not Installed[/yellow]"
|
||||||
|
else:
|
||||||
|
status = "[green]✓ Cream installed[/green]" if cream_status else "[yellow]Not Installed[/yellow]"
|
||||||
|
|
||||||
|
game_type = "[blue]Proton[/blue]" if needs_proton else "[green]Native[/green]"
|
||||||
|
|
||||||
|
# Highlight only the game name if selected
|
||||||
|
if selected_idx is not None and idx == selected_idx + 1:
|
||||||
|
name = f"[bold white on blue]{name}[/bold white on blue]"
|
||||||
|
|
||||||
|
table.add_row(
|
||||||
|
f"[bold cyan]{idx}[/bold cyan]",
|
||||||
|
name,
|
||||||
|
game_type,
|
||||||
|
status
|
||||||
|
)
|
||||||
|
|
||||||
|
self.console.print("\n[cyan]Available Games:[/cyan]")
|
||||||
|
self.console.print(table)
|
||||||
|
|
||||||
|
if selected_idx is not None:
|
||||||
|
# Get selected game details
|
||||||
|
_, (game_name, cream_status, install_path, needs_proton, steam_api_files, smoke_status) = games_list[selected_idx]
|
||||||
|
if needs_proton:
|
||||||
|
status = "[green]✓ Smoke installed[/green]" if smoke_status else "[yellow]Not Installed[/yellow]"
|
||||||
|
else:
|
||||||
|
status = "[green]✓ Cream installed[/green]" if cream_status else "[yellow]Not Installed[/yellow]"
|
||||||
|
|
||||||
|
game_type = "[blue]Proton[/blue]" if needs_proton else "[green]Native[/green]"
|
||||||
|
app_id = games_list[selected_idx][0]
|
||||||
|
|
||||||
|
# Show footer with selected game info
|
||||||
|
self.console.print(f"[dim]Selected: {game_name} (Type: {game_type}) - Status: {status}[/dim]")
|
||||||
|
|
||||||
|
# Show Steam API files if present for Proton games
|
||||||
|
if needs_proton and steam_api_files:
|
||||||
|
self.console.print("\n[cyan]Steam API files found:[/cyan]")
|
||||||
|
for api_file in steam_api_files:
|
||||||
|
self.console.print(f"[dim]- {api_file}[/dim]")
|
||||||
|
|
||||||
|
# Show options based on game type and status
|
||||||
|
self.console.print("\n[cyan]Selected Game Options:[/cyan]")
|
||||||
|
options = []
|
||||||
|
|
||||||
|
if needs_proton and steam_api_files:
|
||||||
|
if smoke_status:
|
||||||
|
options.append("1. Uninstall SmokeAPI")
|
||||||
|
options.append("2. Go Back")
|
||||||
|
else:
|
||||||
|
options.append("1. Install SmokeAPI")
|
||||||
|
options.append("2. Go Back")
|
||||||
|
else:
|
||||||
|
if cream_status:
|
||||||
|
options.append("1. Fetch DLCs")
|
||||||
|
options.append("2. Uninstall CreamLinux")
|
||||||
|
options.append("3. Go Back")
|
||||||
|
else:
|
||||||
|
options.append("1. Install CreamLinux")
|
||||||
|
options.append("2. Go Back")
|
||||||
|
|
||||||
|
for option in options:
|
||||||
|
self.console.print(option)
|
||||||
|
|
||||||
|
def show_dlc_table(self, dlcs):
|
||||||
|
"""Display the table of available DLCs"""
|
||||||
|
table = Table(show_header=True, header_style="cyan", box=box.ROUNDED)
|
||||||
|
table.add_column("#", style="dim")
|
||||||
|
table.add_column("DLC Name")
|
||||||
|
table.add_column("DLC ID", style="dim")
|
||||||
|
|
||||||
|
for idx, dlc in enumerate(dlcs, 1):
|
||||||
|
table.add_row(
|
||||||
|
f"[bold cyan]{idx}[/bold cyan]",
|
||||||
|
dlc['name'],
|
||||||
|
str(dlc['appid'])
|
||||||
|
)
|
||||||
|
|
||||||
|
self.console.print(Panel.fit(table, title="[bold cyan]📦 Found DLCs[/bold cyan]", border_style="cyan"))
|
||||||
|
|
||||||
|
def create_progress_context(self):
|
||||||
|
"""Create and return a progress context"""
|
||||||
|
return Progress()
|
||||||
|
|
||||||
|
def show_error(self, message, show_exception=False):
|
||||||
|
"""Display an error message"""
|
||||||
|
self.console.print(f"[red]{message}[/red]")
|
||||||
|
if show_exception:
|
||||||
|
self.console.print_exception()
|
||||||
|
|
||||||
|
def show_warning(self, message):
|
||||||
|
"""Display a warning message"""
|
||||||
|
self.console.print(f"[yellow]{message}[/yellow]")
|
||||||
|
|
||||||
|
def show_success(self, message):
|
||||||
|
"""Display a success message"""
|
||||||
|
self.console.print(f"[green]✓ {message}[/green]")
|
||||||
|
|
||||||
|
def show_info(self, message):
|
||||||
|
"""Display an info message"""
|
||||||
|
self.console.print(f"[cyan]{message}[/cyan]")
|
||||||
|
|
||||||
|
def create_status_context(self, message):
|
||||||
|
"""Create and return a status context"""
|
||||||
|
return self.console.status(f"[cyan]{message}", spinner="dots")
|
||||||
|
|
||||||
|
def get_user_input(self, prompt_message, choices=None):
|
||||||
|
"""Get user input with optional choices"""
|
||||||
|
if choices:
|
||||||
|
return Prompt.ask(prompt_message, choices=choices)
|
||||||
|
return Prompt.ask(prompt_message)
|
||||||
|
|
||||||
|
def get_user_confirmation(self, message):
|
||||||
|
"""Get user confirmation"""
|
||||||
|
return Confirm.ask(message)
|
||||||
|
|
||||||
|
def show_cream_options(self, game_name):
|
||||||
|
"""Show CreamLinux options for installed games"""
|
||||||
|
self.show_info(f"\nCreamLinux is installed for {game_name}")
|
||||||
|
self.console.print("1. Fetch DLC IDs")
|
||||||
|
self.console.print("2. Uninstall CreamLinux")
|
||||||
|
|
||||||
|
def show_launch_options(self, game_name):
|
||||||
|
"""Show launch options after installation"""
|
||||||
|
self.console.print("\n[yellow]Steam Launch Options:[/yellow]")
|
||||||
|
self.console.print(f"[green]sh ./cream.sh %command%[/green] for {game_name}")
|
||||||
|
|
||||||
|
def show_uninstall_reminder(self):
|
||||||
|
"""Show reminder about launch options after uninstall"""
|
||||||
|
self.console.print("\n[yellow]Remember to remove[/yellow] [green]'sh ./cream.sh %command%'[/green] [yellow]from launch options[/yellow]")
|
||||||
136
updater.py
Normal file
136
updater.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import requests
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
from zipfile import ZipFile
|
||||||
|
from helper import SteamHelper
|
||||||
|
|
||||||
|
class UpdateError(Exception):
|
||||||
|
"""Raised when update operations fail"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_for_updates(ui_handler, helper):
|
||||||
|
"""
|
||||||
|
Check for updates and handle the update process if needed
|
||||||
|
Returns True if update was performed, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
helper._log_debug("Starting update check")
|
||||||
|
|
||||||
|
helper._log_debug(f"Current version: {helper.config['version']}")
|
||||||
|
helper._log_debug(f"Checking for updates from: {helper.config['github_repo']}")
|
||||||
|
|
||||||
|
# Get latest release info from GitHub
|
||||||
|
api_url = f"{helper.config['github_api']}{helper.config['github_repo']}/releases/latest"
|
||||||
|
helper._log_debug(f"Fetching from: {api_url}")
|
||||||
|
|
||||||
|
response = requests.get(api_url)
|
||||||
|
latest_release = response.json()
|
||||||
|
latest_version = latest_release['tag_name']
|
||||||
|
helper._log_debug(f"Latest version found: {latest_version}")
|
||||||
|
|
||||||
|
if latest_version != helper.config['version']:
|
||||||
|
ui_handler.show_info(f"\nNew version available: {latest_version}")
|
||||||
|
ui_handler.show_info(f"Current version: {helper.config['version']}")
|
||||||
|
|
||||||
|
if ui_handler.get_user_confirmation("Would you like to update?"):
|
||||||
|
perform_update(latest_release, ui_handler, helper)
|
||||||
|
# Update config with new version
|
||||||
|
helper.config['version'] = latest_version
|
||||||
|
helper.save_config()
|
||||||
|
helper._log_debug("Updated config.json with new version")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
helper._log_error(f"Failed to check for updates: {str(e)}")
|
||||||
|
ui_handler.show_warning(f"Failed to check for updates: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def perform_update(release_info, ui_handler, helper):
|
||||||
|
"""Download and install the update"""
|
||||||
|
script_path = os.path.abspath(sys.argv[0])
|
||||||
|
script_dir = os.path.dirname(script_path)
|
||||||
|
helper._log_debug(f"Script directory: {script_dir}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find the asset URL
|
||||||
|
zip_asset = next((asset for asset in release_info['assets']
|
||||||
|
if asset['name'].endswith('.zip')), None)
|
||||||
|
|
||||||
|
if not zip_asset:
|
||||||
|
helper._log_debug("No zip asset found in release")
|
||||||
|
raise UpdateError("No valid update package found")
|
||||||
|
|
||||||
|
helper._log_debug(f"Found update package: {zip_asset['name']}")
|
||||||
|
|
||||||
|
# Create temporary directory
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
helper._log_debug(f"Created temp directory: {temp_dir}")
|
||||||
|
|
||||||
|
# Download the update
|
||||||
|
with ui_handler.create_status_context("Downloading update..."):
|
||||||
|
response = requests.get(zip_asset['browser_download_url'], stream=True)
|
||||||
|
zip_path = os.path.join(temp_dir, 'update.zip')
|
||||||
|
helper._log_debug(f"Downloading to: {zip_path}")
|
||||||
|
|
||||||
|
with open(zip_path, 'wb') as f:
|
||||||
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
|
if chunk:
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
# Extract update
|
||||||
|
with ui_handler.create_status_context("Installing update..."):
|
||||||
|
helper._log_debug("Extracting update files")
|
||||||
|
with ZipFile(zip_path, 'r') as zip_ref:
|
||||||
|
zip_ref.extractall(temp_dir)
|
||||||
|
|
||||||
|
# Create backup of current files
|
||||||
|
backup_dir = os.path.join(script_dir, 'backup')
|
||||||
|
os.makedirs(backup_dir, exist_ok=True)
|
||||||
|
helper._log_debug(f"Created backup directory: {backup_dir}")
|
||||||
|
|
||||||
|
# Copy current files to backup
|
||||||
|
for file in os.listdir(script_dir):
|
||||||
|
if file.endswith('.py') or file == 'config.json':
|
||||||
|
helper._log_debug(f"Backing up: {file}")
|
||||||
|
shutil.copy2(
|
||||||
|
os.path.join(script_dir, file),
|
||||||
|
os.path.join(backup_dir, file)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copy new files
|
||||||
|
for file in os.listdir(temp_dir):
|
||||||
|
if file.endswith('.py'):
|
||||||
|
helper._log_debug(f"Installing new file: {file}")
|
||||||
|
shutil.copy2(
|
||||||
|
os.path.join(temp_dir, file),
|
||||||
|
os.path.join(script_dir, file)
|
||||||
|
)
|
||||||
|
|
||||||
|
ui_handler.show_success("Update completed successfully!")
|
||||||
|
ui_handler.show_info("Restarting application...")
|
||||||
|
helper._log_debug("Preparing to restart application")
|
||||||
|
|
||||||
|
# Restart the application
|
||||||
|
python = sys.executable
|
||||||
|
os.execl(python, python, *sys.argv)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
helper._log_error(f"Update failed: {str(e)}")
|
||||||
|
# Attempt to restore from backup if available
|
||||||
|
backup_dir = os.path.join(script_dir, 'backup')
|
||||||
|
if os.path.exists(backup_dir):
|
||||||
|
helper._log_debug("Attempting to restore from backup")
|
||||||
|
for file in os.listdir(backup_dir):
|
||||||
|
helper._log_debug(f"Restoring: {file}")
|
||||||
|
shutil.copy2(
|
||||||
|
os.path.join(backup_dir, file),
|
||||||
|
os.path.join(script_dir, file)
|
||||||
|
)
|
||||||
|
shutil.rmtree(backup_dir)
|
||||||
|
|
||||||
|
raise UpdateError(f"Update failed: {str(e)}")
|
||||||
Reference in New Issue
Block a user