2 Commits

Author SHA1 Message Date
Yannik Schmidt
8e5bfd9ae3 feat: implement age limit 2025-04-13 00:52:51 +02:00
Yannik Schmidt
4e9b85ee6d feat: fully working async install 2025-04-13 00:18:02 +02:00
6 changed files with 121 additions and 34 deletions

View File

@@ -34,6 +34,7 @@ db = None # app data-backend (i.e. LocalFS or FTP)
CONFIG_FILE = "gamevault_config.json" CONFIG_FILE = "gamevault_config.json"
def close_input_window(input_window): def close_input_window(input_window):
'''Close the config window and save the settings''' '''Close the config window and save the settings'''
@@ -188,7 +189,7 @@ def load_main():
app.title("Lan Vault: Overview") app.title("Lan Vault: Overview")
if not infowidget_window: if not infowidget_window:
infowidget_window = infowidget.ProgressBarApp(app) infowidget_window = infowidget.ProgressBarApp(app, data_backend=db)
# navbar should not expand when window is resized # navbar should not expand when window is resized
app.grid_rowconfigure(0, weight=0) app.grid_rowconfigure(0, weight=0)
@@ -337,6 +338,8 @@ if __name__ == "__main__":
password = config_loaded.get("Password:") password = config_loaded.get("Password:")
install_dir = config_loaded["Install dir:"] install_dir = config_loaded["Install dir:"]
backend_type = config_loaded["Select option:"] backend_type = config_loaded["Select option:"]
hide_above_age = config_loaded.get("hide_above_age") or 100
# fix abs path if not set # # fix abs path if not set #
if os.path.abspath(install_dir): if os.path.abspath(install_dir):
@@ -364,7 +367,8 @@ if __name__ == "__main__":
# add db backend # # add db backend #
if backend_type == "HTTP/HTTPS": if backend_type == "HTTP/HTTPS":
db = data_backend.HTTP(None, None, install_dir, remote_root_dir="./", server=server, progress_bar_wrapper=pgw, tkinter_root=app) db = data_backend.HTTP(None, None, install_dir, remote_root_dir="./", server=server, progress_bar_wrapper=pgw,
tkinter_root=app, hide_above_age=hide_above_age)
elif backend_type == "FTP/FTPS": elif backend_type == "FTP/FTPS":
db = data_backend.FTP(user, password, install_dir, server=server, db = data_backend.FTP(user, password, install_dir, server=server,
remote_root_dir=remote_root_dir, progress_bar_wrapper=pgw, tkinter_root=app) remote_root_dir=remote_root_dir, progress_bar_wrapper=pgw, tkinter_root=app)

View File

