mirror of
https://github.com/blw1138/Zordon.git
synced 2026-02-05 21:56:10 +00:00
Settings Window (#124)
* Initial commit for settings window * More WIP for the Settings panel * Added Local Files section to Settings * More WIP on Settings * Add ability to ignore system builds * Improvements to Launch and Delete buttons * Fix issue where icons were not loading * Network password settings WIP * Update label * Import and naming fixes * Speed improvements to launch * Update requirements.txt * Update Windows CPU name lookup * Add missing default values to a few settings * More settings fixes * Fix Windows Path issue * Added hard types for getting settings values * More UI cleanup * Correctly refresh Engines list after downloading new engine * Improve downloader with UI progress * More download improvements * Add Settings Button to Toolbar
This commit is contained in:
@@ -8,8 +8,7 @@ from src.engines.blender.blender_engine import Blender
|
||||
from src.engines.core.base_downloader import EngineDownloader
|
||||
from src.utilities.misc_helper import current_system_os, current_system_cpu
|
||||
|
||||
# url = "https://download.blender.org/release/"
|
||||
url = "https://ftp.nluug.nl/pub/graphics/blender/release/" # much faster mirror for testing
|
||||
url = "https://download.blender.org/release/"
|
||||
|
||||
logger = logging.getLogger()
|
||||
supported_formats = ['.zip', '.tar.xz', '.dmg']
|
||||
@@ -88,8 +87,8 @@ class BlenderDownloader(EngineDownloader):
|
||||
threads = []
|
||||
results = [[] for _ in majors]
|
||||
|
||||
def thread_function(major_version, index, system_os, cpu):
|
||||
results[index] = cls.__get_minor_versions(major_version, system_os, cpu)
|
||||
def thread_function(major_version, index, system_os_t, cpu_t):
|
||||
results[index] = cls.__get_minor_versions(major_version, system_os_t, cpu_t)
|
||||
|
||||
for i, m in enumerate(majors):
|
||||
thread = threading.Thread(target=thread_function, args=(m, i, system_os, cpu))
|
||||
@@ -126,7 +125,7 @@ class BlenderDownloader(EngineDownloader):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def download_engine(cls, version, download_location, system_os=None, cpu=None, timeout=120):
|
||||
def download_engine(cls, version, download_location, system_os=None, cpu=None, timeout=120, progress_callback=None):
|
||||
system_os = system_os or current_system_os()
|
||||
cpu = cpu or current_system_cpu()
|
||||
|
||||
@@ -136,7 +135,7 @@ class BlenderDownloader(EngineDownloader):
|
||||
minor_versions = [x for x in cls.__get_minor_versions(major_version, system_os, cpu) if
|
||||
x['version'] == version]
|
||||
cls.download_and_extract_app(remote_url=minor_versions[0]['url'], download_location=download_location,
|
||||
timeout=timeout)
|
||||
timeout=timeout, progress_callback=progress_callback)
|
||||
except IndexError:
|
||||
logger.error("Cannot find requested engine")
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ class EngineDownloader:
|
||||
raise NotImplementedError(f"version_is_available_to_download not implemented for {cls.__class__.__name__}")
|
||||
|
||||
@classmethod
|
||||
def download_engine(cls, version, download_location, system_os=None, cpu=None, timeout=120):
|
||||
def download_engine(cls, version, download_location, system_os=None, cpu=None, timeout=120, progress_callback=None):
|
||||
"""Downloads the requested version of the rendering engine to the given download location.
|
||||
|
||||
This method should be overridden in a subclass to implement the logic for downloading
|
||||
@@ -88,6 +88,7 @@ class EngineDownloader:
|
||||
system_os (str, optional): Desired OS ('linux', 'macos', 'windows'). Defaults to system os.
|
||||
cpu (str, optional): The CPU architecture for which to download the engine. Default is system cpu.
|
||||
timeout (int, optional): The maximum time in seconds to wait for the download. Default is 120 seconds.
|
||||
progress_callback (callable, optional): A callback function that is called periodically with the current download progress.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If the method is not overridden in a subclass.
|
||||
@@ -125,7 +126,7 @@ class EngineDownloader:
|
||||
# --------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def download_and_extract_app(cls, remote_url, download_location, timeout=120):
|
||||
def download_and_extract_app(cls, remote_url, download_location, timeout=120, progress_callback=None):
|
||||
"""Downloads an application from the given remote URL and extracts it to the specified location.
|
||||
|
||||
This method handles the downloading of the application, supports multiple archive formats,
|
||||
@@ -136,6 +137,7 @@ class EngineDownloader:
|
||||
remote_url (str): The URL of the application to download.
|
||||
download_location (str): The directory where the application should be extracted.
|
||||
timeout (int, optional): The maximum time in seconds to wait for the download. Default is 120 seconds.
|
||||
progress_callback (callable, optional): A callback function that is called periodically with the current download progress.
|
||||
|
||||
Returns:
|
||||
str: The path to the directory where the application was extracted.
|
||||
@@ -154,6 +156,8 @@ class EngineDownloader:
|
||||
and return without downloading or extracting.
|
||||
- Temporary files created during the download process are cleaned up after completion.
|
||||
"""
|
||||
if progress_callback:
|
||||
progress_callback(0)
|
||||
|
||||
# Create a temp download directory
|
||||
temp_download_dir = tempfile.mkdtemp()
|
||||
@@ -166,7 +170,7 @@ class EngineDownloader:
|
||||
|
||||
if os.path.exists(os.path.join(download_location, output_dir_name)):
|
||||
logger.error(f"Engine download for {output_dir_name} already exists")
|
||||
return
|
||||
return None
|
||||
|
||||
if not os.path.exists(temp_downloaded_file_path):
|
||||
# Make a GET request to the URL with stream=True to enable streaming
|
||||
@@ -182,20 +186,26 @@ class EngineDownloader:
|
||||
progress_bar = tqdm(total=file_size, unit="B", unit_scale=True)
|
||||
|
||||
# Open a file for writing in binary mode
|
||||
total_saved = 0
|
||||
with open(temp_downloaded_file_path, "wb") as file:
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
if chunk:
|
||||
# Write the chunk to the file
|
||||
file.write(chunk)
|
||||
total_saved += len(chunk)
|
||||
# Update the progress bar
|
||||
progress_bar.update(len(chunk))
|
||||
if progress_callback:
|
||||
percent = float(total_saved) / float(file_size)
|
||||
progress_callback(percent)
|
||||
|
||||
# Close the progress bar
|
||||
progress_callback(1.0)
|
||||
progress_bar.close()
|
||||
logger.info(f"Successfully downloaded {os.path.basename(temp_downloaded_file_path)}")
|
||||
else:
|
||||
logger.error(f"Failed to download the file. Status code: {response.status_code}")
|
||||
return
|
||||
return None
|
||||
|
||||
os.makedirs(download_location, exist_ok=True)
|
||||
|
||||
|
||||
@@ -23,6 +23,14 @@ class EngineManager:
|
||||
def supported_engines():
|
||||
return [Blender, FFMPEG]
|
||||
|
||||
@classmethod
|
||||
def downloadable_engines(cls):
|
||||
return [engine for engine in cls.supported_engines() if hasattr(engine, "downloader") and engine.downloader()]
|
||||
|
||||
@classmethod
|
||||
def active_downloads(cls) -> list:
|
||||
return [x for x in cls.download_tasks if x.is_alive()]
|
||||
|
||||
@classmethod
|
||||
def engine_with_name(cls, engine_name):
|
||||
for obj in cls.supported_engines():
|
||||
@@ -30,7 +38,15 @@ class EngineManager:
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def get_engines(cls, filter_name=None, include_corrupt=False):
|
||||
def update_all_engines(cls):
|
||||
for engine in cls.downloadable_engines():
|
||||
update_available = cls.is_engine_update_available(engine)
|
||||
if update_available:
|
||||
update_available['name'] = engine.name()
|
||||
cls.download_engine(engine.name(), update_available['version'], background=True)
|
||||
|
||||
@classmethod
|
||||
def get_engines(cls, filter_name=None, include_corrupt=False, ignore_system=False):
|
||||
|
||||
if not cls.engines_path:
|
||||
raise FileNotFoundError("Engine path is not set")
|
||||
@@ -63,13 +79,13 @@ class EngineManager:
|
||||
)
|
||||
result_dict['path'] = path
|
||||
|
||||
# fetch version number from binary - helps detect corrupted downloads
|
||||
binary_version = eng(path).version()
|
||||
if not binary_version:
|
||||
logger.warning(f"Possible corrupt {eng.name()} {result_dict['version']} install detected: {path}")
|
||||
if not include_corrupt:
|
||||
continue
|
||||
result_dict['version'] = binary_version or 'error'
|
||||
# fetch version number from binary - helps detect corrupted downloads - disabled due to perf issues
|
||||
# binary_version = eng(path).version()
|
||||
# if not binary_version:
|
||||
# logger.warning(f"Possible corrupt {eng.name()} {result_dict['version']} install detected: {path}")
|
||||
# if not include_corrupt:
|
||||
# continue
|
||||
# result_dict['version'] = binary_version or 'error'
|
||||
|
||||
# Add the result dictionary to results if it matches the filter_name or if no filter is applied
|
||||
if not filter_name or filter_name == result_dict['engine']:
|
||||
@@ -92,46 +108,47 @@ class EngineManager:
|
||||
'type': 'system'
|
||||
}
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
futures = {
|
||||
executor.submit(fetch_engine_details, eng, include_corrupt): eng.name()
|
||||
for eng in cls.supported_engines()
|
||||
if eng.default_engine_path() and (not filter_name or filter_name == eng.name())
|
||||
}
|
||||
if not ignore_system:
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
futures = {
|
||||
executor.submit(fetch_engine_details, eng, include_corrupt): eng.name()
|
||||
for eng in cls.supported_engines()
|
||||
if eng.default_engine_path() and (not filter_name or filter_name == eng.name())
|
||||
}
|
||||
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
result = future.result()
|
||||
if result:
|
||||
results.append(result)
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
result = future.result()
|
||||
if result:
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def all_versions_for_engine(cls, engine_name, include_corrupt=False):
|
||||
versions = cls.get_engines(filter_name=engine_name, include_corrupt=include_corrupt)
|
||||
def all_versions_for_engine(cls, engine_name, include_corrupt=False, ignore_system=False):
|
||||
versions = cls.get_engines(filter_name=engine_name, include_corrupt=include_corrupt, ignore_system=ignore_system)
|
||||
sorted_versions = sorted(versions, key=lambda x: x['version'], reverse=True)
|
||||
return sorted_versions
|
||||
|
||||
@classmethod
|
||||
def newest_engine_version(cls, engine, system_os=None, cpu=None):
|
||||
def newest_engine_version(cls, engine, system_os=None, cpu=None, ignore_system=None):
|
||||
system_os = system_os or current_system_os()
|
||||
cpu = cpu or current_system_cpu()
|
||||
|
||||
try:
|
||||
filtered = [x for x in cls.all_versions_for_engine(engine) if x['system_os'] == system_os and
|
||||
x['cpu'] == cpu]
|
||||
filtered = [x for x in cls.all_versions_for_engine(engine, ignore_system=ignore_system)
|
||||
if x['system_os'] == system_os and x['cpu'] == cpu]
|
||||
return filtered[0]
|
||||
except IndexError:
|
||||
logger.error(f"Cannot find newest engine version for {engine}-{system_os}-{cpu}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def is_version_downloaded(cls, engine, version, system_os=None, cpu=None):
|
||||
def is_version_downloaded(cls, engine, version, system_os=None, cpu=None, ignore_system=False):
|
||||
system_os = system_os or current_system_os()
|
||||
cpu = cpu or current_system_cpu()
|
||||
|
||||
filtered = [x for x in cls.get_engines(filter_name=engine) if x['system_os'] == system_os and
|
||||
x['cpu'] == cpu and x['version'] == version]
|
||||
filtered = [x for x in cls.get_engines(filter_name=engine, ignore_system=ignore_system) if
|
||||
x['system_os'] == system_os and x['cpu'] == cpu and x['version'] == version]
|
||||
return filtered[0] if filtered else False
|
||||
|
||||
@classmethod
|
||||
@@ -164,7 +181,7 @@ class EngineManager:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def download_engine(cls, engine, version, system_os=None, cpu=None, background=False):
|
||||
def download_engine(cls, engine, version, system_os=None, cpu=None, background=False, ignore_system=False):
|
||||
|
||||
engine_to_download = cls.engine_with_name(engine)
|
||||
existing_task = cls.get_existing_download_task(engine, version, system_os, cpu)
|
||||
@@ -172,10 +189,10 @@ class EngineManager:
|
||||
logger.debug(f"Already downloading {engine} {version}")
|
||||
if not background:
|
||||
existing_task.join() # If download task exists, wait until it's done downloading
|
||||
return
|
||||
return None
|
||||
elif not engine_to_download.downloader():
|
||||
logger.warning("No valid downloader for this engine. Please update this software manually.")
|
||||
return
|
||||
return None
|
||||
elif not cls.engines_path:
|
||||
raise FileNotFoundError("Engines path must be set before requesting downloads")
|
||||
|
||||
@@ -187,7 +204,7 @@ class EngineManager:
|
||||
return thread
|
||||
|
||||
thread.join()
|
||||
found_engine = cls.is_version_downloaded(engine, version, system_os, cpu) # Check that engine downloaded
|
||||
found_engine = cls.is_version_downloaded(engine, version, system_os, cpu, ignore_system) # Check that engine downloaded
|
||||
if not found_engine:
|
||||
logger.error(f"Error downloading {engine}")
|
||||
return found_engine
|
||||
@@ -213,31 +230,21 @@ class EngineManager:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def update_all_engines(cls):
|
||||
def engine_update_task(engine_class):
|
||||
logger.debug(f"Checking for updates to {engine_class.name()}")
|
||||
latest_version = engine_class.downloader().find_most_recent_version()
|
||||
def is_engine_update_available(cls, engine_class, ignore_system_installs=False):
|
||||
logger.debug(f"Checking for updates to {engine_class.name()}")
|
||||
latest_version = engine_class.downloader().find_most_recent_version()
|
||||
|
||||
if not latest_version:
|
||||
logger.warning(f"Could not find most recent version of {engine.name()} to download")
|
||||
return
|
||||
if not latest_version:
|
||||
logger.warning(f"Could not find most recent version of {engine_class.name()} to download")
|
||||
return None
|
||||
|
||||
version_num = latest_version.get('version')
|
||||
if cls.is_version_downloaded(engine_class.name(), version_num):
|
||||
logger.debug(f"Latest version of {engine_class.name()} ({version_num}) already downloaded")
|
||||
return
|
||||
version_num = latest_version.get('version')
|
||||
if cls.is_version_downloaded(engine_class.name(), version_num, ignore_system=ignore_system_installs):
|
||||
logger.debug(f"Latest version of {engine_class.name()} ({version_num}) already downloaded")
|
||||
return None
|
||||
|
||||
# download the engine
|
||||
logger.info(f"Downloading latest version of {engine_class.name()} ({version_num})...")
|
||||
cls.download_engine(engine=engine_class.name(), version=version_num, background=True)
|
||||
return latest_version
|
||||
|
||||
logger.info(f"Checking for updates for render engines...")
|
||||
threads = []
|
||||
for engine in cls.supported_engines():
|
||||
if engine.downloader():
|
||||
thread = threading.Thread(target=engine_update_task, args=(engine,))
|
||||
threads.append(thread)
|
||||
thread.start()
|
||||
|
||||
@classmethod
|
||||
def create_worker(cls, engine_name, input_path, output_path, engine_version=None, args=None, parent=None, name=None):
|
||||
@@ -303,18 +310,23 @@ class EngineDownloadWorker(threading.Thread):
|
||||
self.version = version
|
||||
self.system_os = system_os
|
||||
self.cpu = cpu
|
||||
self.percent_complete = 0
|
||||
|
||||
def _update_progress(self, current_progress):
|
||||
self.percent_complete = current_progress
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
existing_download = EngineManager.is_version_downloaded(self.engine, self.version, self.system_os, self.cpu)
|
||||
existing_download = EngineManager.is_version_downloaded(self.engine, self.version, self.system_os, self.cpu,
|
||||
ignore_system=True)
|
||||
if existing_download:
|
||||
logger.info(f"Requested download of {self.engine} {self.version}, but local copy already exists")
|
||||
return existing_download
|
||||
|
||||
# Get the appropriate downloader class based on the engine type
|
||||
EngineManager.engine_with_name(self.engine).downloader().download_engine(
|
||||
self.version, download_location=EngineManager.engines_path, system_os=self.system_os, cpu=self.cpu,
|
||||
timeout=300)
|
||||
downloader = EngineManager.engine_with_name(self.engine).downloader()
|
||||
downloader.download_engine( self.version, download_location=EngineManager.engines_path,
|
||||
system_os=self.system_os, cpu=self.cpu, timeout=300, progress_callback=self._update_progress)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in download worker: {e}")
|
||||
finally:
|
||||
|
||||
@@ -97,7 +97,7 @@ class FFMPEGDownloader(EngineDownloader):
|
||||
'windows': cls.__get_windows_versions}
|
||||
if not versions_per_os.get(system_os):
|
||||
logger.error(f"Cannot find version list for {system_os}")
|
||||
return
|
||||
return None
|
||||
|
||||
results = []
|
||||
all_versions = versions_per_os[system_os]()
|
||||
@@ -144,7 +144,7 @@ class FFMPEGDownloader(EngineDownloader):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def download_engine(cls, version, download_location, system_os=None, cpu=None, timeout=120):
|
||||
def download_engine(cls, version, download_location, system_os=None, cpu=None, timeout=120, progress_callback=None):
|
||||
system_os = system_os or current_system_os()
|
||||
cpu = cpu or current_system_cpu()
|
||||
|
||||
@@ -152,7 +152,7 @@ class FFMPEGDownloader(EngineDownloader):
|
||||
found_version = [item for item in cls.all_versions(system_os, cpu) if item['version'] == version]
|
||||
if not found_version:
|
||||
logger.error(f"Cannot find FFMPEG version {version} for {system_os} and {cpu}")
|
||||
return
|
||||
return None
|
||||
|
||||
# Platform specific naming cleanup
|
||||
remote_url = cls.__get_remote_url_for_version(version=version, system_os=system_os, cpu=cpu)
|
||||
@@ -162,7 +162,8 @@ class FFMPEGDownloader(EngineDownloader):
|
||||
# Download and extract
|
||||
try:
|
||||
logger.info(f"Requesting download of ffmpeg-{version}-{system_os}-{cpu}")
|
||||
cls.download_and_extract_app(remote_url=remote_url, download_location=download_location, timeout=timeout)
|
||||
cls.download_and_extract_app(remote_url=remote_url, download_location=download_location, timeout=timeout,
|
||||
progress_callback=progress_callback)
|
||||
|
||||
# naming cleanup to match existing naming convention
|
||||
output_path = os.path.join(download_location, f'ffmpeg-{version}-{system_os}-{cpu}')
|
||||
|
||||
Reference in New Issue
Block a user