3 Commits

Author SHA1 Message Date
f6ee57fb55 FFMPEG path fixes for ffprobe 2026-01-06 23:49:22 -06:00
2ba99cee31 EngineManager renaming and refactoring 2026-01-06 23:48:49 -06:00
Brett Williams
13a82a540a More window improvements 2026-01-06 21:50:41 -06:00
4 changed files with 95 additions and 69 deletions

View File

@@ -389,7 +389,7 @@ def engine_info():
def process_engine(engine):
try:
# Get all installed versions of the engine
installed_versions = EngineManager.all_versions_for_engine(engine.name())
installed_versions = EngineManager.all_version_data_for_engine(engine.name())
if not installed_versions:
return None
@@ -420,7 +420,7 @@ def engine_info():
except Exception as e:
logger.error(f"Error fetching details for engine '{engine.name()}': {e}")
raise e
return {}
engine_data = {}
with concurrent.futures.ThreadPoolExecutor() as executor:
@@ -438,7 +438,7 @@ def engine_info():
def is_engine_available(engine_name):
return {'engine': engine_name, 'available': RenderQueue.is_available_for_job(engine_name),
'cpu_count': int(psutil.cpu_count(logical=False)),
'versions': EngineManager.all_versions_for_engine(engine_name),
'versions': EngineManager.all_version_data_for_engine(engine_name),
'hostname': server.config['HOSTNAME']}

View File