@@ -16,7 +16,7 @@ class DataBackend:
os.makedirs(cache_dir, exist_ok=True) os.makedirs(cache_dir, exist_ok=True)
def __init__(self, user, password, install_dir, server=None, remote_root_dir=None, def __init__(self, user, password, install_dir, server=None, remote_root_dir=None,
progress_bar_wrapper=None, tkinter_root=None): progress_bar_wrapper=None, tkinter_root=None, hide_above_age=100):
self.user = user self.user = user
self.password = password self.password = password
@@ -26,6 +26,7 @@ class DataBackend:
self.progress_bar_wrapper = progress_bar_wrapper self.progress_bar_wrapper = progress_bar_wrapper
self.root = tkinter_root self.root = tkinter_root
self.cache_dir = "./cache/" self.cache_dir = "./cache/"
self.hide_above_age = hide_above_age
def get(self, path, return_content=False): def get(self, path, return_content=False):
'''Return the contents of this path''' '''Return the contents of this path'''
@@ -111,6 +112,12 @@ class HTTP(DataBackend):
local_file = os.path.join(self.cache_dir, fullpath) local_file = os.path.join(self.cache_dir, fullpath)
print("Local Target is", local_file) print("Local Target is", local_file)
return local_file return local_file
def local_delete_cache_file(self, path, cache_dir=None):
'''Delete a local cache file'''
print("WARNING: removing:", self.get_local_target(path))
os.remove(self.get_local_target(path))
def get(self, path, cache_dir=None, return_content=False, wait=False): def get(self, path, cache_dir=None, return_content=False, wait=False):
@@ -146,6 +153,7 @@ class HTTP(DataBackend):
if return_content or wait: if return_content or wait:
print("Sync Requested")
# the content is needed for the UI now and not cached, it's needs to be downloaded synchroniously # # the content is needed for the UI now and not cached, it's needs to be downloaded synchroniously #
# as there cannot be a meaningful UI-draw without it. # # as there cannot be a meaningful UI-draw without it. #
# THIS IS THE OLD WAY # THIS IS THE OLD WAY
@@ -158,11 +166,28 @@ class HTTP(DataBackend):
# this is with streaming # this is with streaming
chunk_size = 1024 * 1024 * 5 # 5MB chunk_size = 1024 * 1024 * 5 # 5MB
r = requests.get(self._get_url(), params={"path": path, "as_string": True}, stream=True) r = requests.get(self._get_url(), params={"path": path, "as_string": True}, stream=True)
r.raise_for_status()
with open(local_file, "wb") as f: if path.endswith(".reg") or path.endswith(".txt"):
TYPE = "w"
else:
TYPE = "wb"
with open(local_file, TYPE) as f:
count = 0
for chunk in r.iter_content(chunk_size=chunk_size, decode_unicode=True): for chunk in r.iter_content(chunk_size=chunk_size, decode_unicode=True):
print(f"Doing chunk.. {chunk_size*count}")
if chunk: if chunk:
f.write(chunk)
try:
f.write(chunk)
except TypeError as e:
print("Cannot write:", chunk, "..to ", path, " because it is the wrong type.", e)
raise e
f.flush()
count += 1
if return_content: if return_content:
print("Content for", path, ":", r.text) print("Content for", path, ":", r.text)
@@ -240,4 +265,12 @@ class HTTP(DataBackend):
print("Software List:", software_list) print("Software List:", software_list)
print("Invalid:", list(filter(lambda x: x.invalid, software_list))) print("Invalid:", list(filter(lambda x: x.invalid, software_list)))
print("Valid:", list(filter(lambda x: not x.invalid, software_list))) print("Valid:", list(filter(lambda x: not x.invalid, software_list)))
return list(filter(lambda x: not x.invalid, software_list))
# filter valid #
results_valid = list(filter(lambda x: not x.invalid, software_list))
# filer age #
print("Age limit set to", self.hide_above_age, "games have", [x.age_limit for x in software_list])
results_with_age = list(filter(lambda x: x.age_limit <= self.hide_above_age, results_valid))
return results_with_age

6
db.py
View File

@@ -4,6 +4,9 @@ from sqlalchemy.orm import sessionmaker, scoped_session
Base = declarative_base() Base = declarative_base()
import logging
logging.basicConfig()
logging.getLogger('sqlalchemy').setLevel(logging.ERROR)
class Download(Base): class Download(Base):
__tablename__ = 'files' __tablename__ = 'files'
@@ -24,7 +27,8 @@ class Download(Base):
class Database: class Database:
def __init__(self, db_url="sqlite:///database.db"): def __init__(self, db_url="sqlite:///database.db"):
self.engine = create_engine(db_url, echo=True)
self.engine = create_engine(db_url, echo=False)
self.session_factory = sessionmaker(bind=self.engine) self.session_factory = sessionmaker(bind=self.engine)
self.Session = scoped_session(self.session_factory) # Thread-safe sessions self.Session = scoped_session(self.session_factory) # Thread-safe sessions

View File

