mirror of
https://github.com/blw1138/Zordon.git
synced 2025-12-17 16:58:12 +00:00
Compare commits
9 Commits
master
...
feature/10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4da03e30a2 | ||
|
|
4a566ec7c3 | ||
|
|
085d39fde8 | ||
|
|
d5f1224c33 | ||
|
|
e97e3d74c8 | ||
|
|
1af4169447 | ||
|
|
ea728f7809 | ||
|
|
a4e6fca73d | ||
|
|
9aafb5c0fb |
11
README.md
11
README.md
@@ -1,15 +1,6 @@
|
|||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Zordon
|
# Zordon
|
||||||
|
|
||||||
A lightweight, zero-install, distributed rendering and management tool designed to streamline and optimize rendering workflows across multiple machines
|
A tool designed for small render farms, such as those used in home studios or small businesses, to efficiently manage and run render jobs for Blender, FFMPEG, and other video renderers. It simplifies the process of distributing rendering tasks across multiple available machines, optimizing the rendering workflow for artists, animators, and video professionals.
|
||||||
|
|
||||||
## What is Zordon?
|
|
||||||
|
|
||||||
Zordon is tool designed for small render farms, such as those used in home studios or small businesses, to efficiently manage and run render jobs for Blender, FFMPEG, and other video renderers. It simplifies the process of distributing rendering tasks across multiple available machines, optimizing the rendering workflow for artists, animators, and video professionals.
|
|
||||||
|
|
||||||
|
|
||||||
Notice: This should be considered a beta and is meant for casual / hobbiest use. Do not use in mission critical environments!
|
Notice: This should be considered a beta and is meant for casual / hobbiest use. Do not use in mission critical environments!
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 838 KiB |
@@ -5,10 +5,8 @@ from PyInstaller.utils.hooks import collect_all
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import platform
|
import platform
|
||||||
src_path = os.path.abspath("src")
|
|
||||||
sys.path.insert(0, src_path)
|
|
||||||
from version import APP_NAME, APP_VERSION, APP_AUTHOR
|
|
||||||
sys.path.insert(0, os.path.abspath('.'))
|
sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
from version import APP_NAME, APP_VERSION, APP_AUTHOR
|
||||||
|
|
||||||
datas = [('resources', 'resources'), ('src/engines/blender/scripts/', 'src/engines/blender/scripts')]
|
datas = [('resources', 'resources'), ('src/engines/blender/scripts/', 'src/engines/blender/scripts')]
|
||||||
binaries = []
|
binaries = []
|
||||||
@@ -57,7 +55,7 @@ if platform.system() == 'Darwin': # macOS
|
|||||||
a.datas,
|
a.datas,
|
||||||
strip=True,
|
strip=True,
|
||||||
name=f'{APP_NAME}.app',
|
name=f'{APP_NAME}.app',
|
||||||
icon='resources/Server.png',
|
icon=None,
|
||||||
bundle_identifier=None,
|
bundle_identifier=None,
|
||||||
version=APP_VERSION
|
version=APP_VERSION
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -36,6 +36,3 @@ lxml>=5.1.0
|
|||||||
click>=8.1.7
|
click>=8.1.7
|
||||||
requests_toolbelt>=1.0.0
|
requests_toolbelt>=1.0.0
|
||||||
pyinstaller_versionfile>=2.1.1
|
pyinstaller_versionfile>=2.1.1
|
||||||
py-cpuinfo~=9.0.0
|
|
||||||
requests-toolbelt~=1.0.0
|
|
||||||
ifaddr~=0.2.0
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from src.init import run
|
from init import run
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
run(server_only=True)
|
run(server_only=True)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import tempfile
|
|||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import cpuinfo
|
|
||||||
import psutil
|
import psutil
|
||||||
import yaml
|
import yaml
|
||||||
from flask import Flask, request, send_file, after_this_request, Response, redirect, url_for
|
from flask import Flask, request, send_file, after_this_request, Response, redirect, url_for
|
||||||
@@ -26,13 +25,11 @@ from src.utilities.config import Config
|
|||||||
from src.utilities.misc_helper import system_safe_path, current_system_os, current_system_cpu, \
|
from src.utilities.misc_helper import system_safe_path, current_system_os, current_system_cpu, \
|
||||||
current_system_os_version, num_to_alphanumeric
|
current_system_os_version, num_to_alphanumeric
|
||||||
from src.utilities.status_utils import string_to_status
|
from src.utilities.status_utils import string_to_status
|
||||||
from src.version import APP_VERSION
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
server = Flask(__name__)
|
server = Flask(__name__)
|
||||||
ssl._create_default_https_context = ssl._create_unverified_context # disable SSL for downloads
|
ssl._create_default_https_context = ssl._create_unverified_context # disable SSL for downloads
|
||||||
|
|
||||||
API_VERSION = "1"
|
|
||||||
|
|
||||||
def start_server(hostname=None):
|
def start_server(hostname=None):
|
||||||
|
|
||||||
@@ -231,7 +228,6 @@ def status():
|
|||||||
"system_os": current_system_os(),
|
"system_os": current_system_os(),
|
||||||
"system_os_version": current_system_os_version(),
|
"system_os_version": current_system_os_version(),
|
||||||
"system_cpu": current_system_cpu(),
|
"system_cpu": current_system_cpu(),
|
||||||
"system_cpu_brand": cpuinfo.get_cpu_info()['brand_raw'],
|
|
||||||
"cpu_percent": psutil.cpu_percent(percpu=False),
|
"cpu_percent": psutil.cpu_percent(percpu=False),
|
||||||
"cpu_percent_per_cpu": psutil.cpu_percent(percpu=True),
|
"cpu_percent_per_cpu": psutil.cpu_percent(percpu=True),
|
||||||
"cpu_count": psutil.cpu_count(logical=False),
|
"cpu_count": psutil.cpu_count(logical=False),
|
||||||
@@ -240,9 +236,7 @@ def status():
|
|||||||
"memory_percent": psutil.virtual_memory().percent,
|
"memory_percent": psutil.virtual_memory().percent,
|
||||||
"job_counts": RenderQueue.job_counts(),
|
"job_counts": RenderQueue.job_counts(),
|
||||||
"hostname": server.config['HOSTNAME'],
|
"hostname": server.config['HOSTNAME'],
|
||||||
"port": server.config['PORT'],
|
"port": server.config['PORT']
|
||||||
"app_version": APP_VERSION,
|
|
||||||
"api_version": API_VERSION
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class PreviewManager:
|
|||||||
_running_jobs = {}
|
_running_jobs = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __generate_job_preview_worker(cls, job, replace_existing=False, max_width=480):
|
def __generate_job_preview_worker(cls, job, replace_existing=False, max_width=320):
|
||||||
|
|
||||||
# Determine best source file to use for thumbs
|
# Determine best source file to use for thumbs
|
||||||
job_file_list = job.file_list()
|
job_file_list = job.file_list()
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ class RenderServerProxy:
|
|||||||
self.system_cpu_count = None
|
self.system_cpu_count = None
|
||||||
self.system_os = None
|
self.system_os = None
|
||||||
self.system_os_version = None
|
self.system_os_version = None
|
||||||
self.system_api_version = None
|
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
# Basics / Connection:
|
# Basics / Connection:
|
||||||
@@ -101,10 +100,8 @@ class RenderServerProxy:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def request(self, payload, timeout=5):
|
def request(self, payload, timeout=5):
|
||||||
from src.api.api_server import API_VERSION
|
|
||||||
hostname = LOOPBACK if self.is_localhost else self.hostname
|
hostname = LOOPBACK if self.is_localhost else self.hostname
|
||||||
return requests.get(f'http://{hostname}:{self.port}/api/{payload}', timeout=timeout,
|
return requests.get(f'http://{hostname}:{self.port}/api/{payload}', timeout=timeout)
|
||||||
headers={"X-API-Version": str(API_VERSION)})
|
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
# Background Updates:
|
# Background Updates:
|
||||||
@@ -165,7 +162,6 @@ class RenderServerProxy:
|
|||||||
self.system_cpu_count = status['cpu_count']
|
self.system_cpu_count = status['cpu_count']
|
||||||
self.system_os = status['system_os']
|
self.system_os = status['system_os']
|
||||||
self.system_os_version = status['system_os_version']
|
self.system_os_version = status['system_os_version']
|
||||||
self.system_api_version = status['api_version']
|
|
||||||
return status
|
return status
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|||||||
@@ -374,15 +374,13 @@ class DistributedJobManager:
|
|||||||
:param system_os: str, Restrict results to servers running a specific OS
|
:param system_os: str, Restrict results to servers running a specific OS
|
||||||
:return: A list of dictionaries with each dict containing hostname and cpu_count of available servers
|
:return: A list of dictionaries with each dict containing hostname and cpu_count of available servers
|
||||||
"""
|
"""
|
||||||
from api.api_server import API_VERSION
|
|
||||||
available_servers = []
|
available_servers = []
|
||||||
for hostname in ZeroconfServer.found_hostnames():
|
for hostname in ZeroconfServer.found_hostnames():
|
||||||
host_properties = ZeroconfServer.get_hostname_properties(hostname)
|
host_properties = ZeroconfServer.get_hostname_properties(hostname)
|
||||||
if host_properties.get('api_version') == API_VERSION:
|
if not system_os or (system_os and system_os == host_properties.get('system_os')):
|
||||||
if not system_os or (system_os and system_os == host_properties.get('system_os')):
|
response = RenderServerProxy(hostname).is_engine_available(engine_name)
|
||||||
response = RenderServerProxy(hostname).is_engine_available(engine_name)
|
if response and response.get('available', False):
|
||||||
if response and response.get('available', False):
|
available_servers.append(response)
|
||||||
available_servers.append(response)
|
|
||||||
|
|
||||||
return available_servers
|
return available_servers
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ from src.engines.blender.blender_engine import Blender
|
|||||||
from src.engines.core.base_downloader import EngineDownloader
|
from src.engines.core.base_downloader import EngineDownloader
|
||||||
from src.utilities.misc_helper import current_system_os, current_system_cpu
|
from src.utilities.misc_helper import current_system_os, current_system_cpu
|
||||||
|
|
||||||
# url = "https://download.blender.org/release/"
|
url = "https://download.blender.org/release/"
|
||||||
url = "https://ftp.nluug.nl/pub/graphics/blender/release/" # much faster mirror for testing
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
supported_formats = ['.zip', '.tar.xz', '.dmg']
|
supported_formats = ['.zip', '.tar.xz', '.dmg']
|
||||||
|
|||||||
@@ -31,12 +31,6 @@ class BlenderRenderWorker(BaseRenderWorker):
|
|||||||
cmd.append('-b')
|
cmd.append('-b')
|
||||||
cmd.append(self.input_path)
|
cmd.append(self.input_path)
|
||||||
|
|
||||||
# Set Render Engine
|
|
||||||
blender_engine = self.args.get('engine')
|
|
||||||
if blender_engine:
|
|
||||||
blender_engine = blender_engine.upper()
|
|
||||||
cmd.extend(['-E', blender_engine])
|
|
||||||
|
|
||||||
# Start Python expressions - # todo: investigate splitting into separate 'setup' script
|
# Start Python expressions - # todo: investigate splitting into separate 'setup' script
|
||||||
cmd.append('--python-expr')
|
cmd.append('--python-expr')
|
||||||
python_exp = 'import bpy; bpy.context.scene.render.use_overwrite = False;'
|
python_exp = 'import bpy; bpy.context.scene.render.use_overwrite = False;'
|
||||||
@@ -46,7 +40,8 @@ class BlenderRenderWorker(BaseRenderWorker):
|
|||||||
if custom_camera:
|
if custom_camera:
|
||||||
python_exp = python_exp + f"bpy.context.scene.camera = bpy.data.objects['{custom_camera}'];"
|
python_exp = python_exp + f"bpy.context.scene.camera = bpy.data.objects['{custom_camera}'];"
|
||||||
|
|
||||||
# Set Render Device for Cycles (gpu/cpu/any)
|
# Set Render Device (gpu/cpu/any)
|
||||||
|
blender_engine = self.args.get('engine', 'BLENDER_EEVEE').upper()
|
||||||
if blender_engine == 'CYCLES':
|
if blender_engine == 'CYCLES':
|
||||||
render_device = self.args.get('render_device', 'any').lower()
|
render_device = self.args.get('render_device', 'any').lower()
|
||||||
if render_device not in {'any', 'gpu', 'cpu'}:
|
if render_device not in {'any', 'gpu', 'cpu'}:
|
||||||
@@ -71,7 +66,7 @@ class BlenderRenderWorker(BaseRenderWorker):
|
|||||||
# Remove the extension only if it is not composed entirely of digits
|
# Remove the extension only if it is not composed entirely of digits
|
||||||
path_without_ext = main_part if not ext[1:].isdigit() else self.output_path
|
path_without_ext = main_part if not ext[1:].isdigit() else self.output_path
|
||||||
path_without_ext += "_"
|
path_without_ext += "_"
|
||||||
cmd.extend(['-o', path_without_ext, '-F', export_format])
|
cmd.extend(['-E', blender_engine, '-o', path_without_ext, '-F', export_format])
|
||||||
|
|
||||||
# set frame range
|
# set frame range
|
||||||
cmd.extend(['-s', self.start_frame, '-e', self.end_frame, '-a'])
|
cmd.extend(['-s', self.start_frame, '-e', self.end_frame, '-a'])
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ class EngineManager:
|
|||||||
def supported_engines():
|
def supported_engines():
|
||||||
return [Blender, FFMPEG]
|
return [Blender, FFMPEG]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def downloadable_engines(cls):
|
||||||
|
return [engine for engine in cls.supported_engines() if hasattr(engine, "downloader") and engine.downloader()]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def engine_with_name(cls, engine_name):
|
def engine_with_name(cls, engine_name):
|
||||||
for obj in cls.supported_engines():
|
for obj in cls.supported_engines():
|
||||||
@@ -30,7 +34,7 @@ class EngineManager:
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_engines(cls, filter_name=None, include_corrupt=False):
|
def get_engines(cls, filter_name=None, include_corrupt=False, ignore_system=False):
|
||||||
|
|
||||||
if not cls.engines_path:
|
if not cls.engines_path:
|
||||||
raise FileNotFoundError("Engine path is not set")
|
raise FileNotFoundError("Engine path is not set")
|
||||||
@@ -92,46 +96,47 @@ class EngineManager:
|
|||||||
'type': 'system'
|
'type': 'system'
|
||||||
}
|
}
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
if not ignore_system:
|
||||||
futures = {
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
executor.submit(fetch_engine_details, eng, include_corrupt): eng.name()
|
futures = {
|
||||||
for eng in cls.supported_engines()
|
executor.submit(fetch_engine_details, eng, include_corrupt): eng.name()
|
||||||
if eng.default_renderer_path() and (not filter_name or filter_name == eng.name())
|
for eng in cls.supported_engines()
|
||||||
}
|
if eng.default_renderer_path() and (not filter_name or filter_name == eng.name())
|
||||||
|
}
|
||||||
|
|
||||||
for future in concurrent.futures.as_completed(futures):
|
for future in concurrent.futures.as_completed(futures):
|
||||||
result = future.result()
|
result = future.result()
|
||||||
if result:
|
if result:
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all_versions_for_engine(cls, engine_name, include_corrupt=False):
|
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)
|
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)
|
sorted_versions = sorted(versions, key=lambda x: x['version'], reverse=True)
|
||||||
return sorted_versions
|
return sorted_versions
|
||||||
|
|
||||||
@classmethod
|
@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()
|
system_os = system_os or current_system_os()
|
||||||
cpu = cpu or current_system_cpu()
|
cpu = cpu or current_system_cpu()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
filtered = [x for x in cls.all_versions_for_engine(engine) if x['system_os'] == system_os and
|
filtered = [x for x in cls.all_versions_for_engine(engine, ignore_system=ignore_system)
|
||||||
x['cpu'] == cpu]
|
if x['system_os'] == system_os and x['cpu'] == cpu]
|
||||||
return filtered[0]
|
return filtered[0]
|
||||||
except IndexError:
|
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}-{system_os}-{cpu}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@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()
|
system_os = system_os or current_system_os()
|
||||||
cpu = cpu or current_system_cpu()
|
cpu = cpu or current_system_cpu()
|
||||||
|
|
||||||
filtered = [x for x in cls.get_engines(filter_name=engine) if x['system_os'] == system_os and
|
filtered = [x for x in cls.get_engines(filter_name=engine, ignore_system=ignore_system) if
|
||||||
x['cpu'] == cpu and x['version'] == version]
|
x['system_os'] == system_os and x['cpu'] == cpu and x['version'] == version]
|
||||||
return filtered[0] if filtered else False
|
return filtered[0] if filtered else False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -164,7 +169,7 @@ class EngineManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@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)
|
engine_to_download = cls.engine_with_name(engine)
|
||||||
existing_task = cls.get_existing_download_task(engine, version, system_os, cpu)
|
existing_task = cls.get_existing_download_task(engine, version, system_os, cpu)
|
||||||
@@ -187,7 +192,7 @@ class EngineManager:
|
|||||||
return thread
|
return thread
|
||||||
|
|
||||||
thread.join()
|
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:
|
if not found_engine:
|
||||||
logger.error(f"Error downloading {engine}")
|
logger.error(f"Error downloading {engine}")
|
||||||
return found_engine
|
return found_engine
|
||||||
@@ -213,31 +218,21 @@ class EngineManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_all_engines(cls):
|
def is_engine_update_available(cls, engine_class, ignore_system_installs=False):
|
||||||
def engine_update_task(engine_class):
|
logger.debug(f"Checking for updates to {engine_class.name()}")
|
||||||
logger.debug(f"Checking for updates to {engine_class.name()}")
|
latest_version = engine_class.downloader().find_most_recent_version()
|
||||||
latest_version = engine_class.downloader().find_most_recent_version()
|
|
||||||
|
|
||||||
if not latest_version:
|
if not latest_version:
|
||||||
logger.warning(f"Could not find most recent version of {engine.name()} to download")
|
logger.warning(f"Could not find most recent version of {engine_class.name()} to download")
|
||||||
return
|
return
|
||||||
|
|
||||||
version_num = latest_version.get('version')
|
version_num = latest_version.get('version')
|
||||||
if cls.is_version_downloaded(engine_class.name(), version_num):
|
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")
|
logger.debug(f"Latest version of {engine_class.name()} ({version_num}) already downloaded")
|
||||||
return
|
return
|
||||||
|
|
||||||
# download the engine
|
return latest_version
|
||||||
logger.info(f"Downloading latest version of {engine_class.name()} ({version_num})...")
|
|
||||||
cls.download_engine(engine=engine_class.name(), version=version_num, background=True)
|
|
||||||
|
|
||||||
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
|
@classmethod
|
||||||
def create_worker(cls, renderer, input_path, output_path, engine_version=None, args=None, parent=None, name=None):
|
def create_worker(cls, renderer, input_path, output_path, engine_version=None, args=None, parent=None, name=None):
|
||||||
@@ -305,21 +300,19 @@ class EngineDownloadWorker(threading.Thread):
|
|||||||
self.cpu = cpu
|
self.cpu = cpu
|
||||||
|
|
||||||
def run(self):
|
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:
|
if existing_download:
|
||||||
logger.info(f"Requested download of {self.engine} {self.version}, but local copy already exists")
|
logger.info(f"Requested download of {self.engine} {self.version}, but local copy already exists")
|
||||||
return existing_download
|
return existing_download
|
||||||
|
|
||||||
# Get the appropriate downloader class based on the engine type
|
# Get the appropriate downloader class based on the engine type
|
||||||
EngineManager.engine_with_name(self.engine).downloader().download_engine(
|
EngineManager.engine_with_name(self.engine).downloader().download_engine(
|
||||||
self.version, download_location=EngineManager.engines_path, system_os=self.system_os, cpu=self.cpu,
|
self.version, download_location=EngineManager.engines_path, system_os=self.system_os, cpu=self.cpu,
|
||||||
timeout=300)
|
timeout=300)
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in download worker: {e}")
|
# remove itself from the downloader list
|
||||||
finally:
|
EngineManager.download_tasks.remove(self)
|
||||||
# remove itself from the downloader list
|
|
||||||
EngineManager.download_tasks.remove(self)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
27
src/init.py
27
src/init.py
@@ -5,10 +5,10 @@ import socket
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import cpuinfo
|
from PyQt6.QtCore import QSettings
|
||||||
|
|
||||||
from api.api_server import API_VERSION
|
|
||||||
from src.api.api_server import start_server
|
from src.api.api_server import start_server
|
||||||
from src.api.preview_manager import PreviewManager
|
from src.api.preview_manager import PreviewManager
|
||||||
from src.api.serverproxy_manager import ServerProxyManager
|
from src.api.serverproxy_manager import ServerProxyManager
|
||||||
@@ -19,7 +19,7 @@ from src.utilities.config import Config
|
|||||||
from src.utilities.misc_helper import (system_safe_path, current_system_cpu, current_system_os,
|
from src.utilities.misc_helper import (system_safe_path, current_system_cpu, current_system_os,
|
||||||
current_system_os_version, check_for_updates)
|
current_system_os_version, check_for_updates)
|
||||||
from src.utilities.zeroconf_server import ZeroconfServer
|
from src.utilities.zeroconf_server import ZeroconfServer
|
||||||
from src.version import APP_NAME, APP_VERSION, APP_REPO_NAME, APP_REPO_OWNER
|
from version import APP_NAME, APP_VERSION, APP_REPO_NAME, APP_REPO_OWNER, APP_AUTHOR
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
@@ -69,6 +69,8 @@ def run(server_only=False) -> int:
|
|||||||
APP_VERSION))
|
APP_VERSION))
|
||||||
update_thread.start()
|
update_thread.start()
|
||||||
|
|
||||||
|
settings = QSettings(APP_AUTHOR, APP_NAME)
|
||||||
|
|
||||||
# main start
|
# main start
|
||||||
logger.info(f"Starting {APP_NAME} Render Server")
|
logger.info(f"Starting {APP_NAME} Render Server")
|
||||||
return_code = 0
|
return_code = 0
|
||||||
@@ -95,9 +97,16 @@ def run(server_only=False) -> int:
|
|||||||
ServerProxyManager.subscribe_to_listener()
|
ServerProxyManager.subscribe_to_listener()
|
||||||
DistributedJobManager.subscribe_to_listener()
|
DistributedJobManager.subscribe_to_listener()
|
||||||
|
|
||||||
# check for updates for render engines if configured or on first launch
|
# check for updates for render engines if configured
|
||||||
if Config.update_engines_on_launch or not EngineManager.get_engines():
|
ignore_system = settings.value("engines_ignore_system_installs", False)
|
||||||
EngineManager.update_all_engines()
|
if settings.value('check_for_engine_updates_on_launch', False):
|
||||||
|
for engine in EngineManager.downloadable_engines():
|
||||||
|
if settings.value(f'engine_download-{engine.name()}', False):
|
||||||
|
update_result = EngineManager.is_engine_update_available(engine, ignore_system_installs=ignore_system)
|
||||||
|
EngineManager.download_engine(engine=engine.name(), version=update_result['version'],
|
||||||
|
background=True,
|
||||||
|
ignore_system=ignore_system)
|
||||||
|
settings.setValue("engines_last_update_time", datetime.now().isoformat())
|
||||||
|
|
||||||
# get hostname
|
# get hostname
|
||||||
local_hostname = socket.gethostname()
|
local_hostname = socket.gethostname()
|
||||||
@@ -111,11 +120,9 @@ def run(server_only=False) -> int:
|
|||||||
# start zeroconf server
|
# start zeroconf server
|
||||||
ZeroconfServer.configure(f"_{APP_NAME.lower()}._tcp.local.", local_hostname, Config.port_number)
|
ZeroconfServer.configure(f"_{APP_NAME.lower()}._tcp.local.", local_hostname, Config.port_number)
|
||||||
ZeroconfServer.properties = {'system_cpu': current_system_cpu(),
|
ZeroconfServer.properties = {'system_cpu': current_system_cpu(),
|
||||||
'system_cpu_brand': cpuinfo.get_cpu_info()['brand_raw'],
|
|
||||||
'system_cpu_cores': multiprocessing.cpu_count(),
|
'system_cpu_cores': multiprocessing.cpu_count(),
|
||||||
'system_os': current_system_os(),
|
'system_os': current_system_os(),
|
||||||
'system_os_version': current_system_os_version(),
|
'system_os_version': current_system_os_version()}
|
||||||
'api_version': API_VERSION}
|
|
||||||
ZeroconfServer.start()
|
ZeroconfServer.start()
|
||||||
logger.info(f"{APP_NAME} Render Server started - Hostname: {local_hostname}")
|
logger.info(f"{APP_NAME} Render Server started - Hostname: {local_hostname}")
|
||||||
RenderQueue.start() # Start evaluating the render queue
|
RenderQueue.start() # Start evaluating the render queue
|
||||||
@@ -180,8 +187,6 @@ def __show_gui(buffer_handler):
|
|||||||
|
|
||||||
# load application
|
# load application
|
||||||
app: QApplication = QApplication(sys.argv)
|
app: QApplication = QApplication(sys.argv)
|
||||||
if app.style().objectName() != 'macos':
|
|
||||||
app.setStyle('Fusion')
|
|
||||||
|
|
||||||
# configure main window
|
# configure main window
|
||||||
from src.ui.main_window import MainWindow
|
from src.ui.main_window import MainWindow
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from PyQt6.QtCore import Qt
|
|||||||
from PyQt6.QtGui import QPixmap
|
from PyQt6.QtGui import QPixmap
|
||||||
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QHBoxLayout
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QHBoxLayout
|
||||||
|
|
||||||
from src.version import *
|
from version import *
|
||||||
|
|
||||||
|
|
||||||
class AboutDialog(QDialog):
|
class AboutDialog(QDialog):
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import datetime
|
|||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -15,7 +16,6 @@ from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QListWidget, QTab
|
|||||||
QTableWidgetItem, QLabel, QVBoxLayout, QHeaderView, QMessageBox, QGroupBox, QPushButton, QListWidgetItem, \
|
QTableWidgetItem, QLabel, QVBoxLayout, QHeaderView, QMessageBox, QGroupBox, QPushButton, QListWidgetItem, \
|
||||||
QFileDialog
|
QFileDialog
|
||||||
|
|
||||||
from api.api_server import API_VERSION
|
|
||||||
from src.render_queue import RenderQueue
|
from src.render_queue import RenderQueue
|
||||||
from src.utilities.misc_helper import get_time_elapsed, resources_dir, is_localhost
|
from src.utilities.misc_helper import get_time_elapsed, resources_dir, is_localhost
|
||||||
from src.utilities.status_utils import RenderStatus
|
from src.utilities.status_utils import RenderStatus
|
||||||
@@ -29,8 +29,7 @@ from src.ui.widgets.proportional_image_label import ProportionalImageLabel
|
|||||||
from src.ui.widgets.statusbar import StatusBar
|
from src.ui.widgets.statusbar import StatusBar
|
||||||
from src.ui.widgets.toolbar import ToolBar
|
from src.ui.widgets.toolbar import ToolBar
|
||||||
from src.api.serverproxy_manager import ServerProxyManager
|
from src.api.serverproxy_manager import ServerProxyManager
|
||||||
from src.utilities.misc_helper import launch_url, iso_datestring_to_formatted_datestring
|
from src.utilities.misc_helper import launch_url
|
||||||
from src.version import APP_NAME
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.buffer_handler = None
|
self.buffer_handler = None
|
||||||
|
|
||||||
# Window-Settings
|
# Window-Settings
|
||||||
self.setWindowTitle(APP_NAME)
|
self.setWindowTitle("Zordon")
|
||||||
self.setGeometry(100, 100, 900, 800)
|
self.setGeometry(100, 100, 900, 800)
|
||||||
central_widget = QWidget(self)
|
central_widget = QWidget(self)
|
||||||
self.setCentralWidget(central_widget)
|
self.setCentralWidget(central_widget)
|
||||||
@@ -243,7 +242,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# Use the get method with defaults to avoid KeyError
|
# Use the get method with defaults to avoid KeyError
|
||||||
os_info = f"OS: {server_info.get('system_os', 'Unknown')} {server_info.get('system_os_version', '')}"
|
os_info = f"OS: {server_info.get('system_os', 'Unknown')} {server_info.get('system_os_version', '')}"
|
||||||
cpu_info = f"CPU: {server_info.get('system_cpu_brand', 'Unknown')} ({server_info.get('system_cpu_cores', 'Unknown')} cores)"
|
cpu_info = f"CPU: {server_info.get('system_cpu', 'Unknown')} - {server_info.get('system_cpu_cores', 'Unknown')} cores"
|
||||||
|
|
||||||
self.server_info_os.setText(os_info.strip())
|
self.server_info_os.setText(os_info.strip())
|
||||||
self.server_info_cpu.setText(cpu_info)
|
self.server_info_cpu.setText(cpu_info)
|
||||||
@@ -257,7 +256,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.job_list_view.clear()
|
self.job_list_view.clear()
|
||||||
self.refresh_job_headers()
|
self.refresh_job_headers()
|
||||||
|
|
||||||
job_fetch = self.current_server_proxy.get_all_jobs(ignore_token=False)
|
job_fetch = self.current_server_proxy.get_all_jobs(ignore_token=clear_table)
|
||||||
if job_fetch:
|
if job_fetch:
|
||||||
num_jobs = len(job_fetch)
|
num_jobs = len(job_fetch)
|
||||||
self.job_list_view.setRowCount(num_jobs)
|
self.job_list_view.setRowCount(num_jobs)
|
||||||
@@ -277,11 +276,10 @@ class MainWindow(QMainWindow):
|
|||||||
renderer = f"{job.get('renderer', '')}-{job.get('renderer_version')}"
|
renderer = f"{job.get('renderer', '')}-{job.get('renderer_version')}"
|
||||||
priority = str(job.get('priority', ''))
|
priority = str(job.get('priority', ''))
|
||||||
total_frames = str(job.get('total_frames', ''))
|
total_frames = str(job.get('total_frames', ''))
|
||||||
date_created_string = iso_datestring_to_formatted_datestring(job['date_created'])
|
|
||||||
|
|
||||||
items = [QTableWidgetItem(job['id']), QTableWidgetItem(name), QTableWidgetItem(renderer),
|
items = [QTableWidgetItem(job['id']), QTableWidgetItem(name), QTableWidgetItem(renderer),
|
||||||
QTableWidgetItem(priority), QTableWidgetItem(display_status), QTableWidgetItem(time_elapsed),
|
QTableWidgetItem(priority), QTableWidgetItem(display_status), QTableWidgetItem(time_elapsed),
|
||||||
QTableWidgetItem(total_frames), QTableWidgetItem(date_created_string)]
|
QTableWidgetItem(total_frames), QTableWidgetItem(job['date_created'])]
|
||||||
|
|
||||||
for col, item in enumerate(items):
|
for col, item in enumerate(items):
|
||||||
self.job_list_view.setItem(row, col, item)
|
self.job_list_view.setItem(row, col, item)
|
||||||
@@ -412,8 +410,6 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def update_servers(self):
|
def update_servers(self):
|
||||||
found_servers = list(set(ZeroconfServer.found_hostnames() + self.added_hostnames))
|
found_servers = list(set(ZeroconfServer.found_hostnames() + self.added_hostnames))
|
||||||
found_servers = [x for x in found_servers if ZeroconfServer.get_hostname_properties(x)['api_version'] == API_VERSION]
|
|
||||||
|
|
||||||
# Always make sure local hostname is first
|
# Always make sure local hostname is first
|
||||||
if found_servers and not is_localhost(found_servers[0]):
|
if found_servers and not is_localhost(found_servers[0]):
|
||||||
for hostname in found_servers:
|
for hostname in found_servers:
|
||||||
@@ -595,21 +591,3 @@ class MainWindow(QMainWindow):
|
|||||||
if file_name:
|
if file_name:
|
||||||
self.new_job_window = NewRenderJobForm(file_name)
|
self.new_job_window = NewRenderJobForm(file_name)
|
||||||
self.new_job_window.show()
|
self.new_job_window.show()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# lazy load GUI frameworks
|
|
||||||
from PyQt6.QtWidgets import QApplication
|
|
||||||
|
|
||||||
# load application
|
|
||||||
# QtCore.QCoreApplication.setAttribute(QtCore.Qt.ApplicationAttribute.AA_MacDontSwapCtrlAndMeta)
|
|
||||||
app: QApplication = QApplication(sys.argv)
|
|
||||||
|
|
||||||
# configure main main_window
|
|
||||||
main_window = MainWindow()
|
|
||||||
# main_window.buffer_handler = buffer_handler
|
|
||||||
app.setActiveWindow(main_window)
|
|
||||||
|
|
||||||
main_window.show()
|
|
||||||
|
|
||||||
sys.exit(app.exec())
|
|
||||||
|
|||||||
481
src/ui/settings_window.py
Normal file
481
src/ui/settings_window.py
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import humanize
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from PyQt6 import QtCore
|
||||||
|
from PyQt6.QtCore import Qt, QSettings, pyqtSignal as Signal
|
||||||
|
from PyQt6.QtGui import QIcon
|
||||||
|
from PyQt6.QtWidgets import QApplication, QMainWindow, QListWidget, QListWidgetItem, QStackedWidget, QVBoxLayout, \
|
||||||
|
QWidget, QLabel, QCheckBox, QLineEdit, \
|
||||||
|
QComboBox, QPushButton, QHBoxLayout, QGroupBox, QTableWidget, QAbstractItemView, QTableWidgetItem, QHeaderView, \
|
||||||
|
QMessageBox
|
||||||
|
|
||||||
|
from api.server_proxy import RenderServerProxy
|
||||||
|
from engines.engine_manager import EngineManager
|
||||||
|
from utilities.config import Config
|
||||||
|
from utilities.misc_helper import launch_url, system_safe_path
|
||||||
|
from version import APP_AUTHOR, APP_NAME
|
||||||
|
|
||||||
|
settings = QSettings(APP_AUTHOR, APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsWindow(QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
if not EngineManager.engines_path: # fix issue where sometimes path was not set
|
||||||
|
EngineManager.engines_path = system_safe_path(
|
||||||
|
os.path.join(os.path.join(os.path.expanduser(Config.upload_folder),
|
||||||
|
'engines')))
|
||||||
|
|
||||||
|
self.installed_engines_table = None
|
||||||
|
|
||||||
|
self.setWindowTitle("Settings")
|
||||||
|
|
||||||
|
# Create the main layout
|
||||||
|
main_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Create the sidebar (QListWidget) for navigation
|
||||||
|
self.sidebar = QListWidget()
|
||||||
|
self.sidebar.setFixedWidth(150)
|
||||||
|
|
||||||
|
# Set the icon size
|
||||||
|
self.sidebar.setIconSize(QtCore.QSize(32, 32)) # Increase the icon size to 32x32 pixels
|
||||||
|
|
||||||
|
# Adjust the font size for the sidebar items
|
||||||
|
font = self.sidebar.font()
|
||||||
|
font.setPointSize(12) # Increase the font size
|
||||||
|
self.sidebar.setFont(font)
|
||||||
|
|
||||||
|
# Add items with icons to the sidebar
|
||||||
|
resources_dir = os.path.join(Path(__file__).resolve().parent.parent.parent, 'resources')
|
||||||
|
self.add_sidebar_item("General", os.path.join(resources_dir, "Gear.png"))
|
||||||
|
self.add_sidebar_item("Server", os.path.join(resources_dir, "Server.png"))
|
||||||
|
self.add_sidebar_item("Engines", os.path.join(resources_dir, "Blender.png"))
|
||||||
|
self.sidebar.setCurrentRow(0)
|
||||||
|
|
||||||
|
# Create the stacked widget to hold different settings pages
|
||||||
|
self.stacked_widget = QStackedWidget()
|
||||||
|
|
||||||
|
# Create pages for each section
|
||||||
|
general_page = self.create_general_page()
|
||||||
|
network_page = self.create_network_page()
|
||||||
|
engines_page = self.create_engines_page()
|
||||||
|
|
||||||
|
# Add pages to the stacked widget
|
||||||
|
self.stacked_widget.addWidget(general_page)
|
||||||
|
self.stacked_widget.addWidget(network_page)
|
||||||
|
self.stacked_widget.addWidget(engines_page)
|
||||||
|
|
||||||
|
# Connect the sidebar to the stacked widget
|
||||||
|
self.sidebar.currentRowChanged.connect(self.stacked_widget.setCurrentIndex)
|
||||||
|
|
||||||
|
# Create a horizontal layout to hold the sidebar and stacked widget
|
||||||
|
content_layout = QHBoxLayout()
|
||||||
|
content_layout.addWidget(self.sidebar)
|
||||||
|
content_layout.addWidget(self.stacked_widget)
|
||||||
|
|
||||||
|
# Add the content layout to the main layout
|
||||||
|
main_layout.addLayout(content_layout)
|
||||||
|
|
||||||
|
# Add the "OK" button at the bottom
|
||||||
|
ok_button = QPushButton("OK")
|
||||||
|
ok_button.clicked.connect(self.close)
|
||||||
|
ok_button.setFixedWidth(80)
|
||||||
|
ok_button.setDefault(True)
|
||||||
|
main_layout.addWidget(ok_button, alignment=Qt.AlignmentFlag.AlignRight)
|
||||||
|
|
||||||
|
# Create a central widget and set the layout
|
||||||
|
central_widget = QWidget()
|
||||||
|
central_widget.setLayout(main_layout)
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
|
||||||
|
self.setMinimumSize(700, 400)
|
||||||
|
|
||||||
|
def add_sidebar_item(self, name, icon_path):
|
||||||
|
"""Add an item with an icon to the sidebar."""
|
||||||
|
item = QListWidgetItem(QIcon(icon_path), name)
|
||||||
|
self.sidebar.addItem(item)
|
||||||
|
|
||||||
|
def create_general_page(self):
|
||||||
|
"""Create the General settings page."""
|
||||||
|
page = QWidget()
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Startup Settings Group
|
||||||
|
startup_group = QGroupBox("Startup Settings")
|
||||||
|
startup_layout = QVBoxLayout()
|
||||||
|
# startup_layout.addWidget(QCheckBox("Start application on system startup"))
|
||||||
|
check_for_updates_checkbox = QCheckBox("Check for updates automatically")
|
||||||
|
check_for_updates_checkbox.setChecked(settings.value("auto_check_for_updates", True))
|
||||||
|
check_for_updates_checkbox.stateChanged.connect(lambda state: settings.setValue("auto_check_for_updates", bool(state)))
|
||||||
|
startup_layout.addWidget(check_for_updates_checkbox)
|
||||||
|
startup_group.setLayout(startup_layout)
|
||||||
|
|
||||||
|
# Local Files Group
|
||||||
|
data_path = os.path.expanduser(Config.upload_folder)
|
||||||
|
path_size = sum(f.stat().st_size for f in Path(data_path).rglob('*') if f.is_file())
|
||||||
|
database_group = QGroupBox("Local Files")
|
||||||
|
database_layout = QVBoxLayout()
|
||||||
|
database_layout.addWidget(QLabel(f"Local Directory: {data_path}"))
|
||||||
|
database_layout.addWidget(QLabel(f"Size: {humanize.naturalsize(path_size, binary=True)}"))
|
||||||
|
open_database_path_button = QPushButton("Open Directory")
|
||||||
|
open_database_path_button.clicked.connect(lambda: launch_url(data_path))
|
||||||
|
open_database_path_button.setFixedWidth(200)
|
||||||
|
database_layout.addWidget(open_database_path_button)
|
||||||
|
database_group.setLayout(database_layout)
|
||||||
|
|
||||||
|
|
||||||
|
# Render Settings Group
|
||||||
|
render_settings_group = QGroupBox("Render Settings")
|
||||||
|
render_settings_layout = QVBoxLayout()
|
||||||
|
render_settings_layout.addWidget(QLabel("Restrict to render nodes with same:"))
|
||||||
|
require_same_engine_checkbox = QCheckBox("Renderer Version")
|
||||||
|
require_same_engine_checkbox.setChecked(settings.value("render_require_same_engine_version"))
|
||||||
|
require_same_engine_checkbox.stateChanged.connect(lambda state: settings.setValue("render_require_same_engine_version", bool(state)))
|
||||||
|
render_settings_layout.addWidget(require_same_engine_checkbox)
|
||||||
|
require_same_cpu_checkbox = QCheckBox("CPU Architecture")
|
||||||
|
require_same_cpu_checkbox.setChecked(settings.value("render_require_same_cpu_type"))
|
||||||
|
require_same_cpu_checkbox.stateChanged.connect(lambda state: settings.setValue("render_require_same_cpu_type", bool(state)))
|
||||||
|
render_settings_layout.addWidget(require_same_cpu_checkbox)
|
||||||
|
require_same_os_checkbox = QCheckBox("Operating System")
|
||||||
|
require_same_os_checkbox.setChecked(settings.value("render_require_same_os"))
|
||||||
|
require_same_os_checkbox.stateChanged.connect(lambda state: settings.setValue("render_require_same_os", bool(state)))
|
||||||
|
render_settings_layout.addWidget(require_same_os_checkbox)
|
||||||
|
render_settings_group.setLayout(render_settings_layout)
|
||||||
|
|
||||||
|
layout.addWidget(startup_group)
|
||||||
|
layout.addWidget(database_group)
|
||||||
|
layout.addWidget(render_settings_group)
|
||||||
|
|
||||||
|
layout.addStretch() # Add a stretch to push content to the top
|
||||||
|
page.setLayout(layout)
|
||||||
|
return page
|
||||||
|
|
||||||
|
def create_network_page(self):
|
||||||
|
"""Create the Network settings page."""
|
||||||
|
page = QWidget()
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Sharing Settings Group
|
||||||
|
sharing_group = QGroupBox("Sharing Settings")
|
||||||
|
sharing_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
enable_sharing_checkbox = QCheckBox("Enable other computers on the network to render to this machine")
|
||||||
|
enable_sharing_checkbox.setChecked(settings.value("enable_network_sharing", False))
|
||||||
|
enable_sharing_checkbox.stateChanged.connect(self.toggle_render_sharing)
|
||||||
|
sharing_layout.addWidget(enable_sharing_checkbox)
|
||||||
|
|
||||||
|
password_layout = QHBoxLayout()
|
||||||
|
password_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.enable_network_password_checkbox = QCheckBox("Enable network password:")
|
||||||
|
self.enable_network_password_checkbox.setChecked(settings.value("enable_network_password", False))
|
||||||
|
self.enable_network_password_checkbox.stateChanged.connect(self.enable_network_password_changed)
|
||||||
|
sharing_layout.addWidget(self.enable_network_password_checkbox)
|
||||||
|
self.network_password_line = QLineEdit()
|
||||||
|
self.network_password_line.setPlaceholderText("Enter a password")
|
||||||
|
self.network_password_line.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
|
self.network_password_line.setEnabled(settings.value("enable_network_password", False))
|
||||||
|
password_layout.addWidget(self.network_password_line)
|
||||||
|
self.show_password_button = QPushButton("Show")
|
||||||
|
self.show_password_button.setEnabled(settings.value("enable_network_password", False))
|
||||||
|
self.show_password_button.clicked.connect(self.show_password_button_pressed)
|
||||||
|
password_layout.addWidget(self.show_password_button)
|
||||||
|
sharing_layout.addLayout(password_layout)
|
||||||
|
|
||||||
|
sharing_group.setLayout(sharing_layout)
|
||||||
|
|
||||||
|
layout.addWidget(sharing_group)
|
||||||
|
|
||||||
|
layout.addStretch() # Add a stretch to push content to the top
|
||||||
|
page.setLayout(layout)
|
||||||
|
return page
|
||||||
|
|
||||||
|
def toggle_render_sharing(self, enable_sharing):
|
||||||
|
settings.setValue("enable_network_sharing", enable_sharing)
|
||||||
|
self.enable_network_password_checkbox.setEnabled(enable_sharing)
|
||||||
|
enable_password = enable_sharing and settings.value("enable_network_password", False)
|
||||||
|
self.network_password_line.setEnabled(enable_password)
|
||||||
|
self.show_password_button.setEnabled(enable_password)
|
||||||
|
|
||||||
|
def enable_network_password_changed(self, new_value):
|
||||||
|
settings.setValue("enable_network_password", new_value)
|
||||||
|
self.network_password_line.setEnabled(new_value)
|
||||||
|
self.show_password_button.setEnabled(new_value)
|
||||||
|
|
||||||
|
def show_password_button_pressed(self):
|
||||||
|
# toggle showing / hiding the password
|
||||||
|
show_pass = self.show_password_button.text() == "Show"
|
||||||
|
self.show_password_button.setText("Hide" if show_pass else "Show")
|
||||||
|
self.network_password_line.setEchoMode(QLineEdit.EchoMode.Normal if show_pass else QLineEdit.EchoMode.Normal)
|
||||||
|
|
||||||
|
def create_engines_page(self):
|
||||||
|
"""Create the Engines settings page."""
|
||||||
|
page = QWidget()
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Installed Engines Group
|
||||||
|
installed_group = QGroupBox("Installed Engines")
|
||||||
|
installed_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Setup table
|
||||||
|
self.installed_engines_table = EngineTableWidget()
|
||||||
|
self.installed_engines_table.row_selected.connect(self.engine_table_selected)
|
||||||
|
installed_layout.addWidget(self.installed_engines_table)
|
||||||
|
|
||||||
|
# Ignore system installs
|
||||||
|
engine_ignore_system_installs_checkbox = QCheckBox("Ignore system installs")
|
||||||
|
engine_ignore_system_installs_checkbox.setChecked(settings.value("engines_ignore_system_installs", False))
|
||||||
|
engine_ignore_system_installs_checkbox.stateChanged.connect(self.change_ignore_system_installs)
|
||||||
|
installed_layout.addWidget(engine_ignore_system_installs_checkbox)
|
||||||
|
|
||||||
|
# Engine Launch / Delete buttons
|
||||||
|
installed_buttons_layout = QHBoxLayout()
|
||||||
|
self.launch_engine_button = QPushButton("Launch")
|
||||||
|
self.launch_engine_button.setEnabled(False)
|
||||||
|
self.launch_engine_button.clicked.connect(self.launch_selected_engine)
|
||||||
|
self.delete_engine_button = QPushButton("Delete")
|
||||||
|
self.delete_engine_button.setEnabled(False)
|
||||||
|
self.delete_engine_button.clicked.connect(self.delete_selected_engine)
|
||||||
|
|
||||||
|
installed_buttons_layout.addWidget(self.launch_engine_button)
|
||||||
|
installed_buttons_layout.addWidget(self.delete_engine_button)
|
||||||
|
installed_layout.addLayout(installed_buttons_layout)
|
||||||
|
installed_group.setLayout(installed_layout)
|
||||||
|
|
||||||
|
# Engine Updates Group
|
||||||
|
engine_updates_group = QGroupBox("Auto-Install")
|
||||||
|
engine_updates_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
engine_download_layout = QHBoxLayout()
|
||||||
|
engine_download_layout.addWidget(QLabel("Enable Downloads for:"))
|
||||||
|
|
||||||
|
at_least_one_downloadable = False
|
||||||
|
for engine in EngineManager.downloadable_engines():
|
||||||
|
engine_download_check = QCheckBox(engine.name())
|
||||||
|
is_checked = settings.value(f"engine_download-{engine.name()}", False)
|
||||||
|
at_least_one_downloadable |= is_checked
|
||||||
|
engine_download_check.setChecked(is_checked)
|
||||||
|
# Capture the checkbox correctly using a default argument in lambda
|
||||||
|
engine_download_check.clicked.connect(
|
||||||
|
lambda state, checkbox=engine_download_check: self.engine_download_settings_changed(state, checkbox.text())
|
||||||
|
)
|
||||||
|
engine_download_layout.addWidget(engine_download_check)
|
||||||
|
|
||||||
|
engine_updates_layout.addLayout(engine_download_layout)
|
||||||
|
|
||||||
|
check_for_engine_updates_checkbox = QCheckBox("Check for new versions on launch")
|
||||||
|
check_for_engine_updates_checkbox.setChecked(settings.value('check_for_engine_updates_on_launch', True))
|
||||||
|
check_for_engine_updates_checkbox.setEnabled(at_least_one_downloadable)
|
||||||
|
check_for_engine_updates_checkbox.stateChanged.connect(
|
||||||
|
lambda state: settings.setValue("check_for_engine_updates_on_launch", bool(state)))
|
||||||
|
engine_updates_layout.addWidget(check_for_engine_updates_checkbox)
|
||||||
|
self.engines_last_update_label = QLabel()
|
||||||
|
self.update_last_checked_label()
|
||||||
|
self.engines_last_update_label.setEnabled(at_least_one_downloadable)
|
||||||
|
engine_updates_layout.addWidget(self.engines_last_update_label)
|
||||||
|
self.check_for_new_engines_button = QPushButton("Check for New Versions...")
|
||||||
|
self.check_for_new_engines_button.setEnabled(at_least_one_downloadable)
|
||||||
|
self.check_for_new_engines_button.clicked.connect(self.check_for_new_engines)
|
||||||
|
engine_updates_layout.addWidget(self.check_for_new_engines_button)
|
||||||
|
engine_updates_group.setLayout(engine_updates_layout)
|
||||||
|
|
||||||
|
layout.addWidget(installed_group)
|
||||||
|
layout.addWidget(engine_updates_group)
|
||||||
|
|
||||||
|
layout.addStretch() # Add a stretch to push content to the top
|
||||||
|
page.setLayout(layout)
|
||||||
|
return page
|
||||||
|
|
||||||
|
def change_ignore_system_installs(self, value):
|
||||||
|
settings.setValue("engines_ignore_system_installs", bool(value))
|
||||||
|
self.installed_engines_table.update_table()
|
||||||
|
|
||||||
|
def update_last_checked_label(self):
|
||||||
|
"""Retrieve the last check timestamp and return a human-friendly string."""
|
||||||
|
last_checked_str = settings.value("engines_last_update_time", None)
|
||||||
|
if not last_checked_str:
|
||||||
|
time_string = "Never"
|
||||||
|
else:
|
||||||
|
last_checked_dt = datetime.fromisoformat(last_checked_str)
|
||||||
|
now = datetime.now()
|
||||||
|
time_string = humanize.naturaltime(now - last_checked_dt)
|
||||||
|
self.engines_last_update_label.setText(f"Last Updated: {time_string}")
|
||||||
|
|
||||||
|
def engine_download_settings_changed(self, state, engine_name):
|
||||||
|
settings.setValue(f"engine_download-{engine_name}", state)
|
||||||
|
|
||||||
|
at_least_one_downloadable = False
|
||||||
|
for engine in EngineManager.downloadable_engines():
|
||||||
|
at_least_one_downloadable |= settings.value(f"engine_download-{engine.name()}", False)
|
||||||
|
self.check_for_new_engines_button.setEnabled(at_least_one_downloadable)
|
||||||
|
self.check_for_engine_updates_checkbox.setEnabled(at_least_one_downloadable)
|
||||||
|
self.engines_last_update_label.setEnabled(at_least_one_downloadable)
|
||||||
|
|
||||||
|
def delete_selected_engine(self):
|
||||||
|
engine_info = self.installed_engines_table.selected_engine_data()
|
||||||
|
reply = QMessageBox.question(self, f"Delete {engine_info['engine']} {engine_info['version']}?",
|
||||||
|
f"Do you want to delete {engine_info['engine']} {engine_info['version']}?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||||
|
|
||||||
|
if reply is not QMessageBox.StandardButton.Yes:
|
||||||
|
return
|
||||||
|
|
||||||
|
delete_result = EngineManager.delete_engine_download(engine_info.get('engine'),
|
||||||
|
engine_info.get('version'),
|
||||||
|
engine_info.get('system_os'),
|
||||||
|
engine_info.get('cpu'))
|
||||||
|
|
||||||
|
if delete_result:
|
||||||
|
QMessageBox.information(self, f"{engine_info['engine']} {engine_info['version']} Deleted",
|
||||||
|
f"{engine_info['engine']} {engine_info['version']} deleted successfully",
|
||||||
|
QMessageBox.StandardButton.Ok)
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(self, f"Unknown Error",
|
||||||
|
f"Unknown error while deleting {engine_info['engine']} {engine_info['version']}.",
|
||||||
|
QMessageBox.StandardButton.Ok)
|
||||||
|
|
||||||
|
self.installed_engines_table.update_table(use_cached=False)
|
||||||
|
|
||||||
|
def launch_selected_engine(self):
|
||||||
|
engine_info = self.installed_engines_table.selected_engine_data()
|
||||||
|
if engine_info:
|
||||||
|
launch_url(engine_info['path'])
|
||||||
|
|
||||||
|
def engine_table_selected(self):
|
||||||
|
engine_data = self.installed_engines_table.selected_engine_data()
|
||||||
|
if engine_data:
|
||||||
|
self.launch_engine_button.setEnabled(bool(engine_data.get('path') or True))
|
||||||
|
self.delete_engine_button.setEnabled(engine_data.get('type') == 'managed')
|
||||||
|
else:
|
||||||
|
self.launch_engine_button.setEnabled(False)
|
||||||
|
self.delete_engine_button.setEnabled(False)
|
||||||
|
|
||||||
|
def check_for_new_engines(self):
|
||||||
|
|
||||||
|
ignore_system = settings.value("engines_ignore_system_installs", False)
|
||||||
|
messagebox_shown = False
|
||||||
|
for engine in EngineManager.downloadable_engines():
|
||||||
|
if settings.value(f'engine_download-{engine.name()}', False):
|
||||||
|
result = EngineManager.is_engine_update_available(engine, ignore_system_installs=ignore_system)
|
||||||
|
if result:
|
||||||
|
result['name'] = engine.name()
|
||||||
|
msg_box = QMessageBox()
|
||||||
|
msg_box.setWindowTitle(f"{result['name']} ({result['version']}) Available")
|
||||||
|
msg_box.setText(f"A new version of {result['name']} is available ({result['version']}).\n\n"
|
||||||
|
f"Would you like to download it now?")
|
||||||
|
msg_box.setIcon(QMessageBox.Icon.Question)
|
||||||
|
msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||||
|
msg_result = msg_box.exec()
|
||||||
|
messagebox_shown = True
|
||||||
|
if msg_result == QMessageBox.StandardButton.Yes:
|
||||||
|
EngineManager.download_engine(engine=engine.name(), version=result['version'], background=True,
|
||||||
|
ignore_system=ignore_system)
|
||||||
|
self.update_engine_download_status()
|
||||||
|
|
||||||
|
if not messagebox_shown:
|
||||||
|
msg_box = QMessageBox()
|
||||||
|
msg_box.setWindowTitle("No Updates Available")
|
||||||
|
msg_box.setText("All your render engines are up-to-date.")
|
||||||
|
msg_box.setIcon(QMessageBox.Icon.Information)
|
||||||
|
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||||
|
msg_box.exec()
|
||||||
|
|
||||||
|
settings.setValue("engines_last_update_time", datetime.now().isoformat())
|
||||||
|
self.update_engine_download_status()
|
||||||
|
|
||||||
|
def update_engine_download_status(self):
|
||||||
|
running_tasks = [x for x in EngineManager.download_tasks if x.is_alive()]
|
||||||
|
if not running_tasks:
|
||||||
|
self.update_last_checked_label()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.engines_last_update_label.setText(f"Downloading {running_tasks[0].engine} ({running_tasks[0].version})...")
|
||||||
|
|
||||||
|
|
||||||
|
class EngineTableWidget(QWidget):
|
||||||
|
row_selected = Signal()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.table = QTableWidget(0, 4)
|
||||||
|
self.table.setHorizontalHeaderLabels(["Engine", "Version", "Type", "Path"])
|
||||||
|
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
|
self.table.verticalHeader().setVisible(False)
|
||||||
|
# self.table_widget.itemSelectionChanged.connect(self.engine_picked)
|
||||||
|
self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
|
self.table.selectionModel().selectionChanged.connect(self.on_selection_changed)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
layout.addWidget(self.table)
|
||||||
|
|
||||||
|
self.raw_server_data = None
|
||||||
|
|
||||||
|
def showEvent(self, event):
|
||||||
|
"""Runs when the widget is about to be shown."""
|
||||||
|
self.update_table()
|
||||||
|
super().showEvent(event) # Ensure normal event processing
|
||||||
|
|
||||||
|
def update_table(self, use_cached=True):
|
||||||
|
if not self.raw_server_data or not use_cached:
|
||||||
|
self.raw_server_data = RenderServerProxy(socket.gethostname()).get_renderer_info()
|
||||||
|
if not self.raw_server_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
table_data = [] # convert the data into a flat list
|
||||||
|
for _, engine_data in self.raw_server_data.items():
|
||||||
|
table_data.extend(engine_data['versions'])
|
||||||
|
|
||||||
|
if settings.value("engines_ignore_system_installs", False):
|
||||||
|
table_data = [x for x in table_data if x['type'] != 'system']
|
||||||
|
|
||||||
|
self.table.clear()
|
||||||
|
self.table.setRowCount(len(table_data))
|
||||||
|
self.table.setColumnCount(4)
|
||||||
|
|
||||||
|
self.table.setHorizontalHeaderLabels(['Engine', 'Version', 'Type', 'Path'])
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
|
||||||
|
|
||||||
|
for row, engine in enumerate(table_data):
|
||||||
|
self.table.setItem(row, 0, QTableWidgetItem(engine['engine']))
|
||||||
|
self.table.setItem(row, 1, QTableWidgetItem(engine['version']))
|
||||||
|
self.table.setItem(row, 2, QTableWidgetItem(engine['type']))
|
||||||
|
self.table.setItem(row, 3, QTableWidgetItem(engine['path']))
|
||||||
|
|
||||||
|
self.table.selectRow(0)
|
||||||
|
|
||||||
|
def selected_engine_data(self):
|
||||||
|
"""Returns the data from the selected row as a dictionary."""
|
||||||
|
row = self.table.currentRow() # Get the selected row index
|
||||||
|
|
||||||
|
if row < 0 or not len(self.table.selectedItems()): # No row selected
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"engine": self.table.item(row, 0).text(),
|
||||||
|
"version": self.table.item(row, 1).text(),
|
||||||
|
"type": self.table.item(row, 2).text(),
|
||||||
|
"path": self.table.item(row, 3).text(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def on_selection_changed(self):
|
||||||
|
self.row_selected.emit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QApplication([])
|
||||||
|
window = SettingsWindow()
|
||||||
|
window.show()
|
||||||
|
app.exec()
|
||||||
@@ -14,6 +14,8 @@ class MenuBar(QMenuBar):
|
|||||||
def __init__(self, parent=None) -> None:
|
def __init__(self, parent=None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.settings_window = None
|
||||||
|
|
||||||
# setup menus
|
# setup menus
|
||||||
file_menu = self.addMenu("File")
|
file_menu = self.addMenu("File")
|
||||||
# edit_menu = self.addMenu("Edit")
|
# edit_menu = self.addMenu("Edit")
|
||||||
@@ -30,7 +32,7 @@ class MenuBar(QMenuBar):
|
|||||||
settings_action = QAction("Settings...", self)
|
settings_action = QAction("Settings...", self)
|
||||||
settings_action.triggered.connect(self.show_settings)
|
settings_action.triggered.connect(self.show_settings)
|
||||||
settings_action.setShortcut(f'Ctrl+,')
|
settings_action.setShortcut(f'Ctrl+,')
|
||||||
# file_menu.addAction(settings_action) # todo: enable once we have a setting screen
|
file_menu.addAction(settings_action)
|
||||||
# exit
|
# exit
|
||||||
exit_action = QAction('&Exit', self)
|
exit_action = QAction('&Exit', self)
|
||||||
exit_action.setShortcut('Ctrl+Q')
|
exit_action.setShortcut('Ctrl+Q')
|
||||||
@@ -49,7 +51,9 @@ class MenuBar(QMenuBar):
|
|||||||
self.parent().new_job()
|
self.parent().new_job()
|
||||||
|
|
||||||
def show_settings(self):
|
def show_settings(self):
|
||||||
pass
|
from src.ui.settings_window import SettingsWindow
|
||||||
|
self.settings_window = SettingsWindow()
|
||||||
|
self.settings_window.show()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def show_about():
|
def show_about():
|
||||||
@@ -60,7 +64,7 @@ class MenuBar(QMenuBar):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def check_for_updates():
|
def check_for_updates():
|
||||||
from src.utilities.misc_helper import check_for_updates
|
from src.utilities.misc_helper import check_for_updates
|
||||||
from src.version import APP_NAME, APP_VERSION, APP_REPO_NAME, APP_REPO_OWNER
|
from version import APP_NAME, APP_VERSION, APP_REPO_NAME, APP_REPO_OWNER
|
||||||
found_update = check_for_updates(APP_REPO_NAME, APP_REPO_OWNER, APP_NAME, APP_VERSION)
|
found_update = check_for_updates(APP_REPO_NAME, APP_REPO_OWNER, APP_NAME, APP_VERSION)
|
||||||
if found_update:
|
if found_update:
|
||||||
dialog = UpdateDialog(found_update, APP_VERSION)
|
dialog = UpdateDialog(found_update, APP_VERSION)
|
||||||
|
|||||||
@@ -210,18 +210,3 @@ def num_to_alphanumeric(num):
|
|||||||
result += characters[remainder]
|
result += characters[remainder]
|
||||||
|
|
||||||
return result[::-1] # Reverse the result to get the correct alphanumeric string
|
return result[::-1] # Reverse the result to get the correct alphanumeric string
|
||||||
|
|
||||||
|
|
||||||
def iso_datestring_to_formatted_datestring(iso_date_string):
|
|
||||||
from dateutil import parser
|
|
||||||
import pytz
|
|
||||||
|
|
||||||
# Parse the ISO date string into a datetime object and convert timezones
|
|
||||||
date = parser.isoparse(iso_date_string).astimezone(pytz.UTC)
|
|
||||||
local_timezone = datetime.now().astimezone().tzinfo
|
|
||||||
date_local = date.astimezone(local_timezone)
|
|
||||||
|
|
||||||
# Format the date to the desired readable yet sortable format with 12-hour time
|
|
||||||
formatted_date = date_local.strftime('%Y-%m-%d %I:%M %p')
|
|
||||||
|
|
||||||
return formatted_date
|
|
||||||
|
|||||||
@@ -32,11 +32,9 @@ class ZeroconfServer:
|
|||||||
def start(cls, listen_only=False):
|
def start(cls, listen_only=False):
|
||||||
if not cls.service_type:
|
if not cls.service_type:
|
||||||
raise RuntimeError("The 'configure' method must be run before starting the zeroconf server")
|
raise RuntimeError("The 'configure' method must be run before starting the zeroconf server")
|
||||||
elif not listen_only:
|
logger.debug("Starting zeroconf service")
|
||||||
logger.debug(f"Starting zeroconf service")
|
if not listen_only:
|
||||||
cls._register_service()
|
cls._register_service()
|
||||||
else:
|
|
||||||
logger.debug(f"Starting zeroconf service - Listen only mode")
|
|
||||||
cls._browse_services()
|
cls._browse_services()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
Reference in New Issue
Block a user