Skip to content

Commit 4a7c470

Browse files
authored
Update and rename settings.py to proton_manager.py
1 parent 12a84db commit 4a7c470

File tree

2 files changed

+270
-497
lines changed

2 files changed

+270
-497
lines changed

proton_manager.py

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import requests
2+
import json
3+
import tarfile
4+
import os
5+
import shutil
6+
import tempfile
7+
import time
8+
from datetime import datetime
9+
from config_manager import ConfigManager
10+
import logging
11+
12+
class ProtonManager:
13+
def __init__(self):
14+
self.protons_dir = ConfigManager.protons_dir
15+
os.makedirs(self.protons_dir, exist_ok=True)
16+
self.available_ge_cache = None
17+
self.available_official_stable_cache = None
18+
self.available_official_exp_cache = None
19+
self.cache_time = 0
20+
self.cache_duration = 3600 # 1 hour
21+
22+
def refresh_cache_if_needed(self):
23+
if time.time() - self.cache_time > self.cache_duration:
24+
self.available_ge_cache = self.get_available_ge()
25+
self.available_official_stable_cache = self.get_available_official(stable=True)
26+
self.available_official_exp_cache = self.get_available_official(stable=False)
27+
self.cache_time = time.time()
28+
29+
def get_installed_protons(self):
30+
protons = []
31+
for d in os.listdir(self.protons_dir):
32+
if os.path.isdir(os.path.join(self.protons_dir, d)):
33+
proton_path = os.path.join(self.protons_dir, d)
34+
version = d
35+
proton_type = 'GE' if d.startswith('GE-Proton') else 'Official'
36+
install_date = datetime.fromtimestamp(os.path.getctime(proton_path)).strftime('%Y-%m-%d')
37+
update_info = self.check_update(version, proton_type)
38+
status = 'Update Available' if update_info else 'Installed'
39+
protons.append({'version': version, 'type': proton_type, 'date': install_date, 'status': status})
40+
return sorted(protons, key=lambda x: x['version'])
41+
42+
def get_proton_path(self, version):
43+
base = os.path.join(self.protons_dir, version)
44+
for root, dirs, files in os.walk(base):
45+
if 'proton' in files:
46+
return os.path.join(root, 'proton')
47+
raise Exception(f"Proton binary not found in {version}")
48+
49+
def _version_key(self, version):
50+
version = version.replace('GE-Proton', '').replace('Proton-', '')
51+
parts = []
52+
current = ''
53+
for char in version:
54+
if char.isdigit() or char == '.':
55+
if current and not (current[-1].isdigit() or current[-1] == '.'):
56+
parts.append(current)
57+
current = ''
58+
current += char
59+
else:
60+
if current and (current[-1].isdigit() or current[-1] == '.'):
61+
parts.append(current)
62+
current = ''
63+
current += char
64+
if current:
65+
parts.append(current)
66+
def convert_part(part):
67+
try:
68+
return float(part) if '.' in part else int(part)
69+
except ValueError:
70+
return part
71+
return [convert_part(part) for part in parts]
72+
73+
def get_available_ge(self):
74+
if self.available_ge_cache is not None:
75+
return self.available_ge_cache
76+
for attempt in range(3):
77+
try:
78+
url = 'https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases'
79+
response = requests.get(url, timeout=15)
80+
response.raise_for_status()
81+
releases = json.loads(response.text)
82+
tags = [r['tag_name'] for r in releases if 'tag_name' in r and r['tag_name'].startswith('GE-Proton')]
83+
tags.sort(key=self._version_key, reverse=True)
84+
self.available_ge_cache = tags
85+
return tags
86+
except Exception as e:
87+
logging.error(f"Error fetching GE protons (attempt {attempt+1}/3): {e}")
88+
print(f"Error fetching GE protons (attempt {attempt+1}/3): {e}")
89+
time.sleep(3)
90+
logging.warning("Failed to fetch GE protons after retries, returning empty list")
91+
print("Failed to fetch GE protons after retries, returning empty list")
92+
self.available_ge_cache = []
93+
return []
94+
95+
def get_available_official(self, stable=True):
96+
if stable and self.available_official_stable_cache is not None:
97+
return self.available_official_stable_cache
98+
if not stable and self.available_official_exp_cache is not None:
99+
return self.available_official_exp_cache
100+
for attempt in range(3):
101+
try:
102+
url = 'https://api.github.com/repos/ValveSoftware/Proton/releases'
103+
response = requests.get(url, timeout=15)
104+
response.raise_for_status()
105+
releases = json.loads(response.text)
106+
filtered = [r['tag_name'] for r in releases if 'tag_name' in r]
107+
if stable:
108+
filtered = [t for t in filtered if 'experimental' not in t.lower() and 'hotfix' not in t.lower()]
109+
else:
110+
filtered = [t for t in filtered if 'experimental' in t.lower() or 'hotfix' in t.lower()]
111+
filtered.sort(key=self._version_key, reverse=True)
112+
if stable:
113+
self.available_official_stable_cache = filtered
114+
else:
115+
self.available_official_exp_cache = filtered
116+
return filtered
117+
except Exception as e:
118+
logging.error(f"Error fetching official protons (attempt {attempt+1}/3): {e}")
119+
print(f"Error fetching official protons (attempt {attempt+1}/3): {e}")
120+
time.sleep(3)
121+
logging.warning(f"Failed to fetch {'stable' if stable else 'experimental'} official protons after retries, returning empty list")
122+
print(f"Failed to fetch {'stable' if stable else 'experimental'} official protons after retries, returning empty list")
123+
if stable:
124+
self.available_official_stable_cache = []
125+
else:
126+
self.available_official_exp_cache = []
127+
return []
128+
129+
def install_proton(self, version, proton_type, progress_callback=None):
130+
try:
131+
repo = 'GloriousEggroll/proton-ge-custom' if proton_type == 'GE' else 'ValveSoftware/Proton'
132+
url = f'https://api.github.com/repos/{repo}/releases'
133+
response = requests.get(url, timeout=15)
134+
response.raise_for_status()
135+
releases = json.loads(response.text)
136+
selected_release = next((r for r in releases if r['tag_name'] == version), None)
137+
if not selected_release:
138+
logging.error(f"No release found for {version}")
139+
print(f"No release found for {version}")
140+
return False, f"No release found for {version}"
141+
assets = selected_release['assets']
142+
tar_asset = next((a for a in assets if a['name'].endswith('.tar.gz')), None)
143+
if not tar_asset:
144+
logging.error(f"No tar.gz asset found for {version}")
145+
print(f"No tar.gz asset found for {version}")
146+
return False, f"No tar.gz asset found for {version}"
147+
dl_url = tar_asset['browser_download_url']
148+
progress_callback("Downloading", 0, 100)
149+
with tempfile.NamedTemporaryFile(suffix='.tar.gz', delete=False) as temp_tar:
150+
response = requests.get(dl_url, stream=True, timeout=30)
151+
response.raise_for_status()
152+
total_size = int(response.headers.get('content-length', 0))
153+
downloaded = 0
154+
chunk_size = 8192
155+
with open(temp_tar.name, 'wb') as f:
156+
for chunk in response.iter_content(chunk_size=chunk_size):
157+
if chunk:
158+
f.write(chunk)
159+
downloaded += len(chunk)
160+
if progress_callback and total_size:
161+
progress_callback("Downloading", downloaded, total_size)
162+
temp_tar_path = temp_tar.name
163+
progress_callback("Extracting", 0, 100)
164+
extract_dir = os.path.join(self.protons_dir, version)
165+
os.makedirs(extract_dir, exist_ok=True)
166+
with tarfile.open(temp_tar_path) as tar:
167+
for member in tar.getmembers():
168+
member_path = os.path.join(extract_dir, member.name)
169+
abs_path = os.path.abspath(member_path)
170+
if not abs_path.startswith(os.path.abspath(extract_dir)):
171+
raise Exception("Path traversal detected in tar file")
172+
total_size = sum(m.size for m in tar.getmembers())
173+
extracted = 0
174+
for member in tar.getmembers():
175+
tar.extract(member, extract_dir)
176+
extracted += member.size
177+
if progress_callback and total_size:
178+
progress_callback("Extracting", extracted, total_size)
179+
subdirs = [d for d in os.listdir(extract_dir) if os.path.isdir(os.path.join(extract_dir, d))]
180+
if len(subdirs) == 1:
181+
subdir = os.path.join(extract_dir, subdirs[0])
182+
for item in os.listdir(subdir):
183+
shutil.move(os.path.join(subdir, item), extract_dir)
184+
os.rmdir(subdir)
185+
os.remove(temp_tar_path)
186+
if not os.path.exists(self.get_proton_path(version)):
187+
shutil.rmtree(extract_dir)
188+
return False, f"Proton binary not found after extraction for {version}"
189+
logging.info(f"Installed Proton {version} ({proton_type})")
190+
return True, "Success"
191+
except Exception as e:
192+
logging.error(f"Error installing {proton_type} proton {version}: {e}")
193+
print(f"Error installing {proton_type} proton {version}: {e}")
194+
return False, str(e)
195+
196+
def install_custom_tar(self, tar_path, version, progress_callback=None):
197+
try:
198+
extract_dir = os.path.join(self.protons_dir, version)
199+
os.makedirs(extract_dir, exist_ok=True)
200+
progress_callback("Extracting", 0, 100)
201+
with tarfile.open(tar_path) as tar:
202+
for member in tar.getmembers():
203+
member_path = os.path.join(extract_dir, member.name)
204+
abs_path = os.path.abspath(member_path)
205+
if not abs_path.startswith(os.path.abspath(extract_dir)):
206+
raise Exception("Path traversal detected in tar file")
207+
total_size = sum(m.size for m in tar.getmembers())
208+
extracted = 0
209+
for member in tar.getmembers():
210+
tar.extract(member, extract_dir)
211+
extracted += member.size
212+
if progress_callback and total_size:
213+
progress_callback("Extracting", extracted, total_size)
214+
subdirs = [d for d in os.listdir(extract_dir) if os.path.isdir(os.path.join(extract_dir, d))]
215+
if len(subdirs) == 1:
216+
subdir = os.path.join(extract_dir, subdirs[0])
217+
for item in os.listdir(subdir):
218+
shutil.move(os.path.join(subdir, item), extract_dir)
219+
os.rmdir(subdir)
220+
if not os.path.exists(self.get_proton_path(version)):
221+
shutil.rmtree(extract_dir)
222+
return False, f"Proton binary not found after extraction for {version}"
223+
logging.info(f"Installed custom Proton {version} from tar")
224+
return True, "Success"
225+
except Exception as e:
226+
logging.error(f"Error installing custom tar: {e}")
227+
print(f"Error installing custom tar: {e}")
228+
return False, str(e)
229+
230+
def install_custom_folder(self, src_folder, version):
231+
try:
232+
dest = os.path.join(self.protons_dir, version)
233+
shutil.copytree(src_folder, dest, dirs_exist_ok=True)
234+
if not os.path.exists(self.get_proton_path(version)):
235+
shutil.rmtree(dest)
236+
return False, f"Proton binary not found in folder for {version}"
237+
logging.info(f"Installed custom Proton {version} from folder")
238+
return True, "Success"
239+
except Exception as e:
240+
logging.error(f"Error installing custom folder: {e}")
241+
print(f"Error installing custom folder: {e}")
242+
return False, str(e)
243+
244+
def remove_proton(self, version):
245+
path = os.path.join(self.protons_dir, version)
246+
if not os.path.exists(path):
247+
return False
248+
try:
249+
shutil.rmtree(path)
250+
logging.info(f"Removed Proton {version}")
251+
return True
252+
except Exception as e:
253+
logging.error(f"Error removing proton: {e}")
254+
print(f"Error removing proton: {e}")
255+
return False
256+
257+
def check_update(self, version, proton_type):
258+
self.refresh_cache_if_needed()
259+
if proton_type == 'GE':
260+
available = self.available_ge_cache
261+
if available and available[0] != version:
262+
return ('GE', available[0])
263+
elif proton_type == 'Official':
264+
available_stable = self.available_official_stable_cache
265+
available_exp = self.available_official_exp_cache
266+
available = available_stable + available_exp
267+
if available and available[0] != version:
268+
new_type = 'Official' if available[0] in available_stable else 'Experimental'
269+
return (new_type, available[0])
270+
return None

0 commit comments

Comments
 (0)