@@ -18,8 +18,10 @@ import statekeeper
import os import os
class ProgressBarApp: class ProgressBarApp:
def __init__(self, parent): def __init__(self, parent, data_backend):
self.data_backend = data_backend
self.parent = parent
self.root = tk.Toplevel(parent) self.root = tk.Toplevel(parent)
self.root.title("Dynamic Progress Bars") self.root.title("Dynamic Progress Bars")
@@ -32,44 +34,51 @@ class ProgressBarApp:
self.progress_bars = [] # Store tuples of (progressbar, frame, duration, delete_button) self.progress_bars = [] # Store tuples of (progressbar, frame, duration, delete_button)
self.running = True self.running = True
threading.Thread(target=self.add_progress_bars, daemon=True).start() self.root.after(0, self.start_tracking_progress_bars)
def add_progress_bars(self): def start_tracking_progress_bars(self):
already_tracked = set() self.already_tracked = set()
while self.running: self.check_for_new_progress_bars()
downloads = set(statekeeper.get_download()) def check_for_new_progress_bars(self):
new = downloads - already_tracked
already_tracked |= set(downloads)
for element in new: downloads = set(statekeeper.get_download())
new = downloads - self.already_tracked
self.already_tracked |= downloads
frame = tk.Frame(self.frame) for element in new:
frame.pack(fill=tk.X, pady=2) frame = tk.Frame(self.frame)
frame.pack(fill=tk.X, pady=2)
progress = ttk.Progressbar(frame, length=200, mode='determinate') progress = ttk.Progressbar(frame, length=200, mode='determinate')
progress.pack(side=tk.LEFT, padx=5) progress.pack(side=tk.LEFT, padx=5)
delete_button = tk.Button(frame, text="Delete", command=lambda f=frame: self.delete_progress(f), state=tk.DISABLED) delete_button = tk.Button(frame, text="Delete", command=lambda f=frame: self.delete_progress(f), state=tk.DISABLED)
delete_button.pack(side=tk.LEFT, padx=5) delete_button.pack(side=tk.LEFT, padx=5)
label = tk.Label(frame, text=os.path.basename(element.path)) label = tk.Label(frame, text=os.path.basename(element.path))
label.pack(side=tk.LEFT, padx=5) label.pack(side=tk.LEFT, padx=5)
self.progress_bars.insert(0, (progress, frame, delete_button)) # Insert at the top self.progress_bars.insert(0, (progress, frame, delete_button)) # Insert at the top
frame.pack(fill=tk.X, pady=2, before=self.frame.winfo_children()[-1] if self.frame.winfo_children() else None) frame.pack(fill=tk.X, pady=2, before=self.frame.winfo_children()[-1] if self.frame.winfo_children() else None)
print("Starting tracker for", element.path) print("Starting tracker for", element.path)
threading.Thread(target=self.fill_progress, args=(progress, element.path, frame, delete_button), daemon=True).start() threading.Thread(target=self.fill_progress, args=(progress, element.path, frame, delete_button), daemon=True).start()
time.sleep(2) # Wait before adding a new progress bar # Schedule the next check in 2 seconds
if self.running:
self.root.after(2000, self.check_for_new_progress_bars)
def fill_progress(self, progress, path, frame, delete_button): def fill_progress(self, progress, path, frame, delete_button):
fail_count = 0 fail_count = 0
same_size_count = 0
prev_precent = 0
while True: while True:
print("Checking download progress..")
if not progress.winfo_exists(): # Check if progress bar still exists if not progress.winfo_exists(): # Check if progress bar still exists
return return
@@ -84,13 +93,29 @@ class ProgressBarApp:
continue continue
print("Percent filled:", percent_filled, path) print("Percent filled:", percent_filled, path)
if not percent_filled or percent_filled >= 99.9: if percent_filled >= 99.9:
self.root.after(0, progress.config, { "value" : 100 }) self.root.after(0, progress.config, { "value" : 100 })
print("Finished", path)
break break
else: else:
self.root.after(0, progress.config, { "value" : percent_filled }) self.root.after(0, progress.config, { "value" : percent_filled })
time.sleep(0.5) time.sleep(0.5)
# check for stuck downloads #
print("same size count", same_size_count)
if prev_precent == percent_filled:
same_size_count += 1
else:
same_size_count = 0
if same_size_count > 100:
self.root.after(0, delete_button.config, {"state": tk.NORMAL, "text": "Failed - Delete file manually!"})
self.progress_bars.append((progress, frame, path, delete_button))
self.update_delete_all_button()
statekeeper.log_end_download(path)
self.data_backend.local_delete_cache_file(path)
break
prev_precent = percent_filled
# handle finished download # # handle finished download #
self.root.after(0, delete_button.config, {"state": tk.NORMAL}) self.root.after(0, delete_button.config, {"state": tk.NORMAL})
self.progress_bars.append((progress, frame, path, delete_button)) self.progress_bars.append((progress, frame, path, delete_button))
@@ -98,7 +123,7 @@ class ProgressBarApp:
def delete_progress(self, frame): def delete_progress(self, frame):
frame.destroy() frame.destroy()
self.progress_bars = [(p, f, d, b) for p, f, d, b in self.progress_bars if f != frame] self.progress_bars = [(p, f, d) for p, f, d in self.progress_bars if f != frame]
self.update_delete_all_button() self.update_delete_all_button()
def delete_all_finished(self): def delete_all_finished(self):

View File

