New UI Redesign in pyqt6 (#56)
* Initial commit for new UI * Initial commit for new UI * WIP * Status bar updates and has an icon for online / offline * Add log_viewer.py * Use JSON for delete_engine_download API * Fix class issue with Downloaders * Move Config class to new ui * Add engine_browser.py * Add a close event handler to the main window * Fix issue with engine manager not deleting engines properly * Rearrange all the files * Add icons and resources * Cache system info in RenderServerProxy * Toolbar polish * Fix resource path in status bar * Add config_dir to misc_helper.py * Add try block to zeroconf setup * Add add_job.py * Add raw args to add_job.py
@@ -3,6 +3,7 @@ update_engines_on_launch: true
|
|||||||
max_content_path: 100000000
|
max_content_path: 100000000
|
||||||
server_log_level: info
|
server_log_level: info
|
||||||
log_buffer_length: 250
|
log_buffer_length: 250
|
||||||
|
subjob_connection_timeout: 120
|
||||||
flask_log_level: error
|
flask_log_level: error
|
||||||
flask_debug_enable: false
|
flask_debug_enable: false
|
||||||
queue_eval_seconds: 1
|
queue_eval_seconds: 1
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ if __name__ == '__main__':
|
|||||||
start_server_input = input("Local server not running. Start server? (y/n) ")
|
start_server_input = input("Local server not running. Start server? (y/n) ")
|
||||||
if start_server_input and start_server_input[0].lower() == "y":
|
if start_server_input and start_server_input[0].lower() == "y":
|
||||||
# Startup the local server
|
# Startup the local server
|
||||||
start_server(background_thread=True)
|
start_server()
|
||||||
test = server_proxy.connect()
|
test = server_proxy.connect()
|
||||||
print(f"connected? {test}")
|
print(f"connected? {test}")
|
||||||
else:
|
else:
|
||||||
|
|||||||
6
main.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
''' main.py '''
|
||||||
|
from src import init
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
sys.exit(init.run())
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
requests_toolbelt==1.0.0
|
|
||||||
psutil==5.9.6
|
psutil==5.9.6
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
Flask==3.0.0
|
Flask==3.0.0
|
||||||
@@ -12,6 +11,6 @@ Pillow==10.1.0
|
|||||||
zeroconf==0.119.0
|
zeroconf==0.119.0
|
||||||
Pypubsub~=4.0.3
|
Pypubsub~=4.0.3
|
||||||
tqdm==4.66.1
|
tqdm==4.66.1
|
||||||
dmglib==0.9.4
|
|
||||||
plyer==2.1.0
|
plyer==2.1.0
|
||||||
pyobjus==1.2.3
|
PyQt6~=6.5.3
|
||||||
|
PySide6~=6.6.0
|
||||||
BIN
resources/Rectangle.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
resources/icons/AddProduct.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
resources/icons/Adobe After Effects.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
resources/icons/Blender.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
resources/icons/Console.png
Normal file
|
After Width: | Height: | Size: 921 B |
BIN
resources/icons/Document.png
Normal file
|
After Width: | Height: | Size: 476 B |
BIN
resources/icons/Download.png
Normal file
|
After Width: | Height: | Size: 979 B |
BIN
resources/icons/FFmpeg.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
resources/icons/Gear.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
resources/icons/GreenCircle.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
resources/icons/Monitor.png
Normal file
|
After Width: | Height: | Size: 249 B |
BIN
resources/icons/RedSquare.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
resources/icons/SearchFolder.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
resources/icons/Server.png
Normal file
|
After Width: | Height: | Size: 694 B |
BIN
resources/icons/SoftwareInstaller.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
resources/icons/StopSign.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
resources/icons/Synchronize.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
resources/icons/Trash.png
Normal file
|
After Width: | Height: | Size: 816 B |
@@ -127,7 +127,7 @@ def process_zipped_project(zip_path):
|
|||||||
return extracted_project_path
|
return extracted_project_path
|
||||||
|
|
||||||
|
|
||||||
def create_render_jobs(jobs_list, loaded_project_local_path, job_dir, enable_split_jobs=False):
|
def create_render_jobs(jobs_list, loaded_project_local_path, job_dir):
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for job_data in jobs_list:
|
for job_data in jobs_list:
|
||||||
@@ -162,7 +162,7 @@ def create_render_jobs(jobs_list, loaded_project_local_path, job_dir, enable_spl
|
|||||||
worker.end_frame = int(job_data.get("end_frame", worker.end_frame))
|
worker.end_frame = int(job_data.get("end_frame", worker.end_frame))
|
||||||
|
|
||||||
# determine if we can / should split the job
|
# determine if we can / should split the job
|
||||||
if enable_split_jobs and (worker.total_frames > 1) and not worker.parent:
|
if job_data.get("enable_split_jobs", False) and (worker.total_frames > 1) and not worker.parent:
|
||||||
DistributedJobManager.split_into_subjobs(worker, job_data, loaded_project_local_path)
|
DistributedJobManager.split_into_subjobs(worker, job_data, loaded_project_local_path)
|
||||||
else:
|
else:
|
||||||
logger.debug("Not splitting into subjobs")
|
logger.debug("Not splitting into subjobs")
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ from src.engines.core.base_worker import string_to_status, RenderStatus
|
|||||||
from src.engines.engine_manager import EngineManager
|
from src.engines.engine_manager import EngineManager
|
||||||
from src.render_queue import RenderQueue, JobNotFoundError
|
from src.render_queue import RenderQueue, JobNotFoundError
|
||||||
from src.utilities.config import Config
|
from src.utilities.config import Config
|
||||||
from src.utilities.misc_helper import system_safe_path, current_system_os, current_system_cpu, current_system_os_version
|
from src.utilities.misc_helper import system_safe_path, current_system_os, current_system_cpu, \
|
||||||
|
current_system_os_version, config_dir
|
||||||
from src.utilities.server_helper import generate_thumbnail_for_job
|
from src.utilities.server_helper import generate_thumbnail_for_job
|
||||||
from src.utilities.zeroconf_server import ZeroconfServer
|
from src.utilities.zeroconf_server import ZeroconfServer
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ def sorted_jobs(all_jobs, sort_by_date=True):
|
|||||||
@server.route('/')
|
@server.route('/')
|
||||||
@server.route('/index')
|
@server.route('/index')
|
||||||
def index():
|
def index():
|
||||||
with open(system_safe_path('config/presets.yaml')) as f:
|
with open(system_safe_path(os.path.join(config_dir(), 'presets.yaml'))) as f:
|
||||||
render_presets = yaml.load(f, Loader=yaml.FullLoader)
|
render_presets = yaml.load(f, Loader=yaml.FullLoader)
|
||||||
|
|
||||||
return render_template('index.html', all_jobs=sorted_jobs(RenderQueue.all_jobs()),
|
return render_template('index.html', all_jobs=sorted_jobs(RenderQueue.all_jobs()),
|
||||||
@@ -312,8 +313,7 @@ def add_job_handler():
|
|||||||
if loaded_project_local_path.lower().endswith('.zip'):
|
if loaded_project_local_path.lower().endswith('.zip'):
|
||||||
loaded_project_local_path = process_zipped_project(loaded_project_local_path)
|
loaded_project_local_path = process_zipped_project(loaded_project_local_path)
|
||||||
|
|
||||||
results = create_render_jobs(jobs_list, loaded_project_local_path, referred_name,
|
results = create_render_jobs(jobs_list, loaded_project_local_path, referred_name)
|
||||||
server.config['enable_split_jobs'])
|
|
||||||
for response in results:
|
for response in results:
|
||||||
if response.get('error', None):
|
if response.get('error', None):
|
||||||
return results, 400
|
return results, 400
|
||||||
@@ -417,18 +417,18 @@ def status():
|
|||||||
|
|
||||||
@server.get('/api/renderer_info')
|
@server.get('/api/renderer_info')
|
||||||
def renderer_info():
|
def renderer_info():
|
||||||
|
return_simple = request.args.get('simple', False)
|
||||||
renderer_data = {}
|
renderer_data = {}
|
||||||
for engine_name in EngineManager.supported_engines():
|
for engine in EngineManager.supported_engines():
|
||||||
engine = EngineManager.engine_with_name(engine_name)
|
|
||||||
|
|
||||||
# Get all installed versions of engine
|
# Get all installed versions of engine
|
||||||
installed_versions = EngineManager.all_versions_for_engine(engine_name)
|
installed_versions = EngineManager.all_versions_for_engine(engine.name())
|
||||||
if installed_versions:
|
if installed_versions:
|
||||||
install_path = installed_versions[0]['path']
|
install_path = installed_versions[0]['path']
|
||||||
renderer_data[engine_name] = {'is_available': RenderQueue.is_available_for_job(engine.name()),
|
renderer_data[engine.name()] = {'is_available': RenderQueue.is_available_for_job(engine.name()),
|
||||||
'versions': installed_versions,
|
'versions': installed_versions}
|
||||||
'supported_extensions': engine.supported_extensions,
|
if not return_simple:
|
||||||
'supported_export_formats': engine(install_path).get_output_formats()}
|
renderer_data[engine.name()]['supported_extensions'] = engine.supported_extensions
|
||||||
|
renderer_data[engine.name()]['supported_export_formats'] = engine(install_path).get_output_formats()
|
||||||
return renderer_data
|
return renderer_data
|
||||||
|
|
||||||
|
|
||||||
@@ -471,19 +471,20 @@ def download_engine():
|
|||||||
|
|
||||||
@server.post('/api/delete_engine')
|
@server.post('/api/delete_engine')
|
||||||
def delete_engine_download():
|
def delete_engine_download():
|
||||||
delete_result = EngineManager.delete_engine_download(request.args.get('engine'),
|
json_data = request.json
|
||||||
request.args.get('version'),
|
delete_result = EngineManager.delete_engine_download(json_data.get('engine'),
|
||||||
request.args.get('system_os'),
|
json_data.get('version'),
|
||||||
request.args.get('cpu'))
|
json_data.get('system_os'),
|
||||||
|
json_data.get('cpu'))
|
||||||
return "Success" if delete_result else \
|
return "Success" if delete_result else \
|
||||||
(f"Error deleting {request.args.get('engine')} {request.args.get('version')}", 500)
|
(f"Error deleting {json_data.get('engine')} {json_data.get('version')}", 500)
|
||||||
|
|
||||||
|
|
||||||
@server.get('/api/renderer/<renderer>/args')
|
@server.get('/api/renderer/<renderer>/args')
|
||||||
def get_renderer_args(renderer):
|
def get_renderer_args(renderer):
|
||||||
try:
|
try:
|
||||||
renderer_engine_class = EngineManager.engine_with_name(renderer)
|
renderer_engine_class = EngineManager.engine_with_name(renderer)
|
||||||
return renderer_engine_class.get_arguments()
|
return renderer_engine_class().get_arguments()
|
||||||
except LookupError:
|
except LookupError:
|
||||||
return f"Cannot find renderer '{renderer}'", 400
|
return f"Cannot find renderer '{renderer}'", 400
|
||||||
|
|
||||||
@@ -499,13 +500,6 @@ def start_server():
|
|||||||
RenderQueue.evaluate_queue()
|
RenderQueue.evaluate_queue()
|
||||||
time.sleep(delay_sec)
|
time.sleep(delay_sec)
|
||||||
|
|
||||||
# Load Config YAML
|
|
||||||
config_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'config')
|
|
||||||
Config.load_config(system_safe_path(os.path.join(config_dir, 'config.yaml')))
|
|
||||||
|
|
||||||
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(module)s: %(message)s', datefmt='%d-%b-%y %H:%M:%S',
|
|
||||||
level=Config.server_log_level.upper())
|
|
||||||
|
|
||||||
# get hostname
|
# get hostname
|
||||||
local_hostname = socket.gethostname()
|
local_hostname = socket.gethostname()
|
||||||
local_hostname = local_hostname + (".local" if not local_hostname.endswith(".local") else "")
|
local_hostname = local_hostname + (".local" if not local_hostname.endswith(".local") else "")
|
||||||
|
|||||||
@@ -34,9 +34,14 @@ class RenderServerProxy:
|
|||||||
self.__offline_flags = 0
|
self.__offline_flags = 0
|
||||||
self.update_cadence = 5
|
self.update_cadence = 5
|
||||||
|
|
||||||
|
# Cache some basic server info
|
||||||
|
self.system_cpu = None
|
||||||
|
self.system_cpu_count = None
|
||||||
|
self.system_os = None
|
||||||
|
self.system_os_version = None
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
status = self.request_data('status')
|
return self.status()
|
||||||
return status
|
|
||||||
|
|
||||||
def is_online(self):
|
def is_online(self):
|
||||||
if self.__update_in_background:
|
if self.__update_in_background:
|
||||||
@@ -48,7 +53,7 @@ class RenderServerProxy:
|
|||||||
if not self.is_online():
|
if not self.is_online():
|
||||||
return "Offline"
|
return "Offline"
|
||||||
running_jobs = [x for x in self.__jobs_cache if x['status'] == 'running'] if self.__jobs_cache else []
|
running_jobs = [x for x in self.__jobs_cache if x['status'] == 'running'] if self.__jobs_cache else []
|
||||||
return f"{len(running_jobs)} running" if running_jobs else "Available"
|
return f"{len(running_jobs)} running" if running_jobs else "Ready"
|
||||||
|
|
||||||
def request_data(self, payload, timeout=5):
|
def request_data(self, payload, timeout=5):
|
||||||
try:
|
try:
|
||||||
@@ -72,6 +77,8 @@ class RenderServerProxy:
|
|||||||
return requests.get(f'http://{self.hostname}:{self.port}/api/{payload}', timeout=timeout)
|
return requests.get(f'http://{self.hostname}:{self.port}/api/{payload}', timeout=timeout)
|
||||||
|
|
||||||
def start_background_update(self):
|
def start_background_update(self):
|
||||||
|
if self.__update_in_background:
|
||||||
|
return
|
||||||
self.__update_in_background = True
|
self.__update_in_background = True
|
||||||
|
|
||||||
def thread_worker():
|
def thread_worker():
|
||||||
@@ -113,12 +120,24 @@ class RenderServerProxy:
|
|||||||
def cancel_job(self, job_id, confirm=False):
|
def cancel_job(self, job_id, confirm=False):
|
||||||
return self.request_data(f'job/{job_id}/cancel?confirm={confirm}')
|
return self.request_data(f'job/{job_id}/cancel?confirm={confirm}')
|
||||||
|
|
||||||
|
def delete_job(self, job_id, confirm=False):
|
||||||
|
return self.request_data(f'job/{job_id}/delete?confirm={confirm}')
|
||||||
|
|
||||||
def get_status(self):
|
def get_status(self):
|
||||||
return self.request_data('status')
|
status = self.request_data('status')
|
||||||
|
if not self.system_cpu:
|
||||||
|
self.system_cpu = status['system_cpu']
|
||||||
|
self.system_cpu_count = status['cpu_count']
|
||||||
|
self.system_os = status['system_os']
|
||||||
|
self.system_os_version = status['system_os_version']
|
||||||
|
return status
|
||||||
|
|
||||||
def is_engine_available(self, engine_name):
|
def is_engine_available(self, engine_name):
|
||||||
return self.request_data(f'{engine_name}/is_available')
|
return self.request_data(f'{engine_name}/is_available')
|
||||||
|
|
||||||
|
def get_all_engines(self):
|
||||||
|
return self.request_data('all_engines')
|
||||||
|
|
||||||
def notify_parent_of_status_change(self, parent_id, subjob):
|
def notify_parent_of_status_change(self, parent_id, subjob):
|
||||||
return requests.post(f'http://{self.hostname}:{self.port}/api/job/{parent_id}/notify_parent_of_status_change',
|
return requests.post(f'http://{self.hostname}:{self.port}/api/job/{parent_id}/notify_parent_of_status_change',
|
||||||
json=subjob.json())
|
json=subjob.json())
|
||||||
@@ -160,3 +179,12 @@ class RenderServerProxy:
|
|||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
# --- Renderer --- #
|
||||||
|
|
||||||
|
def get_renderer_info(self, timeout=5, simple=False):
|
||||||
|
all_data = self.request_data(f'renderer_info?simple={simple}', timeout=timeout)
|
||||||
|
return all_data
|
||||||
|
|
||||||
|
def delete_engine(self, engine, version, system_cpu=None):
|
||||||
|
form_data = {'engine': engine, 'version': version, 'system_cpu': system_cpu}
|
||||||
|
return requests.post(f'http://{self.hostname}:{self.port}/api/delete_engine', json=form_data)
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ class BlenderDownloader(EngineDownloader):
|
|||||||
minor_versions = [x for x in cls.__get_minor_versions(major_version, system_os, cpu) if x['version'] == version]
|
minor_versions = [x for x in cls.__get_minor_versions(major_version, system_os, cpu) if x['version'] == version]
|
||||||
# we get the URL instead of calculating it ourselves. May change this
|
# we get the URL instead of calculating it ourselves. May change this
|
||||||
|
|
||||||
cls.__download_and_extract_app(remote_url=minor_versions[0]['url'], download_location=download_location,
|
cls.download_and_extract_app(remote_url=minor_versions[0]['url'], download_location=download_location,
|
||||||
timeout=timeout)
|
timeout=timeout)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logger.error("Cannot find requested engine")
|
logger.error("Cannot find requested engine")
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class EngineDownloader:
|
|||||||
raise NotImplementedError # implement this method in your engine subclass
|
raise NotImplementedError # implement this method in your engine subclass
|
||||||
|
|
||||||
@classmethod
|
@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):
|
||||||
|
|
||||||
# Create a temp download directory
|
# Create a temp download directory
|
||||||
temp_download_dir = tempfile.mkdtemp()
|
temp_download_dir = tempfile.mkdtemp()
|
||||||
@@ -154,5 +154,7 @@ def copy_directory_contents(src_dir, dest_dir):
|
|||||||
# Otherwise, copy the file
|
# Otherwise, copy the file
|
||||||
shutil.copy2(item_path, dest_item_path)
|
shutil.copy2(item_path, dest_item_path)
|
||||||
|
|
||||||
|
except PermissionError as ex:
|
||||||
|
logger.error(f"Permissions error: {ex}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error copying directory contents: {e}")
|
logger.exception(f"Error copying directory contents: {e}")
|
||||||
|
|||||||
@@ -170,14 +170,22 @@ class EngineManager:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def delete_engine_download(cls, engine, version, system_os=None, cpu=None):
|
def delete_engine_download(cls, engine, version, system_os=None, cpu=None):
|
||||||
logger.info(f"Requested deletion of engine: {engine}-{version}")
|
logger.info(f"Requested deletion of engine: {engine}-{version}")
|
||||||
|
|
||||||
found = cls.is_version_downloaded(engine, version, system_os, cpu)
|
found = cls.is_version_downloaded(engine, version, system_os, cpu)
|
||||||
if found:
|
if found and found['type'] == 'managed': # don't delete system installs
|
||||||
dir_path = os.path.dirname(found['path'])
|
# find the root directory of the engine executable
|
||||||
shutil.rmtree(dir_path, ignore_errors=True)
|
root_dir_name = '-'.join([engine, version, found['system_os'], found['cpu']])
|
||||||
|
remove_path = os.path.join(found['path'].split(root_dir_name)[0], root_dir_name)
|
||||||
|
# delete the file path
|
||||||
|
logger.info(f"Deleting engine at path: {remove_path}")
|
||||||
|
shutil.rmtree(remove_path, ignore_errors=False)
|
||||||
logger.info(f"Engine {engine}-{version}-{found['system_os']}-{found['cpu']} successfully deleted")
|
logger.info(f"Engine {engine}-{version}-{found['system_os']}-{found['cpu']} successfully deleted")
|
||||||
return True
|
return True
|
||||||
|
elif found: # these are managed by the system / user. Don't delete these.
|
||||||
|
logger.error(f'Cannot delete requested {engine} {version}. Managed externally.')
|
||||||
else:
|
else:
|
||||||
logger.error(f"Cannot find engine: {engine}-{version}")
|
logger.error(f"Cannot find engine: {engine}-{version}")
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_all_engines(cls):
|
def update_all_engines(cls):
|
||||||
@@ -233,6 +241,13 @@ class EngineManager:
|
|||||||
return worker_class(input_path=input_path, output_path=output_path, engine_path=engine_path, args=args,
|
return worker_class(input_path=input_path, output_path=output_path, engine_path=engine_path, args=args,
|
||||||
parent=parent, name=name)
|
parent=parent, name=name)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def engine_for_project_path(cls, path):
|
||||||
|
name, extension = os.path.splitext(path)
|
||||||
|
for engine in cls.supported_engines():
|
||||||
|
if extension in engine.supported_extensions:
|
||||||
|
return engine
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ class FFMPEGDownloader(EngineDownloader):
|
|||||||
# Download and extract
|
# Download and extract
|
||||||
try:
|
try:
|
||||||
logger.info(f"Requesting download of ffmpeg-{version}-{system_os}-{cpu}")
|
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)
|
||||||
|
|
||||||
# naming cleanup to match existing naming convention
|
# naming cleanup to match existing naming convention
|
||||||
output_path = os.path.join(download_location, f'ffmpeg-{version}-{system_os}-{cpu}')
|
output_path = os.path.join(download_location, f'ffmpeg-{version}-{system_os}-{cpu}')
|
||||||
|
|||||||
65
src/init.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
''' app/init.py '''
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
from .ui.main_window import MainWindow
|
||||||
|
|
||||||
|
from src.api.api_server import start_server
|
||||||
|
from src.utilities.config import Config
|
||||||
|
from src.utilities.misc_helper import system_safe_path
|
||||||
|
|
||||||
|
|
||||||
|
def run() -> int:
|
||||||
|
"""
|
||||||
|
Initializes the application and runs it.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The exit status code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Load Config YAML
|
||||||
|
config_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'config')
|
||||||
|
Config.load_config(system_safe_path(os.path.join(config_dir, 'config.yaml')))
|
||||||
|
|
||||||
|
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(module)s: %(message)s', datefmt='%d-%b-%y %H:%M:%S',
|
||||||
|
level=Config.server_log_level.upper())
|
||||||
|
|
||||||
|
app: QApplication = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# Start server in background
|
||||||
|
background_server = threading.Thread(target=start_server)
|
||||||
|
background_server.daemon = True
|
||||||
|
background_server.start()
|
||||||
|
|
||||||
|
# Setup logging for console ui
|
||||||
|
buffer_handler = BufferingHandler()
|
||||||
|
buffer_handler.setFormatter(logging.getLogger().handlers[0].formatter)
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.addHandler(buffer_handler)
|
||||||
|
|
||||||
|
window: MainWindow = MainWindow()
|
||||||
|
window.buffer_handler = buffer_handler
|
||||||
|
window.show()
|
||||||
|
return sys.exit(app.exec())
|
||||||
|
|
||||||
|
|
||||||
|
class BufferingHandler(logging.Handler, QObject):
|
||||||
|
new_record = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, capacity=100):
|
||||||
|
logging.Handler.__init__(self)
|
||||||
|
QObject.__init__(self)
|
||||||
|
self.buffer = deque(maxlen=capacity) # Define a buffer with a fixed capacity
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
msg = self.format(record)
|
||||||
|
self.buffer.append(msg) # Add message to the buffer
|
||||||
|
self.new_record.emit(msg) # Emit signal
|
||||||
|
|
||||||
|
def get_buffer(self):
|
||||||
|
return list(self.buffer) # Return a copy of the buffer
|
||||||
0
src/ui/__init__.py
Normal file
289
src/ui/add_job.py
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
import os.path
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QSpinBox, QComboBox,
|
||||||
|
QGroupBox, QCheckBox, QProgressBar, QPlainTextEdit
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.api.server_proxy import RenderServerProxy
|
||||||
|
from src.engines.engine_manager import EngineManager
|
||||||
|
from src.utilities.zeroconf_server import ZeroconfServer
|
||||||
|
|
||||||
|
|
||||||
|
class NewRenderJobForm(QWidget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
# UI
|
||||||
|
self.raw_args = None
|
||||||
|
self.submit_progress_label = None
|
||||||
|
self.submit_progress = None
|
||||||
|
self.renderer_type = None
|
||||||
|
self.process_label = None
|
||||||
|
self.process_progress_bar = None
|
||||||
|
self.splitjobs_same_os = None
|
||||||
|
self.enable_splitjobs = None
|
||||||
|
self.server_input = None
|
||||||
|
self.submit_button = None
|
||||||
|
self.notes_input = None
|
||||||
|
self.priority_input = None
|
||||||
|
self.end_frame_input = None
|
||||||
|
self.start_frame_input = None
|
||||||
|
self.output_path_browse_button = None
|
||||||
|
self.output_path_input = None
|
||||||
|
self.scene_file_input = None
|
||||||
|
self.scene_file_browse_button = None
|
||||||
|
self.job_name_input = None
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
self.setWindowTitle("New Job")
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
# Job / Server Data
|
||||||
|
self.server_proxy = RenderServerProxy(socket.gethostname())
|
||||||
|
self.renderer_info = None
|
||||||
|
self.project_info = None
|
||||||
|
|
||||||
|
# get renderer info in bg thread
|
||||||
|
t = threading.Thread(target=self.update_renderer_info)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
# Main Layout
|
||||||
|
main_layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Server Group
|
||||||
|
# Server List
|
||||||
|
server_group = QGroupBox("Server")
|
||||||
|
server_layout = QVBoxLayout(server_group)
|
||||||
|
server_list_layout = QHBoxLayout()
|
||||||
|
server_list_layout.setSpacing(0)
|
||||||
|
self.server_input = QComboBox()
|
||||||
|
server_list_layout.addWidget(QLabel("Hostname:"), 1)
|
||||||
|
server_list_layout.addWidget(self.server_input, 3)
|
||||||
|
server_layout.addLayout(server_list_layout)
|
||||||
|
main_layout.addWidget(server_group)
|
||||||
|
self.update_server_list()
|
||||||
|
# Priority
|
||||||
|
priority_layout = QHBoxLayout()
|
||||||
|
priority_layout.addWidget(QLabel("Priority:"), 1)
|
||||||
|
self.priority_input = QComboBox()
|
||||||
|
self.priority_input.addItems(["High", "Medium", "Low"])
|
||||||
|
self.priority_input.setCurrentIndex(1)
|
||||||
|
priority_layout.addWidget(self.priority_input, 3)
|
||||||
|
server_layout.addLayout(priority_layout)
|
||||||
|
# Splitjobs
|
||||||
|
self.enable_splitjobs = QCheckBox("Automatically split render across multiple servers")
|
||||||
|
self.enable_splitjobs.setEnabled(True)
|
||||||
|
server_layout.addWidget(self.enable_splitjobs)
|
||||||
|
self.splitjobs_same_os = QCheckBox("Only render on same OS")
|
||||||
|
self.splitjobs_same_os.setEnabled(True)
|
||||||
|
server_layout.addWidget(self.splitjobs_same_os)
|
||||||
|
|
||||||
|
# Scene File Group
|
||||||
|
scene_file_group = QGroupBox("Project")
|
||||||
|
scene_file_layout = QVBoxLayout(scene_file_group)
|
||||||
|
scene_file_picker_layout = QHBoxLayout()
|
||||||
|
self.scene_file_input = QLineEdit()
|
||||||
|
self.scene_file_browse_button = QPushButton("Browse...")
|
||||||
|
self.scene_file_browse_button.clicked.connect(self.browse_scene_file)
|
||||||
|
scene_file_picker_layout.addWidget(self.scene_file_input)
|
||||||
|
scene_file_picker_layout.addWidget(self.scene_file_browse_button)
|
||||||
|
scene_file_layout.addLayout(scene_file_picker_layout)
|
||||||
|
# progress bar
|
||||||
|
progress_layout = QHBoxLayout()
|
||||||
|
self.process_progress_bar = QProgressBar()
|
||||||
|
self.process_progress_bar.setMinimum(0)
|
||||||
|
self.process_progress_bar.setMaximum(0)
|
||||||
|
self.process_progress_bar.setHidden(True)
|
||||||
|
self.process_label = QLabel("Processing")
|
||||||
|
self.process_label.setHidden(True)
|
||||||
|
progress_layout.addWidget(self.process_label)
|
||||||
|
progress_layout.addWidget(self.process_progress_bar)
|
||||||
|
scene_file_layout.addLayout(progress_layout)
|
||||||
|
main_layout.addWidget(scene_file_group)
|
||||||
|
|
||||||
|
# Output Settings Group
|
||||||
|
output_settings_group = QGroupBox("Output Settings")
|
||||||
|
output_settings_layout = QVBoxLayout(output_settings_group)
|
||||||
|
frame_range_layout = QHBoxLayout(output_settings_group)
|
||||||
|
self.start_frame_input = QSpinBox()
|
||||||
|
self.start_frame_input.setRange(1, 99999)
|
||||||
|
self.end_frame_input = QSpinBox()
|
||||||
|
self.end_frame_input.setRange(1, 99999)
|
||||||
|
frame_range_layout.addWidget(QLabel("Frames:"))
|
||||||
|
frame_range_layout.addWidget(self.start_frame_input)
|
||||||
|
frame_range_layout.addWidget(QLabel("to"))
|
||||||
|
frame_range_layout.addWidget(self.end_frame_input)
|
||||||
|
output_settings_layout.addLayout(frame_range_layout)
|
||||||
|
# output path
|
||||||
|
output_path_layout = QHBoxLayout()
|
||||||
|
output_path_layout.addWidget(QLabel("Render name:"))
|
||||||
|
self.output_path_input = QLineEdit()
|
||||||
|
# self.output_path_browse_button = QPushButton("Browse...")
|
||||||
|
# self.output_path_browse_button.clicked.connect(self.browse_output_path)
|
||||||
|
output_path_layout.addWidget(self.output_path_input)
|
||||||
|
output_path_layout.addWidget(self.output_path_browse_button)
|
||||||
|
output_settings_layout.addLayout(output_path_layout)
|
||||||
|
main_layout.addWidget(output_settings_group)
|
||||||
|
|
||||||
|
# Renderer Group
|
||||||
|
renderer_group = QGroupBox("Renderer Settings")
|
||||||
|
renderer_layout = QVBoxLayout(renderer_group)
|
||||||
|
self.renderer_type = QComboBox()
|
||||||
|
renderer_layout.addWidget(self.renderer_type)
|
||||||
|
# Raw Args
|
||||||
|
raw_args_layout = QHBoxLayout(renderer_group)
|
||||||
|
raw_args_layout.addWidget(QLabel("Raw Args:"))
|
||||||
|
self.raw_args = QLineEdit()
|
||||||
|
raw_args_layout.addWidget(self.raw_args)
|
||||||
|
args_help_button = QPushButton("?")
|
||||||
|
args_help_button.clicked.connect(self.args_help_button_clicked)
|
||||||
|
raw_args_layout.addWidget(args_help_button)
|
||||||
|
renderer_layout.addLayout(raw_args_layout)
|
||||||
|
main_layout.addWidget(renderer_group)
|
||||||
|
|
||||||
|
# Notes Group
|
||||||
|
notes_group = QGroupBox("Additional Notes")
|
||||||
|
notes_layout = QVBoxLayout(notes_group)
|
||||||
|
self.notes_input = QPlainTextEdit()
|
||||||
|
notes_layout.addWidget(self.notes_input)
|
||||||
|
main_layout.addWidget(notes_group)
|
||||||
|
|
||||||
|
# Submit Button
|
||||||
|
self.submit_button = QPushButton("Submit Job")
|
||||||
|
self.submit_button.clicked.connect(self.submit_job)
|
||||||
|
main_layout.addWidget(self.submit_button)
|
||||||
|
|
||||||
|
self.submit_progress = QProgressBar()
|
||||||
|
self.submit_progress.setMinimum(0)
|
||||||
|
self.submit_progress.setMaximum(0)
|
||||||
|
self.submit_progress.setHidden(True)
|
||||||
|
main_layout.addWidget(self.submit_progress)
|
||||||
|
|
||||||
|
self.submit_progress_label = QLabel("Submitting...")
|
||||||
|
self.submit_progress_label.setHidden(True)
|
||||||
|
main_layout.addWidget(self.submit_progress_label)
|
||||||
|
|
||||||
|
self.toggle_renderer_enablement(False)
|
||||||
|
|
||||||
|
def update_renderer_info(self):
|
||||||
|
self.renderer_info = self.server_proxy.get_renderer_info()
|
||||||
|
self.renderer_type.addItems(self.renderer_info.keys())
|
||||||
|
|
||||||
|
def update_server_list(self):
|
||||||
|
clients = ZeroconfServer.found_clients()
|
||||||
|
self.server_input.clear()
|
||||||
|
self.server_input.addItems(clients)
|
||||||
|
|
||||||
|
def browse_scene_file(self):
|
||||||
|
|
||||||
|
def get_project_info():
|
||||||
|
self.process_progress_bar.setHidden(False)
|
||||||
|
self.process_label.setHidden(False)
|
||||||
|
self.toggle_renderer_enablement(False)
|
||||||
|
output_name, _ = os.path.splitext(os.path.basename(file_name))
|
||||||
|
self.output_path_input.setText(output_name)
|
||||||
|
|
||||||
|
engine = EngineManager.engine_for_project_path(file_name)
|
||||||
|
self.project_info = engine().get_scene_info(file_name)
|
||||||
|
|
||||||
|
index = self.renderer_type.findText(engine.name().lower())
|
||||||
|
if index >= 0:
|
||||||
|
self.renderer_type.setCurrentIndex(index)
|
||||||
|
|
||||||
|
self.update_project_ui()
|
||||||
|
|
||||||
|
self.process_progress_bar.setHidden(True)
|
||||||
|
self.process_label.setHidden(True)
|
||||||
|
self.toggle_renderer_enablement(True)
|
||||||
|
|
||||||
|
file_name, _ = QFileDialog.getOpenFileName(self, "Select Scene File")
|
||||||
|
if file_name:
|
||||||
|
self.scene_file_input.setText(file_name)
|
||||||
|
# analyze the file
|
||||||
|
update_thread = threading.Thread(target=get_project_info)
|
||||||
|
update_thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
def browse_output_path(self):
|
||||||
|
directory = QFileDialog.getExistingDirectory(self, "Select Output Directory")
|
||||||
|
if directory:
|
||||||
|
self.output_path_input.setText(directory)
|
||||||
|
|
||||||
|
def args_help_button_clicked(self):
|
||||||
|
#todo: create a popup window showing args
|
||||||
|
pass
|
||||||
|
|
||||||
|
# -------- Update --------
|
||||||
|
|
||||||
|
def update_project_ui(self):
|
||||||
|
self.start_frame_input.setValue(self.project_info.get('frame_start'))
|
||||||
|
self.end_frame_input.setValue(self.project_info.get('frame_end'))
|
||||||
|
|
||||||
|
def toggle_renderer_enablement(self, enabled=False):
|
||||||
|
self.start_frame_input.setEnabled(enabled)
|
||||||
|
self.end_frame_input.setEnabled(enabled)
|
||||||
|
self.notes_input.setEnabled(enabled)
|
||||||
|
self.output_path_input.setEnabled(enabled)
|
||||||
|
self.submit_button.setEnabled(enabled)
|
||||||
|
|
||||||
|
# -------- Submit Job Calls --------
|
||||||
|
|
||||||
|
def submit_job(self):
|
||||||
|
def submit_job_worker():
|
||||||
|
def create_callback(encoder):
|
||||||
|
encoder_len = encoder.len
|
||||||
|
def callback(monitor):
|
||||||
|
percent = f"{monitor.bytes_read / encoder_len * 100:.0f}"
|
||||||
|
self.submit_progress_label.setText(f"Transferring to {hostname} - {percent}%")
|
||||||
|
self.submit_progress.setMaximum(100)
|
||||||
|
self.submit_progress.setValue(int(percent))
|
||||||
|
|
||||||
|
return callback
|
||||||
|
|
||||||
|
self.submit_progress.setHidden(False)
|
||||||
|
self.submit_progress_label.setHidden(False)
|
||||||
|
self.submit_button.setHidden(True)
|
||||||
|
|
||||||
|
hostname = self.server_input.currentText()
|
||||||
|
job_json = {'owner': psutil.Process().username() + '@' + socket.gethostname(),
|
||||||
|
'renderer': self.renderer_type.currentText().lower(),
|
||||||
|
# 'input_path': self.scene_file_input.text(),
|
||||||
|
# 'output_path': os.path.join(os.path.dirname(self.chosen_file), self.output_entry.get()),
|
||||||
|
'args': {'raw': self.raw_args.text()},
|
||||||
|
'output_path': self.output_path_input.text(),
|
||||||
|
'start_frame': self.start_frame_input.value(),
|
||||||
|
'end_frame': self.end_frame_input.value(),
|
||||||
|
'priority': self.priority_input.currentIndex() + 1,
|
||||||
|
'notes': self.notes_input.toPlainText(),
|
||||||
|
'enable_split_jobs': self.enable_splitjobs.isChecked()}
|
||||||
|
|
||||||
|
input_path = self.scene_file_input.text()
|
||||||
|
job_list = [job_json]
|
||||||
|
self.submit_progress.setMaximum(0)
|
||||||
|
result = self.server_proxy.post_job_to_server(file_path=input_path, job_list=job_list,
|
||||||
|
callback=create_callback)
|
||||||
|
self.submit_progress.setMaximum(0)
|
||||||
|
|
||||||
|
print(result.json())
|
||||||
|
self.submit_button.setHidden(False)
|
||||||
|
self.submit_progress.setHidden(True)
|
||||||
|
self.submit_progress_label.setHidden(True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# submit thread
|
||||||
|
worker_thread = threading.Thread(target=submit_job_worker)
|
||||||
|
worker_thread.start()
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app = QApplication([])
|
||||||
|
window = NewRenderJobForm()
|
||||||
|
app.exec()
|
||||||
60
src/ui/console.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from PyQt6.QtGui import QFont
|
||||||
|
from PyQt6.QtWidgets import QMainWindow, QVBoxLayout, QWidget, QPlainTextEdit
|
||||||
|
from PyQt6.QtCore import pyqtSignal, QObject
|
||||||
|
|
||||||
|
|
||||||
|
# Create a custom logging handler that emits a signal
|
||||||
|
class QSignalHandler(logging.Handler, QObject):
|
||||||
|
new_record = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
logging.Handler.__init__(self)
|
||||||
|
QObject.__init__(self)
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
msg = self.format(record)
|
||||||
|
self.new_record.emit(msg) # Emit signal
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleWindow(QMainWindow):
|
||||||
|
def __init__(self, buffer_handler):
|
||||||
|
super().__init__()
|
||||||
|
self.buffer_handler = buffer_handler
|
||||||
|
self.log_handler = None
|
||||||
|
self.init_ui()
|
||||||
|
self.init_logging()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
self.setGeometry(100, 100, 600, 800)
|
||||||
|
self.setWindowTitle("Log Output")
|
||||||
|
|
||||||
|
self.text_edit = QPlainTextEdit(self)
|
||||||
|
self.text_edit.setReadOnly(True)
|
||||||
|
self.text_edit.setFont(QFont("Courier", 10))
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.addWidget(self.text_edit)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
central_widget = QWidget()
|
||||||
|
central_widget.setLayout(layout)
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
|
||||||
|
def init_logging(self):
|
||||||
|
|
||||||
|
self.buffer_handler.new_record.connect(self.append_log_record)
|
||||||
|
# Display all messages that were buffered before the window was opened
|
||||||
|
for record in self.buffer_handler.get_buffer():
|
||||||
|
self.text_edit.appendPlainText(record)
|
||||||
|
|
||||||
|
self.log_handler = QSignalHandler()
|
||||||
|
# self.log_handler.new_record.connect(self.append_log_record)
|
||||||
|
self.log_handler.setFormatter(self.buffer_handler.formatter)
|
||||||
|
logging.getLogger().addHandler(self.log_handler)
|
||||||
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
|
|
||||||
|
def append_log_record(self, record):
|
||||||
|
self.text_edit.appendPlainText(record)
|
||||||
159
src/ui/engine_browser.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QMainWindow, QWidget, QVBoxLayout, QPushButton, QTableWidget, QTableWidgetItem, QHBoxLayout, QAbstractItemView,
|
||||||
|
QHeaderView, QProgressBar, QLabel, QMessageBox
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.api.server_proxy import RenderServerProxy
|
||||||
|
from src.engines.engine_manager import EngineManager
|
||||||
|
|
||||||
|
|
||||||
|
class EngineBrowserWindow(QMainWindow):
|
||||||
|
def __init__(self, hostname=None):
|
||||||
|
super().__init__()
|
||||||
|
self.delete_button = None
|
||||||
|
self.install_button = None
|
||||||
|
self.progress_label = None
|
||||||
|
self.progress_bar = None
|
||||||
|
self.table_widget = None
|
||||||
|
self.launch_button = None
|
||||||
|
self.hostname = hostname or socket.gethostname()
|
||||||
|
self.setWindowTitle(f'Engine Browser ({self.hostname})')
|
||||||
|
self.setGeometry(100, 100, 500, 300)
|
||||||
|
self.engine_data = []
|
||||||
|
self.initUI()
|
||||||
|
|
||||||
|
def initUI(self):
|
||||||
|
# Central widget
|
||||||
|
central_widget = QWidget()
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
|
||||||
|
# Layout
|
||||||
|
layout = QVBoxLayout(central_widget)
|
||||||
|
|
||||||
|
# Table
|
||||||
|
self.table_widget = QTableWidget(0, 4)
|
||||||
|
self.table_widget.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
|
self.table_widget.verticalHeader().setVisible(False)
|
||||||
|
self.table_widget.itemSelectionChanged.connect(self.engine_picked)
|
||||||
|
self.table_widget.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
|
layout.addWidget(self.table_widget)
|
||||||
|
self.update_table()
|
||||||
|
|
||||||
|
# Progress Bar Layout
|
||||||
|
self.progress_bar = QProgressBar()
|
||||||
|
self.progress_bar.setMinimum(0)
|
||||||
|
self.progress_bar.setMaximum(0)
|
||||||
|
# self.progress_bar.setHidden(True)
|
||||||
|
layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
|
# Progress Bar Label
|
||||||
|
self.progress_label = QLabel('Downloading blah blah')
|
||||||
|
layout.addWidget(self.progress_label)
|
||||||
|
|
||||||
|
# Buttons Layout
|
||||||
|
buttons_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
# Install Button
|
||||||
|
self.install_button = QPushButton('Install')
|
||||||
|
self.install_button.clicked.connect(self.install_button_click) # Connect to slot
|
||||||
|
# buttons_layout.addWidget(self.install_button)
|
||||||
|
|
||||||
|
# Launch Button
|
||||||
|
self.launch_button = QPushButton('Launch')
|
||||||
|
self.launch_button.clicked.connect(self.launch_button_click) # Connect to slot
|
||||||
|
self.launch_button.setEnabled(False)
|
||||||
|
buttons_layout.addWidget(self.launch_button)
|
||||||
|
|
||||||
|
#Delete Button
|
||||||
|
self.delete_button = QPushButton('Delete')
|
||||||
|
self.delete_button.clicked.connect(self.delete_button_click) # Connect to slot
|
||||||
|
self.delete_button.setEnabled(False)
|
||||||
|
buttons_layout.addWidget(self.delete_button)
|
||||||
|
|
||||||
|
# Add Buttons Layout to the Main Layout
|
||||||
|
layout.addLayout(buttons_layout)
|
||||||
|
|
||||||
|
self.update_download_status()
|
||||||
|
|
||||||
|
def update_table(self):
|
||||||
|
|
||||||
|
def update_table_worker():
|
||||||
|
raw_server_data = RenderServerProxy(self.hostname).get_renderer_info(simple=True)
|
||||||
|
if not raw_server_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
table_data = [] # convert the data into a flat list
|
||||||
|
for engine_name, engine_data in raw_server_data.items():
|
||||||
|
table_data.extend(engine_data['versions'])
|
||||||
|
self.engine_data = table_data
|
||||||
|
|
||||||
|
self.table_widget.setRowCount(len(self.engine_data))
|
||||||
|
self.table_widget.setColumnCount(4)
|
||||||
|
|
||||||
|
for row, engine in enumerate(self.engine_data):
|
||||||
|
self.table_widget.setItem(row, 0, QTableWidgetItem(engine['engine']))
|
||||||
|
self.table_widget.setItem(row, 1, QTableWidgetItem(engine['version']))
|
||||||
|
self.table_widget.setItem(row, 2, QTableWidgetItem(engine['type']))
|
||||||
|
self.table_widget.setItem(row, 3, QTableWidgetItem(engine['path']))
|
||||||
|
|
||||||
|
self.table_widget.selectRow(0)
|
||||||
|
|
||||||
|
self.table_widget.clear()
|
||||||
|
self.table_widget.setHorizontalHeaderLabels(['Engine', 'Version', 'Type', 'Path'])
|
||||||
|
self.table_widget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.table_widget.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.table_widget.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.table_widget.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
|
||||||
|
update_thread = threading.Thread(target=update_table_worker,)
|
||||||
|
update_thread.start()
|
||||||
|
|
||||||
|
def engine_picked(self):
|
||||||
|
engine_info = self.engine_data[self.table_widget.currentRow()]
|
||||||
|
self.delete_button.setEnabled(engine_info['type'] == 'managed')
|
||||||
|
self.launch_button.setEnabled(self.hostname == socket.gethostname())
|
||||||
|
|
||||||
|
def update_download_status(self):
|
||||||
|
hide_progress = not bool(EngineManager.download_tasks)
|
||||||
|
self.progress_bar.setHidden(hide_progress)
|
||||||
|
self.progress_label.setHidden(hide_progress)
|
||||||
|
|
||||||
|
# todo: update progress bar with status
|
||||||
|
self.progress_label.setText(f"Downloading {len(EngineManager.download_tasks)} engines")
|
||||||
|
|
||||||
|
def launch_button_click(self):
|
||||||
|
engine_info = self.engine_data[self.table_widget.currentRow()]
|
||||||
|
path = engine_info['path']
|
||||||
|
if sys.platform.startswith('darwin'):
|
||||||
|
subprocess.run(['open', path])
|
||||||
|
elif sys.platform.startswith('win32'):
|
||||||
|
os.startfile(path)
|
||||||
|
elif sys.platform.startswith('linux'):
|
||||||
|
subprocess.run(['xdg-open', path])
|
||||||
|
else:
|
||||||
|
raise OSError("Unsupported operating system")
|
||||||
|
|
||||||
|
def install_button_click(self):
|
||||||
|
self.update_download_status()
|
||||||
|
|
||||||
|
def delete_button_click(self):
|
||||||
|
engine_info = self.engine_data[self.table_widget.currentRow()]
|
||||||
|
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
|
||||||
|
|
||||||
|
result = RenderServerProxy(self.hostname).delete_engine(engine_info['engine'], engine_info['version'])
|
||||||
|
if result.ok:
|
||||||
|
self.update_table()
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(self, f"Delete {engine_info['engine']} {engine_info['version']} Failed",
|
||||||
|
f"Failed to delete {engine_info['engine']} {engine_info['version']}.",
|
||||||
|
QMessageBox.StandardButton.Ok)
|
||||||
30
src/ui/log_viewer.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import requests
|
||||||
|
from PyQt6.QtGui import QFont
|
||||||
|
from PyQt6.QtWidgets import QMainWindow, QVBoxLayout, QWidget, QPlainTextEdit
|
||||||
|
|
||||||
|
|
||||||
|
class LogViewer(QMainWindow):
|
||||||
|
def __init__(self, log_path):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.log_path = log_path
|
||||||
|
self.setGeometry(100, 100, 600, 800)
|
||||||
|
self.setWindowTitle("Log Output")
|
||||||
|
|
||||||
|
self.text_edit = QPlainTextEdit(self)
|
||||||
|
self.text_edit.setReadOnly(True)
|
||||||
|
self.text_edit.setFont(QFont("Courier", 10))
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.addWidget(self.text_edit)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
central_widget = QWidget()
|
||||||
|
central_widget.setLayout(layout)
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
|
||||||
|
self.fetch_logs()
|
||||||
|
|
||||||
|
def fetch_logs(self):
|
||||||
|
result = requests.get(self.log_path)
|
||||||
|
self.text_edit.setPlainText(result.text)
|
||||||
562
src/ui/main_window.py
Normal file
@@ -0,0 +1,562 @@
|
|||||||
|
''' app/ui/main_window.py '''
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
from PyQt6.QtCore import Qt, QByteArray, QBuffer, QIODevice, QThread
|
||||||
|
from PyQt6.QtGui import QPixmap, QImage, QFont, QIcon
|
||||||
|
from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QListWidget, QTableWidget, QAbstractItemView, \
|
||||||
|
QTableWidgetItem, QLabel, QVBoxLayout, QHeaderView, QMessageBox, QGroupBox, QPushButton, QListWidgetItem
|
||||||
|
|
||||||
|
from src.api.server_proxy import RenderServerProxy
|
||||||
|
from src.render_queue import RenderQueue
|
||||||
|
from src.utilities.misc_helper import get_time_elapsed, resources_dir
|
||||||
|
from src.utilities.status_utils import RenderStatus
|
||||||
|
from src.utilities.zeroconf_server import ZeroconfServer
|
||||||
|
from .add_job import NewRenderJobForm
|
||||||
|
from .console import ConsoleWindow
|
||||||
|
from .engine_browser import EngineBrowserWindow
|
||||||
|
from .log_viewer import LogViewer
|
||||||
|
from .widgets.menubar import MenuBar
|
||||||
|
from .widgets.proportional_image_label import ProportionalImageLabel
|
||||||
|
from .widgets.statusbar import StatusBar
|
||||||
|
from .widgets.toolbar import ToolBar
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
"""
|
||||||
|
MainWindow
|
||||||
|
|
||||||
|
Args:
|
||||||
|
QMainWindow (QMainWindow): Inheritance
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""
|
||||||
|
Initialize the Main-Window.
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
# Load the queue
|
||||||
|
self.engine_browser_window = None
|
||||||
|
self.server_info_group = None
|
||||||
|
self.server_proxies = {}
|
||||||
|
self.current_hostname = None
|
||||||
|
self.subprocess_runner = None
|
||||||
|
|
||||||
|
# To pass to console
|
||||||
|
self.buffer_handler = None
|
||||||
|
|
||||||
|
# Window-Settings
|
||||||
|
self.setWindowTitle("Zordon")
|
||||||
|
self.setGeometry(100, 100, 900, 800)
|
||||||
|
central_widget = QWidget(self)
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
|
||||||
|
main_layout = QHBoxLayout(central_widget)
|
||||||
|
|
||||||
|
# Create a QLabel widget to display the image
|
||||||
|
self.image_label = ProportionalImageLabel()
|
||||||
|
self.image_label.setMaximumSize(700, 500)
|
||||||
|
self.image_label.setFixedHeight(500)
|
||||||
|
self.image_label.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter)
|
||||||
|
self.load_image_path(os.path.join(resources_dir(), 'Rectangle.png'))
|
||||||
|
|
||||||
|
# Server list
|
||||||
|
self.server_list_view = QListWidget()
|
||||||
|
self.server_list_view.itemClicked.connect(self.server_picked)
|
||||||
|
list_font = QFont()
|
||||||
|
list_font.setPointSize(16)
|
||||||
|
self.server_list_view.setFont(list_font)
|
||||||
|
self.added_hostnames = []
|
||||||
|
|
||||||
|
self.setup_ui(main_layout)
|
||||||
|
|
||||||
|
self.create_toolbars()
|
||||||
|
|
||||||
|
# Add Widgets to Window
|
||||||
|
self.setMenuBar(MenuBar(self))
|
||||||
|
self.setStatusBar(StatusBar(self))
|
||||||
|
|
||||||
|
# start background update
|
||||||
|
self.bg_update_thread = QThread()
|
||||||
|
self.bg_update_thread.run = self.__background_update
|
||||||
|
self.bg_update_thread.start()
|
||||||
|
|
||||||
|
# Setup other windows
|
||||||
|
self.new_job_window = None
|
||||||
|
self.console_window = None
|
||||||
|
self.log_viewer_window = None
|
||||||
|
|
||||||
|
# Pick default job
|
||||||
|
self.job_picked()
|
||||||
|
|
||||||
|
def setup_ui(self, main_layout):
|
||||||
|
|
||||||
|
# Servers
|
||||||
|
server_list_group = QGroupBox("Available Servers")
|
||||||
|
list_layout = QVBoxLayout()
|
||||||
|
list_layout.addWidget(self.server_list_view)
|
||||||
|
list_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
server_list_group.setLayout(list_layout)
|
||||||
|
server_info_group = QGroupBox("Server Info")
|
||||||
|
|
||||||
|
# Server Info Group
|
||||||
|
self.server_info_hostname = QLabel()
|
||||||
|
self.server_info_os = QLabel()
|
||||||
|
self.server_info_cpu = QLabel()
|
||||||
|
self.server_info_ram = QLabel()
|
||||||
|
server_info_engines_button = QPushButton("Render Engines")
|
||||||
|
server_info_engines_button.clicked.connect(self.engine_browser)
|
||||||
|
server_info_layout = QVBoxLayout()
|
||||||
|
server_info_layout.addWidget(self.server_info_hostname)
|
||||||
|
server_info_layout.addWidget(self.server_info_os)
|
||||||
|
server_info_layout.addWidget(self.server_info_cpu)
|
||||||
|
server_info_layout.addWidget(self.server_info_ram)
|
||||||
|
server_info_layout.addWidget(server_info_engines_button)
|
||||||
|
server_info_group.setLayout(server_info_layout)
|
||||||
|
|
||||||
|
# Server Button Layout
|
||||||
|
server_button_layout = QHBoxLayout()
|
||||||
|
add_server_button = QPushButton(text="+")
|
||||||
|
remove_server_button = QPushButton(text="-")
|
||||||
|
server_button_layout.addWidget(add_server_button)
|
||||||
|
server_button_layout.addWidget(remove_server_button)
|
||||||
|
|
||||||
|
# Layouts
|
||||||
|
info_layout = QVBoxLayout()
|
||||||
|
info_layout.addWidget(server_list_group, stretch=True)
|
||||||
|
info_layout.addWidget(server_info_group)
|
||||||
|
info_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
server_list_group.setFixedWidth(260)
|
||||||
|
self.server_picked()
|
||||||
|
|
||||||
|
# Job list
|
||||||
|
self.job_list_view = QTableWidget()
|
||||||
|
self.job_list_view.setRowCount(0)
|
||||||
|
self.job_list_view.setColumnCount(8)
|
||||||
|
self.job_list_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
|
self.job_list_view.verticalHeader().setVisible(False)
|
||||||
|
self.job_list_view.itemSelectionChanged.connect(self.job_picked)
|
||||||
|
self.job_list_view.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
|
self.refresh_job_headers()
|
||||||
|
|
||||||
|
# Image Layout
|
||||||
|
image_group = QGroupBox("Job Preview")
|
||||||
|
image_layout = QVBoxLayout(image_group)
|
||||||
|
image_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
image_center_layout = QHBoxLayout()
|
||||||
|
image_center_layout.addWidget(self.image_label)
|
||||||
|
image_layout.addWidget(self.image_label)
|
||||||
|
# image_layout.addLayout(image_center_layout)
|
||||||
|
|
||||||
|
# Job Layout
|
||||||
|
job_list_group = QGroupBox("Render Jobs")
|
||||||
|
job_list_layout = QVBoxLayout(job_list_group)
|
||||||
|
job_list_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
image_layout.addWidget(self.job_list_view, stretch=True)
|
||||||
|
image_layout.addLayout(job_list_layout)
|
||||||
|
|
||||||
|
# Add them all to the window
|
||||||
|
main_layout.addLayout(info_layout)
|
||||||
|
|
||||||
|
right_layout = QVBoxLayout()
|
||||||
|
right_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
right_layout.addWidget(image_group)
|
||||||
|
# right_layout.addWidget(job_list_group)
|
||||||
|
main_layout.addLayout(right_layout)
|
||||||
|
|
||||||
|
def __background_update(self):
|
||||||
|
while True:
|
||||||
|
self.update_servers()
|
||||||
|
# self.fetch_jobs()
|
||||||
|
# todo: fix job updates - issues with threading
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
running_jobs = len(RenderQueue.running_jobs())
|
||||||
|
if running_jobs:
|
||||||
|
reply = QMessageBox.question(self, "Running Jobs",
|
||||||
|
f"You have {running_jobs} jobs running.\n"
|
||||||
|
f"Quitting will cancel these renders. Continue?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
|
||||||
|
QMessageBox.StandardButton.Cancel)
|
||||||
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
|
event.accept()
|
||||||
|
else:
|
||||||
|
event.ignore()
|
||||||
|
|
||||||
|
# -- Server Code -- #
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_server_proxy(self):
|
||||||
|
return self.server_proxies.get(self.current_hostname, None)
|
||||||
|
|
||||||
|
def server_picked(self):
|
||||||
|
"""Update the table and Server Info box when a server is changed"""
|
||||||
|
try:
|
||||||
|
new_hostname = self.server_list_view.currentItem().text()
|
||||||
|
if new_hostname != self.current_hostname:
|
||||||
|
self.current_hostname = new_hostname
|
||||||
|
self.job_list_view.setRowCount(0)
|
||||||
|
self.fetch_jobs(clear_table=True)
|
||||||
|
|
||||||
|
if self.job_list_view.rowCount():
|
||||||
|
self.job_list_view.selectRow(0)
|
||||||
|
|
||||||
|
# Update the Server Info box when a server is changed
|
||||||
|
self.server_info_hostname.setText(self.current_hostname or "unknown")
|
||||||
|
if self.current_server_proxy.system_os:
|
||||||
|
self.server_info_os.setText(f"OS: {self.current_server_proxy.system_os} "
|
||||||
|
f"{self.current_server_proxy.system_os_version}")
|
||||||
|
self.server_info_cpu.setText(f"CPU: {self.current_server_proxy.system_cpu} - "
|
||||||
|
f"{self.current_server_proxy.system_cpu_count} cores")
|
||||||
|
else:
|
||||||
|
self.server_info_os.setText(f"OS: Loading...")
|
||||||
|
self.server_info_cpu.setText(f"CPU: Loading...")
|
||||||
|
|
||||||
|
def update_server_info_worker():
|
||||||
|
server_details = self.current_server_proxy.get_status()
|
||||||
|
if server_details['hostname'] == self.current_hostname:
|
||||||
|
self.server_info_os.setText(f"OS: {server_details.get('system_os')} "
|
||||||
|
f"{server_details.get('system_os_version')}")
|
||||||
|
self.server_info_cpu.setText(f"CPU: {server_details.get('system_cpu')} - "
|
||||||
|
f"{server_details.get('cpu_count')} cores")
|
||||||
|
|
||||||
|
update_thread = threading.Thread(target=update_server_info_worker)
|
||||||
|
update_thread.start()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fetch_jobs(self, clear_table=False):
|
||||||
|
|
||||||
|
if not self.current_server_proxy:
|
||||||
|
return
|
||||||
|
|
||||||
|
if clear_table:
|
||||||
|
self.job_list_view.clear()
|
||||||
|
self.refresh_job_headers()
|
||||||
|
|
||||||
|
job_fetch = self.current_server_proxy.get_all_jobs(ignore_token=clear_table)
|
||||||
|
if job_fetch:
|
||||||
|
num_jobs = len(job_fetch)
|
||||||
|
self.job_list_view.setRowCount(num_jobs)
|
||||||
|
|
||||||
|
for row, job in enumerate(job_fetch):
|
||||||
|
|
||||||
|
display_status = job['status'] if job['status'] != RenderStatus.RUNNING.value else \
|
||||||
|
('%.0f%%' % (job['percent_complete'] * 100)) # if running, show percent, otherwise just show status
|
||||||
|
tags = (job['status'],)
|
||||||
|
start_time = datetime.datetime.fromisoformat(job['start_time']) if job['start_time'] else None
|
||||||
|
end_time = datetime.datetime.fromisoformat(job['end_time']) if job['end_time'] else None
|
||||||
|
|
||||||
|
time_elapsed = "" if (job['status'] != RenderStatus.RUNNING.value and not end_time) else \
|
||||||
|
get_time_elapsed(start_time, end_time)
|
||||||
|
|
||||||
|
name = job.get('name') or os.path.basename(job.get('input_path', ''))
|
||||||
|
renderer = f"{job.get('renderer', '')}-{job.get('renderer_version')}"
|
||||||
|
priority = str(job.get('priority', ''))
|
||||||
|
total_frames = str(job.get('total_frames', ''))
|
||||||
|
|
||||||
|
items = [QTableWidgetItem(job['id']), QTableWidgetItem(name), QTableWidgetItem(renderer),
|
||||||
|
QTableWidgetItem(priority), QTableWidgetItem(display_status), QTableWidgetItem(time_elapsed),
|
||||||
|
QTableWidgetItem(total_frames), QTableWidgetItem(job['date_created'])]
|
||||||
|
|
||||||
|
for col, item in enumerate(items):
|
||||||
|
self.job_list_view.setItem(row, col, item)
|
||||||
|
|
||||||
|
# -- Job Code -- #
|
||||||
|
def job_picked(self):
|
||||||
|
|
||||||
|
def fetch_preview(job_id):
|
||||||
|
try:
|
||||||
|
before_fetch_hostname = self.current_server_proxy.hostname
|
||||||
|
response = self.current_server_proxy.request(f'job/{job_id}/thumbnail?size=big')
|
||||||
|
if response.ok:
|
||||||
|
import io
|
||||||
|
image_data = response.content
|
||||||
|
image = Image.open(io.BytesIO(image_data))
|
||||||
|
if self.current_server_proxy.hostname == before_fetch_hostname and job_id == \
|
||||||
|
self.selected_job_ids()[0]:
|
||||||
|
self.load_image_data(image)
|
||||||
|
except ConnectionError as e:
|
||||||
|
logger.error(f"Connection error fetching image: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching image: {e}")
|
||||||
|
|
||||||
|
job_id = self.selected_job_ids()[0] if self.selected_job_ids() else None
|
||||||
|
local_server = self.current_hostname == socket.gethostname()
|
||||||
|
|
||||||
|
if job_id:
|
||||||
|
fetch_thread = threading.Thread(target=fetch_preview, args=(job_id,))
|
||||||
|
fetch_thread.daemon = True
|
||||||
|
fetch_thread.start()
|
||||||
|
|
||||||
|
selected_row = self.job_list_view.selectionModel().selectedRows()[0]
|
||||||
|
current_status = self.job_list_view.item(selected_row.row(), 4).text()
|
||||||
|
|
||||||
|
# show / hide the stop button
|
||||||
|
show_stop_button = current_status.lower() == 'running'
|
||||||
|
self.topbar.actions_call['Stop Job'].setEnabled(show_stop_button)
|
||||||
|
self.topbar.actions_call['Stop Job'].setVisible(show_stop_button)
|
||||||
|
self.topbar.actions_call['Delete Job'].setEnabled(not show_stop_button)
|
||||||
|
self.topbar.actions_call['Delete Job'].setVisible(not show_stop_button)
|
||||||
|
|
||||||
|
self.topbar.actions_call['Render Log'].setEnabled(True)
|
||||||
|
self.topbar.actions_call['Download'].setEnabled(not local_server)
|
||||||
|
self.topbar.actions_call['Download'].setVisible(not local_server)
|
||||||
|
self.topbar.actions_call['Open Files'].setEnabled(local_server)
|
||||||
|
self.topbar.actions_call['Open Files'].setVisible(local_server)
|
||||||
|
else:
|
||||||
|
# load default
|
||||||
|
default_image_path = os.path.join(resources_dir(), 'Rectangle.png')
|
||||||
|
self.load_image_path(default_image_path)
|
||||||
|
self.topbar.actions_call['Stop Job'].setVisible(False)
|
||||||
|
self.topbar.actions_call['Stop Job'].setEnabled(False)
|
||||||
|
self.topbar.actions_call['Delete Job'].setEnabled(False)
|
||||||
|
self.topbar.actions_call['Render Log'].setEnabled(False)
|
||||||
|
self.topbar.actions_call['Download'].setEnabled(False)
|
||||||
|
self.topbar.actions_call['Download'].setVisible(True)
|
||||||
|
self.topbar.actions_call['Open Files'].setEnabled(False)
|
||||||
|
self.topbar.actions_call['Open Files'].setVisible(False)
|
||||||
|
|
||||||
|
def selected_job_ids(self):
|
||||||
|
selected_rows = self.job_list_view.selectionModel().selectedRows()
|
||||||
|
job_ids = []
|
||||||
|
for selected_row in selected_rows:
|
||||||
|
id_item = self.job_list_view.item(selected_row.row(), 0)
|
||||||
|
job_ids.append(id_item.text())
|
||||||
|
return job_ids
|
||||||
|
|
||||||
|
def refresh_job_headers(self):
|
||||||
|
self.job_list_view.setHorizontalHeaderLabels(["ID", "Name", "Renderer", "Priority", "Status",
|
||||||
|
"Time Elapsed", "Frames", "Date Created"])
|
||||||
|
self.job_list_view.setColumnHidden(0, True)
|
||||||
|
|
||||||
|
self.job_list_view.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||||
|
self.job_list_view.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
self.job_list_view.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
self.job_list_view.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
self.job_list_view.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
self.job_list_view.horizontalHeader().setSectionResizeMode(6, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
self.job_list_view.horizontalHeader().setSectionResizeMode(7, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
|
||||||
|
# -- Image Code -- #
|
||||||
|
|
||||||
|
def load_image_path(self, image_path):
|
||||||
|
# Load and set the image using QPixmap
|
||||||
|
pixmap = QPixmap(image_path)
|
||||||
|
if not pixmap:
|
||||||
|
logger.error("Error loading image")
|
||||||
|
return
|
||||||
|
self.image_label.setPixmap(pixmap)
|
||||||
|
|
||||||
|
def load_image_data(self, pillow_image):
|
||||||
|
# Convert the Pillow Image to a QByteArray (byte buffer)
|
||||||
|
byte_array = QByteArray()
|
||||||
|
buffer = QBuffer(byte_array)
|
||||||
|
buffer.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||||
|
pillow_image.save(buffer, "PNG")
|
||||||
|
buffer.close()
|
||||||
|
|
||||||
|
# Create a QImage from the QByteArray
|
||||||
|
image = QImage.fromData(byte_array)
|
||||||
|
|
||||||
|
# Create a QPixmap from the QImage
|
||||||
|
pixmap = QPixmap.fromImage(image)
|
||||||
|
|
||||||
|
if not pixmap:
|
||||||
|
logger.error("Error loading image")
|
||||||
|
return
|
||||||
|
self.image_label.setPixmap(pixmap)
|
||||||
|
|
||||||
|
def update_servers(self):
|
||||||
|
found_servers = list(set(ZeroconfServer.found_clients() + self.added_hostnames))
|
||||||
|
# Always make sure local hostname is first
|
||||||
|
current_hostname = socket.gethostname()
|
||||||
|
if found_servers and found_servers[0] != current_hostname:
|
||||||
|
if current_hostname in found_servers:
|
||||||
|
found_servers.remove(current_hostname)
|
||||||
|
found_servers.insert(0, current_hostname)
|
||||||
|
|
||||||
|
old_count = self.server_list_view.count()
|
||||||
|
|
||||||
|
# Update proxys
|
||||||
|
for hostname in found_servers:
|
||||||
|
if not self.server_proxies.get(hostname, None):
|
||||||
|
new_proxy = RenderServerProxy(hostname=hostname)
|
||||||
|
new_proxy.start_background_update()
|
||||||
|
self.server_proxies[hostname] = new_proxy
|
||||||
|
|
||||||
|
# Add in all the missing servers
|
||||||
|
current_server_list = []
|
||||||
|
for i in range(self.server_list_view.count()):
|
||||||
|
current_server_list.append(self.server_list_view.item(i).text())
|
||||||
|
for hostname in found_servers:
|
||||||
|
if hostname not in current_server_list:
|
||||||
|
image_path = os.path.join(resources_dir(), 'icons', 'Monitor.png')
|
||||||
|
list_widget = QListWidgetItem(QIcon(image_path), hostname)
|
||||||
|
self.server_list_view.addItem(list_widget)
|
||||||
|
|
||||||
|
# find any servers that shouldn't be shown any longer
|
||||||
|
servers_to_remove = []
|
||||||
|
for i in range(self.server_list_view.count()):
|
||||||
|
name = self.server_list_view.item(i).text()
|
||||||
|
if name not in found_servers:
|
||||||
|
servers_to_remove.append(name)
|
||||||
|
|
||||||
|
# remove any servers that shouldn't be shown any longer
|
||||||
|
for server in servers_to_remove:
|
||||||
|
# Find and remove the item with the specified text
|
||||||
|
for i in range(self.server_list_view.count()):
|
||||||
|
item = self.server_list_view.item(i)
|
||||||
|
if item is not None and item.text() == server:
|
||||||
|
self.server_list_view.takeItem(i)
|
||||||
|
break # Stop searching after the first match is found
|
||||||
|
|
||||||
|
if not old_count and self.server_list_view.count():
|
||||||
|
self.server_list_view.setCurrentRow(0)
|
||||||
|
self.server_picked()
|
||||||
|
|
||||||
|
def create_toolbars(self) -> None:
|
||||||
|
"""
|
||||||
|
Creates and adds the top and right toolbars to the main window.
|
||||||
|
"""
|
||||||
|
# Top Toolbar [PyQt6.QtWidgets.QToolBar]
|
||||||
|
self.topbar = ToolBar(self, orientation=Qt.Orientation.Horizontal,
|
||||||
|
style=Qt.ToolButtonStyle.ToolButtonTextUnderIcon, icon_size=(24, 24))
|
||||||
|
self.topbar.setMovable(False)
|
||||||
|
|
||||||
|
resources_directory = resources_dir()
|
||||||
|
|
||||||
|
# Top Toolbar Buttons
|
||||||
|
self.topbar.add_button(
|
||||||
|
"New Job", f"{resources_directory}/icons/AddProduct.png", self.new_job)
|
||||||
|
self.topbar.add_button(
|
||||||
|
"Engines", f"{resources_directory}/icons/SoftwareInstaller.png", self.engine_browser)
|
||||||
|
self.topbar.add_button(
|
||||||
|
"Console", f"{resources_directory}/icons/Console.png", self.open_console_window)
|
||||||
|
self.topbar.add_separator()
|
||||||
|
self.topbar.add_button(
|
||||||
|
"Stop Job", f"{resources_directory}/icons/StopSign.png", self.stop_job)
|
||||||
|
self.topbar.add_button(
|
||||||
|
"Delete Job", f"{resources_directory}/icons/Trash.png", self.delete_job)
|
||||||
|
self.topbar.add_button(
|
||||||
|
"Render Log", f"{resources_directory}/icons/Document.png", self.job_logs)
|
||||||
|
self.topbar.add_button(
|
||||||
|
"Download", f"{resources_directory}/icons/Download.png", self.download_files)
|
||||||
|
self.topbar.add_button(
|
||||||
|
"Open Files", f"{resources_directory}/icons/SearchFolder.png", self.open_files)
|
||||||
|
|
||||||
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.topbar)
|
||||||
|
|
||||||
|
# -- Toolbar Buttons -- #
|
||||||
|
|
||||||
|
def open_console_window(self) -> None:
|
||||||
|
"""
|
||||||
|
Event handler for the "Open Console" button
|
||||||
|
"""
|
||||||
|
self.console_window = ConsoleWindow(self.buffer_handler)
|
||||||
|
self.console_window.buffer_handler = self.buffer_handler
|
||||||
|
self.console_window.show()
|
||||||
|
|
||||||
|
def engine_browser(self):
|
||||||
|
self.engine_browser_window = EngineBrowserWindow(hostname=self.current_hostname)
|
||||||
|
self.engine_browser_window.show()
|
||||||
|
|
||||||
|
def job_logs(self) -> None:
|
||||||
|
"""
|
||||||
|
Event handler for the "Logs" button.
|
||||||
|
"""
|
||||||
|
selected_job_ids = self.selected_job_ids()
|
||||||
|
if selected_job_ids:
|
||||||
|
url = f'http://{self.current_server_proxy.hostname}:{self.current_server_proxy.port}/api/job/{selected_job_ids[0]}/logs'
|
||||||
|
self.log_viewer_window = LogViewer(url)
|
||||||
|
self.log_viewer_window.show()
|
||||||
|
|
||||||
|
def stop_job(self, event):
|
||||||
|
"""
|
||||||
|
Event handler for the "Exit" button. Closes the application.
|
||||||
|
"""
|
||||||
|
job_ids = self.selected_job_ids()
|
||||||
|
if not job_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(job_ids) == 1:
|
||||||
|
job = next((job for job in self.current_server_proxy.get_all_jobs() if job.get('id') == job_ids[0]), None)
|
||||||
|
if job:
|
||||||
|
display_name = job.get('name', os.path.basename(job.get('input_path', '')))
|
||||||
|
message = f"Are you sure you want to delete the job:\n{display_name}?"
|
||||||
|
else:
|
||||||
|
return # Job not found, handle this case as needed
|
||||||
|
else:
|
||||||
|
message = f"Are you sure you want to delete these {len(job_ids)} jobs?"
|
||||||
|
|
||||||
|
# Display the message box and check the response in one go
|
||||||
|
msg_box = QMessageBox(QMessageBox.Icon.Warning, "Delete Job", message,
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, self)
|
||||||
|
|
||||||
|
if msg_box.exec() == QMessageBox.StandardButton.Yes:
|
||||||
|
for job_id in job_ids:
|
||||||
|
self.current_server_proxy.cancel_job(job_id, confirm=True)
|
||||||
|
self.fetch_jobs(clear_table=True)
|
||||||
|
|
||||||
|
def delete_job(self, event):
|
||||||
|
"""
|
||||||
|
Event handler for the Delete Job button
|
||||||
|
"""
|
||||||
|
job_ids = self.selected_job_ids()
|
||||||
|
if not job_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(job_ids) == 1:
|
||||||
|
job = next((job for job in self.current_server_proxy.get_all_jobs() if job.get('id') == job_ids[0]), None)
|
||||||
|
if job:
|
||||||
|
display_name = job.get('name', os.path.basename(job.get('input_path', '')))
|
||||||
|
message = f"Are you sure you want to delete the job:\n{display_name}?"
|
||||||
|
else:
|
||||||
|
return # Job not found, handle this case as needed
|
||||||
|
else:
|
||||||
|
message = f"Are you sure you want to delete these {len(job_ids)} jobs?"
|
||||||
|
|
||||||
|
# Display the message box and check the response in one go
|
||||||
|
msg_box = QMessageBox(QMessageBox.Icon.Warning, "Delete Job", message,
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, self)
|
||||||
|
|
||||||
|
if msg_box.exec() == QMessageBox.StandardButton.Yes:
|
||||||
|
for job_id in job_ids:
|
||||||
|
self.current_server_proxy.delete_job(job_id, confirm=True)
|
||||||
|
self.fetch_jobs(clear_table=True)
|
||||||
|
|
||||||
|
def download_files(self, event):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def open_files(self, event):
|
||||||
|
job_ids = self.selected_job_ids()
|
||||||
|
if not job_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
for job_id in job_ids:
|
||||||
|
job_info = self.current_server_proxy.get_job_info(job_id)
|
||||||
|
path = os.path.dirname(job_info['output_path'])
|
||||||
|
|
||||||
|
if sys.platform.startswith('darwin'):
|
||||||
|
subprocess.run(['open', path])
|
||||||
|
elif sys.platform.startswith('win32'):
|
||||||
|
os.startfile(path)
|
||||||
|
elif sys.platform.startswith('linux'):
|
||||||
|
subprocess.run(['xdg-open', path])
|
||||||
|
else:
|
||||||
|
raise OSError("Unsupported operating system")
|
||||||
|
|
||||||
|
def new_job(self) -> None:
|
||||||
|
self.new_job_window = NewRenderJobForm()
|
||||||
|
self.new_job_window.show()
|
||||||
0
src/ui/widgets/__init__.py
Normal file
1
src/ui/widgets/dialog.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
''' app/ui/widgets/dialog.py '''
|
||||||
23
src/ui/widgets/menubar.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
''' app/ui/widgets/menubar.py '''
|
||||||
|
from PyQt6.QtWidgets import QMenuBar
|
||||||
|
|
||||||
|
|
||||||
|
class MenuBar(QMenuBar):
|
||||||
|
"""
|
||||||
|
Initialize the menu bar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: The parent widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
file_menu = self.addMenu("File")
|
||||||
|
# edit_menu = self.addMenu("Edit")
|
||||||
|
# view_menu = self.addMenu("View")
|
||||||
|
# help_menu = self.addMenu("Help")
|
||||||
|
|
||||||
|
# Add actions to the menus
|
||||||
|
# file_menu.addAction(self.parent().topbar.actions_call["Open"]) # type: ignore
|
||||||
|
# file_menu.addAction(self.parent().topbar.actions_call["Save"]) # type: ignore
|
||||||
|
# file_menu.addAction(self.parent().topbar.actions_call["Exit"]) # type: ignore
|
||||||
40
src/ui/widgets/proportional_image_label.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from PyQt6.QtCore import QRectF
|
||||||
|
from PyQt6.QtGui import QPainter
|
||||||
|
from PyQt6.QtWidgets import QLabel
|
||||||
|
|
||||||
|
|
||||||
|
class ProportionalImageLabel(QLabel):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def setPixmap(self, pixmap):
|
||||||
|
self._pixmap = pixmap
|
||||||
|
super().setPixmap(self._pixmap)
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
if self._pixmap.isNull():
|
||||||
|
super().paintEvent(event)
|
||||||
|
return
|
||||||
|
|
||||||
|
painter = QPainter(self)
|
||||||
|
targetRect = event.rect()
|
||||||
|
|
||||||
|
# Calculate the aspect ratio of the pixmap
|
||||||
|
aspectRatio = self._pixmap.width() / self._pixmap.height()
|
||||||
|
|
||||||
|
# Calculate the size of the pixmap within the target rectangle while maintaining the aspect ratio
|
||||||
|
if aspectRatio > targetRect.width() / targetRect.height():
|
||||||
|
scaledWidth = targetRect.width()
|
||||||
|
scaledHeight = targetRect.width() / aspectRatio
|
||||||
|
else:
|
||||||
|
scaledHeight = targetRect.height()
|
||||||
|
scaledWidth = targetRect.height() * aspectRatio
|
||||||
|
|
||||||
|
# Calculate the position to center the pixmap within the target rectangle
|
||||||
|
x = targetRect.x() + (targetRect.width() - scaledWidth) / 2
|
||||||
|
y = targetRect.y() + (targetRect.height() - scaledHeight) / 2
|
||||||
|
|
||||||
|
sourceRect = QRectF(0.0, 0.0, self._pixmap.width(), self._pixmap.height())
|
||||||
|
targetRect = QRectF(x, y, scaledWidth, scaledHeight)
|
||||||
|
|
||||||
|
painter.drawPixmap(targetRect, self._pixmap, sourceRect)
|
||||||
61
src/ui/widgets/statusbar.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
''' app/ui/widgets/statusbar.py '''
|
||||||
|
import os.path
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from PyQt6.QtCore import Qt, QTimer
|
||||||
|
from PyQt6.QtGui import QPixmap
|
||||||
|
from PyQt6.QtWidgets import QStatusBar, QLabel
|
||||||
|
|
||||||
|
from src.api.server_proxy import RenderServerProxy
|
||||||
|
from src.utilities.misc_helper import resources_dir
|
||||||
|
|
||||||
|
|
||||||
|
class StatusBar(QStatusBar):
|
||||||
|
"""
|
||||||
|
Initialize the status bar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: The parent widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
def background_update():
|
||||||
|
|
||||||
|
proxy = RenderServerProxy(socket.gethostname())
|
||||||
|
proxy.start_background_update()
|
||||||
|
image_names = {'Ready': 'GreenCircle.png', 'Offline': "RedSquare.png"}
|
||||||
|
last_update = None
|
||||||
|
|
||||||
|
# Check for status change every 1s on background thread
|
||||||
|
while True:
|
||||||
|
new_status = proxy.status()
|
||||||
|
if new_status is not last_update:
|
||||||
|
new_image_name = image_names.get(new_status, 'Synchronize.png')
|
||||||
|
image_path = os.path.join(resources_dir(), 'icons', new_image_name)
|
||||||
|
self.label.setPixmap((QPixmap(image_path).scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio)))
|
||||||
|
self.messageLabel.setText(new_status)
|
||||||
|
last_update = new_status
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
background_thread = threading.Thread(target=background_update,)
|
||||||
|
background_thread.daemon = True
|
||||||
|
background_thread.start()
|
||||||
|
|
||||||
|
# Create a label that holds an image
|
||||||
|
self.label = QLabel()
|
||||||
|
image_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'resources', 'icons',
|
||||||
|
'RedSquare.png')
|
||||||
|
pixmap = (QPixmap(image_path).scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio))
|
||||||
|
self.label.setPixmap(pixmap)
|
||||||
|
self.addWidget(self.label)
|
||||||
|
|
||||||
|
# Create a label for the message
|
||||||
|
self.messageLabel = QLabel()
|
||||||
|
self.addWidget(self.messageLabel)
|
||||||
|
|
||||||
|
# Call this method to display a message
|
||||||
|
self.messageLabel.setText("Loading...")
|
||||||
49
src/ui/widgets/toolbar.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
''' app/ui/widgets/toolbar.py '''
|
||||||
|
from PyQt6.QtCore import Qt, QSize
|
||||||
|
from PyQt6.QtGui import QAction, QIcon
|
||||||
|
from PyQt6.QtWidgets import QToolBar, QWidget, QSizePolicy
|
||||||
|
|
||||||
|
|
||||||
|
class ToolBar(QToolBar):
|
||||||
|
"""
|
||||||
|
Initialize the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: The parent widget.
|
||||||
|
orientation: The toolbar's orientation.
|
||||||
|
style: The toolbar's tool button style.
|
||||||
|
icon_size: The toolbar's icon size.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent,
|
||||||
|
orientation: Qt.Orientation = Qt.Orientation.Horizontal,
|
||||||
|
style: Qt.ToolButtonStyle = Qt.ToolButtonStyle.ToolButtonTextUnderIcon,
|
||||||
|
icon_size: tuple[int, int] = (32, 32)) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.actions_call = {}
|
||||||
|
self.setOrientation(orientation)
|
||||||
|
|
||||||
|
self.setToolButtonStyle(style)
|
||||||
|
self.setIconSize(QSize(icon_size[0], icon_size[1]))
|
||||||
|
|
||||||
|
def add_button(self, text: str, icon: str, trigger_action) -> None:
|
||||||
|
"""
|
||||||
|
Add a button to the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The button's text.
|
||||||
|
icon: The button's icon.
|
||||||
|
trigger_action: The action to be executed when the button is clicked.
|
||||||
|
"""
|
||||||
|
self.actions_call[text] = QAction(QIcon(icon), text, self)
|
||||||
|
self.actions_call[text].triggered.connect(trigger_action)
|
||||||
|
self.addAction(self.actions_call[text])
|
||||||
|
|
||||||
|
def add_separator(self) -> None:
|
||||||
|
"""
|
||||||
|
Add a separator to the toolbar.
|
||||||
|
"""
|
||||||
|
separator = QWidget(self)
|
||||||
|
separator.setSizePolicy(
|
||||||
|
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||||
|
self.addWidget(separator)
|
||||||
29
src/ui/widgets/treeview.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
''' app/ui/widgets/treeview.py '''
|
||||||
|
from PyQt6.QtWidgets import QTreeView
|
||||||
|
from PyQt6.QtGui import QFileSystemModel
|
||||||
|
from PyQt6.QtCore import QDir
|
||||||
|
|
||||||
|
|
||||||
|
class TreeView(QTreeView):
|
||||||
|
"""
|
||||||
|
Initialize the TreeView widget.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent (QWidget, optional): Parent widget of the TreeView. Defaults to None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.file_system_model: QFileSystemModel = QFileSystemModel()
|
||||||
|
self.file_system_model.setRootPath(QDir.currentPath())
|
||||||
|
self.setModel(self.file_system_model)
|
||||||
|
self.setRootIndex(self.file_system_model.index(QDir.currentPath()))
|
||||||
|
self.setColumnWidth(0, 100)
|
||||||
|
self.setFixedWidth(150)
|
||||||
|
self.setSortingEnabled(True)
|
||||||
|
|
||||||
|
def clear_view(self) -> None:
|
||||||
|
"""
|
||||||
|
Clearing the TreeView
|
||||||
|
"""
|
||||||
|
self.destroy(destroySubWindows=True)
|
||||||
@@ -123,3 +123,15 @@ def current_system_os_version():
|
|||||||
def current_system_cpu():
|
def current_system_cpu():
|
||||||
# convert all x86 64 to "x64"
|
# convert all x86 64 to "x64"
|
||||||
return platform.machine().lower().replace('amd64', 'x64').replace('x86_64', 'x64')
|
return platform.machine().lower().replace('amd64', 'x64').replace('x86_64', 'x64')
|
||||||
|
|
||||||
|
|
||||||
|
def resources_dir():
|
||||||
|
resources_directory = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||||
|
'resources')
|
||||||
|
return resources_directory
|
||||||
|
|
||||||
|
|
||||||
|
def config_dir():
|
||||||
|
config_directory = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||||
|
'config')
|
||||||
|
return config_directory
|
||||||
|
|||||||
@@ -35,19 +35,22 @@ class ZeroconfServer:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _register_service(cls):
|
def _register_service(cls):
|
||||||
cls.server_ip = socket.gethostbyname(socket.gethostname())
|
try:
|
||||||
|
cls.server_ip = socket.gethostbyname(socket.gethostname())
|
||||||
|
|
||||||
info = ServiceInfo(
|
info = ServiceInfo(
|
||||||
cls.service_type,
|
cls.service_type,
|
||||||
f"{cls.server_name}.{cls.service_type}",
|
f"{cls.server_name}.{cls.service_type}",
|
||||||
addresses=[socket.inet_aton(cls.server_ip)],
|
addresses=[socket.inet_aton(cls.server_ip)],
|
||||||
port=cls.server_port,
|
port=cls.server_port,
|
||||||
properties=cls.properties,
|
properties=cls.properties,
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.service_info = info
|
cls.service_info = info
|
||||||
cls.zeroconf.register_service(info)
|
cls.zeroconf.register_service(info)
|
||||||
logger.info(f"Registered zeroconf service: {cls.service_info.name}")
|
logger.info(f"Registered zeroconf service: {cls.service_info.name}")
|
||||||
|
except socket.gaierror as e:
|
||||||
|
logger.error(f"Error starting zeroconf service: {e}")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _unregister_service(cls):
|
def _unregister_service(cls):
|
||||||
@@ -73,7 +76,17 @@ class ZeroconfServer:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def found_clients(cls):
|
def found_clients(cls):
|
||||||
return [x.split(f'.{cls.service_type}')[0] for x in cls.client_cache.keys()]
|
|
||||||
|
fetched_hostnames = [x.split(f'.{cls.service_type}')[0] for x in cls.client_cache.keys()]
|
||||||
|
local_hostname = socket.gethostname()
|
||||||
|
# Define a sort key function
|
||||||
|
def sort_key(hostname):
|
||||||
|
# Return 0 if it's the local hostname so it comes first, else return 1
|
||||||
|
return 0 if hostname == local_hostname else 1
|
||||||
|
|
||||||
|
# Sort the list with the local hostname first
|
||||||
|
sorted_hostnames = sorted(fetched_hostnames, key=sort_key)
|
||||||
|
return sorted_hostnames
|
||||||
|
|
||||||
|
|
||||||
# Example usage:
|
# Example usage:
|
||||||
|
|||||||