@@ -11,6 +11,9 @@ from src.utilities.misc_helper import system_safe_path, current_system_os, curre
logger = logging.getLogger()
ENGINE_CLASSES = [Blender, FFMPEG]
class EngineManager:
"""Class that manages different versions of installed render engines and handles fetching and downloading new versions,
if possible.
@@ -21,32 +24,36 @@ class EngineManager:
@staticmethod
def supported_engines():
return [Blender, FFMPEG]
return ENGINE_CLASSES
# --- Installed Engines ---
@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()]
def engine_for_project_path(cls, path):
_, extension = os.path.splitext(path)
extension = extension.lower().strip('.')
for engine_class in cls.supported_engines():
engine = cls.get_latest_engine_instance(engine_class)
if extension in engine.supported_extensions():
return engine
undefined_renderer_support = [x for x in cls.supported_engines() if not cls.get_latest_engine_instance(x).supported_extensions()]
return undefined_renderer_support[0]
@classmethod
def engine_with_name(cls, engine_name):
for obj in cls.supported_engines():
if obj.name().lower() == engine_name.lower():
return obj
return None
@classmethod
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)
def get_latest_engine_instance(cls, engine_class):
newest = cls.newest_installed_engine_data(engine_class.name())
engine = engine_class(newest["path"])
return engine
@classmethod
def get_engines(cls, filter_name=None, include_corrupt=False, ignore_system=False):
def get_installed_engine_data(cls, filter_name=None, include_corrupt=False, ignore_system=False):
if not cls.engines_path:
raise FileNotFoundError("Engine path is not set")
@@ -123,31 +130,41 @@ class EngineManager:
return results
# --- Check for Updates ---
@classmethod
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)
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 all_version_data_for_engine(cls, engine_name, include_corrupt=False, ignore_system=False):
versions = cls.get_installed_engine_data(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, ignore_system=None):
def newest_installed_engine_data(cls, engine_name, 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, ignore_system=ignore_system)
filtered = [x for x in cls.all_version_data_for_engine(engine_name, 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}")
logger.error(f"Cannot find newest engine version for {engine_name}-{system_os}-{cpu}")
return None
@classmethod
def is_version_downloaded(cls, engine, version, system_os=None, cpu=None, ignore_system=False):
def is_version_installed(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, ignore_system=ignore_system) if
filtered = [x for x in cls.get_installed_engine_data(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
@@ -169,6 +186,28 @@ class EngineManager:
logger.debug(f"Exception in find_most_recent_version: {e}")
return None
@classmethod
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_class.name()} to download")
return None
version_num = latest_version.get('version')
if cls.is_version_installed(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
return latest_version
# --- Downloads ---
@classmethod
def downloadable_engines(cls):
return [engine for engine in cls.supported_engines() if hasattr(engine, "downloader") and engine.downloader()]
@classmethod
def get_existing_download_task(cls, engine, version, system_os=None, cpu=None):
for task in cls.download_tasks:
@@ -204,7 +243,7 @@ class EngineManager:
return thread
thread.join()
found_engine = cls.is_version_downloaded(engine, version, system_os, cpu, ignore_system) # Check that engine downloaded
found_engine = cls.is_version_installed(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,7 +252,7 @@ class EngineManager:
def delete_engine_download(cls, engine, version, system_os=None, cpu=None):
logger.info(f"Requested deletion of engine: {engine}-{version}")
found = cls.is_version_downloaded(engine, version, system_os, cpu)
found = cls.is_version_installed(engine, version, system_os, cpu)
if found and found['type'] == 'managed': # don't delete system installs
# find the root directory of the engine executable
root_dir_name = '-'.join([engine, version, found['system_os'], found['cpu']])
@@ -229,22 +268,11 @@ class EngineManager:
logger.error(f"Cannot find engine: {engine}-{version}")
return False
# --- Background Tasks ---
@classmethod
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_class.name()} to download")
return None
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
return latest_version
def active_downloads(cls) -> list:
return [x for x in cls.download_tasks if x.is_alive()]
@classmethod
def create_worker(cls, engine_name, input_path, output_path, engine_version=None, args=None, parent=None, name=None):
@@ -252,7 +280,7 @@ class EngineManager:
worker_class = cls.engine_with_name(engine_name).worker_class()
# check to make sure we have versions installed
all_versions = cls.all_versions_for_engine(engine_name)
all_versions = cls.all_version_data_for_engine(engine_name)
if not all_versions:
raise FileNotFoundError(f"Cannot find any installed '{engine_name}' engines")
@@ -281,16 +309,6 @@ class EngineManager:
return worker_class(input_path=input_path, output_path=output_path, engine_path=engine_path, args=args,
parent=parent, name=name)
@classmethod
def engine_for_project_path(cls, path):
_, extension = os.path.splitext(path)
extension = extension.lower().strip('.')
for engine in cls.supported_engines():
if extension in engine().supported_extensions():
return engine
undefined_renderer_support = [x for x in cls.supported_engines() if not x().supported_extensions()]
return undefined_renderer_support[0]
class EngineDownloadWorker(threading.Thread):
"""A thread worker for downloading a specific version of a rendering engine.
@@ -317,7 +335,7 @@ class EngineDownloadWorker(threading.Thread):
def run(self):
try:
existing_download = EngineManager.is_version_downloaded(self.engine, self.version, self.system_os, self.cpu,
existing_download = EngineManager.is_version_installed(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")
@@ -341,4 +359,4 @@ if __name__ == '__main__':
# EngineManager.delete_engine_download('blender', '3.2.1', 'macos', 'a')
EngineManager.engines_path = "/Users/brettwilliams/zordon-uploads/engines"
# print(EngineManager.is_version_downloaded("ffmpeg", "6.0"))
print(EngineManager.get_engines())
print(EngineManager.get_installed_engine_data())

View File

@@ -1,4 +1,5 @@
import json
import os
import re
from src.engines.core.base_engine import *
@@ -46,10 +47,19 @@ class FFMPEG(BaseRenderEngine):
return version
def get_project_info(self, project_path, timeout=10):
"""Run ffprobe and parse the output as JSON"""
try:
# Run ffprobe and parse the output as JSON
# resolve ffprobe path
engine_dir = os.path.dirname(self.engine_path())
ffprobe_path = os.path.join(engine_dir, 'ffprobe')
if self.engine_path().endswith('.exe'):
ffprobe_path += '.exe'
if not os.path.exists(ffprobe_path): # fallback to system install (if available)
ffprobe_path = 'ffprobe'
# run ffprobe
cmd = [
'ffprobe', '-v', 'quiet', '-print_format', 'json',
ffprobe_path, '-v', 'quiet', '-print_format', 'json',
'-show_streams', '-select_streams', 'v', project_path
]
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True,
@@ -79,7 +89,7 @@ class FFMPEG(BaseRenderEngine):
}
except Exception as e:
print(f"An error occurred: {e}")
print(f"Failed to get FFMPEG project info: {e}")
return None
def get_encoders(self):

View File

@@ -158,20 +158,18 @@ class NewRenderJobForm(QWidget):
self.output_settings_group = QWidget()
output_settings_layout = QVBoxLayout(self.output_settings_group)
# # Render Name
# render_name_layout = QHBoxLayout()
# render_name_layout.addWidget(QLabel("Render name:"))
# self.job_name_input = QLineEdit()
# render_name_layout.addWidget(self.job_name_input)
# output_settings_layout.addLayout(render_name_layout)
# File Format
format_group = QGroupBox("Format / Range")
output_settings_layout.addWidget(format_group)
format_group_layout = QVBoxLayout()
format_group.setLayout(format_group_layout)
file_format_layout = QHBoxLayout()
file_format_layout.addWidget(QLabel("Format:"))
self.file_format_combo = QComboBox()
# You can populate this later based on engine
self.file_format_combo.setFixedWidth(200)
file_format_layout.addWidget(self.file_format_combo)
output_settings_layout.addLayout(file_format_layout)
file_format_layout.addStretch()
format_group_layout.addLayout(file_format_layout)
# Frame Range
frame_range_layout = QHBoxLayout()
@@ -181,12 +179,12 @@ class NewRenderJobForm(QWidget):
self.start_frame_input.setFixedWidth(80)
self.end_frame_input = QSpinBox()
self.end_frame_input.setRange(1, 99999)
self.start_frame_input.setFixedWidth(80)
self.end_frame_input.setFixedWidth(80)
frame_range_layout.addWidget(self.start_frame_input)
frame_range_layout.addWidget(QLabel("to"))
frame_range_layout.addWidget(self.end_frame_input)
frame_range_layout.addStretch()
output_settings_layout.addLayout(frame_range_layout)
format_group_layout.addLayout(frame_range_layout)
# --- Resolution & FPS Group ---
resolution_group = QGroupBox("Resolution / Frame Rate")