@@ -12,6 +12,7 @@ import threading
import sys import sys
import tkinter import tkinter
import statekeeper import statekeeper
from tkinter import messagebox
class Software: class Software:
@@ -58,6 +59,7 @@ class Software:
self.run_exe = meta.get("run_exe") self.run_exe = meta.get("run_exe")
self.installer = meta.get("installer") self.installer = meta.get("installer")
self.installer_no_admin = meta.get("installer_no_admin") self.installer_no_admin = meta.get("installer_no_admin")
self.age_limit = meta.get("age_limit") or 20
self.pictures = [ self.backend.get(pp, self.cache_dir) for pp in self.pictures = [ self.backend.get(pp, self.cache_dir) for pp in
self.backend.list(os.path.join(self.directory, "pictures"), fullpaths=True) ] self.backend.list(os.path.join(self.directory, "pictures"), fullpaths=True) ]
@@ -82,8 +84,16 @@ class Software:
'''Extract a cached, downloaded zip to the target location''' '''Extract a cached, downloaded zip to the target location'''
software_path = os.path.join(target, self.title) software_path = os.path.join(target, self.title)
if os.path.isdir(software_path): if os.path.isdir(software_path):
return # TODO better skip overwrite = messagebox.askyesno(
"Overwrite Existing Directory",
f"The directory '{software_path}' already exists.\nDo you want to overwrite it?"
)
if not overwrite:
print("Skipping install as instructed by user...")
return
os.makedirs(software_path, exist_ok=True) os.makedirs(software_path, exist_ok=True)
with zipfile.ZipFile(cache_src, 'r') as zip_ref: with zipfile.ZipFile(cache_src, 'r') as zip_ref:
@@ -99,6 +109,7 @@ class Software:
#self.progress_bar_wrapper.set_text( #self.progress_bar_wrapper.set_text(
# text="Extracting: {:.2f}%".format(count/len(total_count)*100)) # text="Extracting: {:.2f}%".format(count/len(total_count)*100))
except zipfile.error as e: except zipfile.error as e:
print(e)
pass # TODO ??? pass # TODO ???
#zip_ref.extractall(software_path) #zip_ref.extractall(software_path)
@@ -137,13 +148,17 @@ class Software:
local_file = self.backend.get(remote_file, self.cache_dir, wait=True) local_file = self.backend.get(remote_file, self.cache_dir, wait=True)
statekeeper.log_end_download(remote_file) statekeeper.log_end_download(remote_file)
print("Deciding on installer...")
# execute or unpack # # execute or unpack #
if local_file.endswith(".exe"): if local_file.endswith(".exe"):
print("Target is an executable.. running as installer.")
if os.name != "nt" and not os.path.isabs(local_file): if os.name != "nt" and not os.path.isabs(local_file):
# need abs path for wine # # need abs path for wine #
local_file = os.path.join(os.getcwd(), local_file) local_file = os.path.join(os.getcwd(), local_file)
localaction.run_exe(local_file) localaction.run_exe(local_file)
elif local_file.endswith(".zip"): elif local_file.endswith(".zip"):
print("Target is a zip.. unpacking first.")
self._extract_to_target(local_file, self.backend.install_dir) self._extract_to_target(local_file, self.backend.install_dir)
# download & install registry files # # download & install registry files #

View File

@@ -44,9 +44,9 @@ def _download(url, path):
def log_begin_download(path, local_path, url): def log_begin_download(path, local_path, url):
session = db.session() session = db.session()
print("Current path", path) print("Download path", path)
path_exists = session.query(Download).filter(and_(Download.path==path, Download.finished==False)).first() path_exists = session.query(Download).filter(and_(Download.path==path, Download.finished==False)).first()
if path_exists: if path_exists and False: # TODO FIX THIS
print("DAFUG", path_exists) print("DAFUG", path_exists)
print("WTF", path_exists.path) print("WTF", path_exists.path)
raise AssertionError("ERROR: {} is already downloading.".format(path)) raise AssertionError("ERROR: {} is already downloading.".format(path))
@@ -59,6 +59,7 @@ def log_begin_download(path, local_path, url):
def log_end_download(path): def log_end_download(path):
print("Downlod end logged", path)
session = db.session() session = db.session()
obj = session.query(Download).filter(Download.path==path).first() obj = session.query(Download).filter(Download.path==path).first()
if not obj: if not obj:
@@ -99,11 +100,16 @@ def get_percent_filled(path):
session = db.session() session = db.session()
obj = session.query(Download).filter(Download.path==path, Download.finished==False).first() obj = session.query(Download).filter(Download.path==path, Download.finished==False).first()
if not obj:
return 100 # means its finished
size = _bytes_to_mb(os.stat(obj.local_path).st_size) size = _bytes_to_mb(os.stat(obj.local_path).st_size)
total_size = get_download_size(obj.path) total_size = get_download_size(obj.path)
session.close() session.close()
if total_size == 0: if total_size == 0:
return 0 return 0
print("Current filled:", size / total_size * 100)
return size / total_size * 100 return size / total_size * 100
def get_download(path=None): def get_download(path=None):