mirror of
https://github.com/blw1138/Zordon.git
synced 2025-12-17 08:48:13 +00:00
5
dashboard.py
Executable file → Normal file
5
dashboard.py
Executable file → Normal file
@@ -6,7 +6,6 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
import requests
|
|
||||||
from rich import box
|
from rich import box
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.layout import Layout
|
from rich.layout import Layout
|
||||||
@@ -17,8 +16,8 @@ from rich.table import Table
|
|||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
from src.workers.base_worker import RenderStatus, string_to_status
|
from src.engines.core.base_worker import RenderStatus, string_to_status
|
||||||
from src.server_proxy import RenderServerProxy
|
from src.api.server_proxy import RenderServerProxy
|
||||||
from src.utilities.misc_helper import get_time_elapsed
|
from src.utilities.misc_helper import get_time_elapsed
|
||||||
from start_server import start_server
|
from start_server import start_server
|
||||||
|
|
||||||
|
|||||||
2
main.py
Normal file → Executable file
2
main.py
Normal file → Executable file
@@ -1,4 +1,4 @@
|
|||||||
''' main.py '''
|
#!/usr/bin/env python3
|
||||||
from src import init
|
from src import init
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ psutil==5.9.6
|
|||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
Flask==3.0.0
|
Flask==3.0.0
|
||||||
rich==13.6.0
|
rich==13.6.0
|
||||||
Werkzeug==3.0.0
|
Werkzeug~=3.0.1
|
||||||
future==0.18.3
|
|
||||||
json2html~=1.3.0
|
json2html~=1.3.0
|
||||||
SQLAlchemy~=2.0.15
|
SQLAlchemy~=2.0.15
|
||||||
Pillow==10.1.0
|
Pillow==10.1.0
|
||||||
@@ -12,5 +11,5 @@ zeroconf==0.119.0
|
|||||||
Pypubsub~=4.0.3
|
Pypubsub~=4.0.3
|
||||||
tqdm==4.66.1
|
tqdm==4.66.1
|
||||||
plyer==2.1.0
|
plyer==2.1.0
|
||||||
PyQt6~=6.5.3
|
PyQt6~=6.6.0
|
||||||
PySide6~=6.6.0
|
PySide6~=6.6.0
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk, messagebox, simpledialog
|
|
||||||
|
|
||||||
from PIL import Image, ImageTk
|
|
||||||
|
|
||||||
from src.client.new_job_window import NewJobWindow
|
|
||||||
# from src.client.server_details import create_server_popup
|
|
||||||
from src.api.server_proxy import RenderServerProxy
|
|
||||||
from src.utilities.misc_helper import launch_url, file_exists_in_mounts, get_time_elapsed
|
|
||||||
from src.utilities.zeroconf_server import ZeroconfServer
|
|
||||||
from src.engines.core.base_worker import RenderStatus
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
|
||||||
|
|
||||||
|
|
||||||
def sort_column(tree, col, reverse=False):
|
|
||||||
data = [(tree.set(child, col), child) for child in tree.get_children('')]
|
|
||||||
data.sort(reverse=reverse)
|
|
||||||
for index, (_, child) in enumerate(data):
|
|
||||||
tree.move(child, '', index)
|
|
||||||
|
|
||||||
|
|
||||||
def make_sortable(tree):
|
|
||||||
for col in tree["columns"]:
|
|
||||||
tree.heading(col, text=col, command=lambda c=col: sort_column(tree, c))
|
|
||||||
|
|
||||||
|
|
||||||
class DashboardWindow:
|
|
||||||
|
|
||||||
lib_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
image_path = os.path.join(lib_path, 'web', 'static', 'images')
|
|
||||||
default_image = Image.open(os.path.join(image_path, 'desktop.png'))
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
|
|
||||||
# Create a Treeview widget
|
|
||||||
self.root = tk.Tk()
|
|
||||||
self.root.title("Zordon Dashboard")
|
|
||||||
self.current_hostname = None
|
|
||||||
self.server_proxies = {}
|
|
||||||
self.added_hostnames = []
|
|
||||||
|
|
||||||
# Setup zeroconf
|
|
||||||
ZeroconfServer.configure("_zordon._tcp.local.", socket.gethostname(), 8080)
|
|
||||||
ZeroconfServer.start(listen_only=True)
|
|
||||||
|
|
||||||
# Setup photo preview
|
|
||||||
photo_pad = tk.Frame(self.root, background="gray")
|
|
||||||
photo_pad.pack(fill=tk.BOTH, pady=5, padx=5)
|
|
||||||
self.photo_label = tk.Label(photo_pad, height=500)
|
|
||||||
self.photo_label.pack(fill=tk.BOTH, expand=True)
|
|
||||||
self.set_image(self.default_image)
|
|
||||||
|
|
||||||
server_frame = tk.LabelFrame(self.root, text="Server")
|
|
||||||
server_frame.pack(fill=tk.BOTH, pady=5, padx=5, expand=True)
|
|
||||||
|
|
||||||
# Create server tree
|
|
||||||
left_frame = tk.Frame(server_frame)
|
|
||||||
left_frame.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
|
|
||||||
self.server_tree = ttk.Treeview(left_frame, show="headings")
|
|
||||||
self.server_tree.pack(expand=True, fill=tk.BOTH)
|
|
||||||
self.server_tree["columns"] = ("Server", "Status")
|
|
||||||
self.server_tree.bind("<<TreeviewSelect>>", self.server_picked)
|
|
||||||
self.server_tree.column("Server", width=200)
|
|
||||||
self.server_tree.column("Status", width=80)
|
|
||||||
|
|
||||||
left_button_frame = tk.Frame(left_frame)
|
|
||||||
left_button_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=5, expand=False)
|
|
||||||
|
|
||||||
# Create buttons
|
|
||||||
self.remove_server_button = tk.Button(left_button_frame, text="-", command=self.remove_server_button)
|
|
||||||
self.remove_server_button.pack(side=tk.RIGHT)
|
|
||||||
self.remove_server_button.config(state='disabled')
|
|
||||||
add_server_button = tk.Button(left_button_frame, text="+", command=self.add_server_button)
|
|
||||||
add_server_button.pack(side=tk.RIGHT)
|
|
||||||
|
|
||||||
# Create separator
|
|
||||||
separator = ttk.Separator(server_frame, orient=tk.VERTICAL)
|
|
||||||
separator.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.Y)
|
|
||||||
|
|
||||||
# Setup the Tree
|
|
||||||
self.job_tree = ttk.Treeview(server_frame, show="headings")
|
|
||||||
self.job_tree.tag_configure(RenderStatus.RUNNING.value, background='lawn green', font=('', 0, 'bold'))
|
|
||||||
self.job_tree.bind("<<TreeviewSelect>>", self.job_picked)
|
|
||||||
self.job_tree["columns"] = ("id", "Name", "Renderer", "Priority", "Status", "Time Elapsed", "Frames",
|
|
||||||
"Date Added", "Parent", "")
|
|
||||||
|
|
||||||
# Format the columns
|
|
||||||
self.job_tree.column("id", width=0, stretch=False)
|
|
||||||
self.job_tree.column("Name", width=300)
|
|
||||||
self.job_tree.column("Renderer", width=100, stretch=False)
|
|
||||||
self.job_tree.column("Priority", width=50, stretch=False)
|
|
||||||
self.job_tree.column("Status", width=100, stretch=False)
|
|
||||||
self.job_tree.column("Time Elapsed", width=100, stretch=False)
|
|
||||||
self.job_tree.column("Frames", width=50, stretch=False)
|
|
||||||
self.job_tree.column("Date Added", width=150, stretch=True)
|
|
||||||
self.job_tree.column("Parent", width=250, stretch=True)
|
|
||||||
|
|
||||||
# Create the column headings
|
|
||||||
for name in self.job_tree['columns']:
|
|
||||||
self.job_tree.heading(name, text=name)
|
|
||||||
|
|
||||||
# Pack the Treeview widget
|
|
||||||
self.job_tree.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
button_frame = tk.Frame(server_frame)
|
|
||||||
button_frame.pack(pady=5, fill=tk.X, expand=False)
|
|
||||||
|
|
||||||
# Create buttons
|
|
||||||
self.logs_button = tk.Button(button_frame, text="Logs", command=self.open_logs)
|
|
||||||
self.show_files_button = tk.Button(button_frame, text="Show Files", command=self.show_files)
|
|
||||||
self.stop_button = tk.Button(button_frame, text="Stop", command=self.stop_job)
|
|
||||||
self.delete_button = tk.Button(button_frame, text="Delete", command=self.delete_job)
|
|
||||||
add_job_button = tk.Button(button_frame, text="Add Job", command=self.show_new_job_window)
|
|
||||||
|
|
||||||
# Pack the buttons in the frame
|
|
||||||
self.stop_button.pack(side=tk.LEFT)
|
|
||||||
self.stop_button.config(state='disabled')
|
|
||||||
self.delete_button.pack(side=tk.LEFT)
|
|
||||||
self.delete_button.config(state='disabled')
|
|
||||||
self.show_files_button.pack(side=tk.LEFT)
|
|
||||||
self.show_files_button.config(state='disabled')
|
|
||||||
self.logs_button.pack(side=tk.LEFT)
|
|
||||||
self.logs_button.config(state='disabled')
|
|
||||||
add_job_button.pack(side=tk.RIGHT)
|
|
||||||
|
|
||||||
# Start the Tkinter event loop
|
|
||||||
self.root.geometry("500x600+300+300")
|
|
||||||
self.root.maxsize(width=2000, height=1200)
|
|
||||||
self.root.minsize(width=900, height=800)
|
|
||||||
make_sortable(self.job_tree)
|
|
||||||
make_sortable(self.server_tree)
|
|
||||||
|
|
||||||
# update servers
|
|
||||||
self.update_servers()
|
|
||||||
try:
|
|
||||||
selected_server = self.server_tree.get_children()[0]
|
|
||||||
self.server_tree.selection_set(selected_server)
|
|
||||||
self.server_picked()
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# update jobs
|
|
||||||
self.update_jobs()
|
|
||||||
try:
|
|
||||||
selected_job = self.job_tree.get_children()[0]
|
|
||||||
self.job_tree.selection_set(selected_job)
|
|
||||||
self.job_picked()
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# start background update
|
|
||||||
x = threading.Thread(target=self.__background_update)
|
|
||||||
x.daemon = True
|
|
||||||
x.start()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_server_proxy(self):
|
|
||||||
return self.server_proxies.get(self.current_hostname, None)
|
|
||||||
|
|
||||||
def remove_server_button(self):
|
|
||||||
new_hostname = self.server_tree.selection()[0]
|
|
||||||
if new_hostname in self.added_hostnames:
|
|
||||||
self.added_hostnames.remove(new_hostname)
|
|
||||||
self.update_servers()
|
|
||||||
if self.server_tree.get_children():
|
|
||||||
self.server_tree.selection_set(self.server_tree.get_children()[0])
|
|
||||||
self.server_picked(event=None)
|
|
||||||
|
|
||||||
def add_server_button(self):
|
|
||||||
hostname = simpledialog.askstring("Server Hostname", "Enter the server hostname to add:")
|
|
||||||
if hostname:
|
|
||||||
hostname = hostname.strip()
|
|
||||||
if hostname not in self.added_hostnames:
|
|
||||||
if RenderServerProxy(hostname=hostname).connect():
|
|
||||||
self.added_hostnames.append(hostname)
|
|
||||||
self.update_servers()
|
|
||||||
else:
|
|
||||||
messagebox.showerror("Cannot Connect", f"Cannot connect to server at hostname: '{hostname}'")
|
|
||||||
|
|
||||||
def server_picked(self, event=None):
|
|
||||||
try:
|
|
||||||
new_hostname = self.server_tree.selection()[0]
|
|
||||||
self.remove_server_button.config(state="normal" if new_hostname in self.added_hostnames else "disabled")
|
|
||||||
if self.current_hostname == new_hostname:
|
|
||||||
return
|
|
||||||
self.current_hostname = new_hostname
|
|
||||||
self.update_jobs(clear_table=True)
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def selected_job_ids(self):
|
|
||||||
selected_items = self.job_tree.selection() # Get the selected item
|
|
||||||
row_data = [self.job_tree.item(item) for item in selected_items] # Get the text of the selected item
|
|
||||||
job_ids = [row['values'][0] for row in row_data]
|
|
||||||
return job_ids
|
|
||||||
|
|
||||||
def stop_job(self):
|
|
||||||
job_ids = self.selected_job_ids()
|
|
||||||
for job_id in job_ids:
|
|
||||||
self.current_server_proxy.cancel_job(job_id, confirm=True)
|
|
||||||
self.update_jobs(clear_table=True)
|
|
||||||
|
|
||||||
def delete_job(self):
|
|
||||||
job_ids = self.selected_job_ids()
|
|
||||||
if len(job_ids) == 1:
|
|
||||||
job = next((d for d in self.current_server_proxy.get_all_jobs() if d.get('id') == job_ids[0]), None)
|
|
||||||
display_name = job['name'] or os.path.basename(job['input_path'])
|
|
||||||
message = f"Are you sure you want to delete the job:\n{display_name}?"
|
|
||||||
else:
|
|
||||||
message = f"Are you sure you want to delete these {len(job_ids)} jobs?"
|
|
||||||
|
|
||||||
result = messagebox.askyesno("Confirmation", message)
|
|
||||||
if result:
|
|
||||||
for job_id in job_ids:
|
|
||||||
self.current_server_proxy.request_data(f'job/{job_id}/delete?confirm=true')
|
|
||||||
self.update_jobs(clear_table=True)
|
|
||||||
|
|
||||||
def set_image(self, image):
|
|
||||||
thumb_image = ImageTk.PhotoImage(image)
|
|
||||||
if thumb_image:
|
|
||||||
self.photo_label.configure(image=thumb_image)
|
|
||||||
self.photo_label.image = thumb_image
|
|
||||||
|
|
||||||
def job_picked(self, event=None):
|
|
||||||
job_id = self.selected_job_ids()[0] if self.selected_job_ids() else None
|
|
||||||
if job_id:
|
|
||||||
# update thumb
|
|
||||||
def fetch_preview():
|
|
||||||
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.set_image(image)
|
|
||||||
except ConnectionError as e:
|
|
||||||
logger.error(f"Connection error fetching image: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching image: {e}")
|
|
||||||
|
|
||||||
fetch_thread = threading.Thread(target=fetch_preview)
|
|
||||||
fetch_thread.daemon = True
|
|
||||||
fetch_thread.start()
|
|
||||||
else:
|
|
||||||
self.set_image(self.default_image)
|
|
||||||
|
|
||||||
# update button status
|
|
||||||
current_jobs = self.current_server_proxy.get_all_jobs() or []
|
|
||||||
job = next((d for d in current_jobs if d.get('id') == job_id), None)
|
|
||||||
stop_button_state = 'normal' if job and job['status'] == RenderStatus.RUNNING.value else 'disabled'
|
|
||||||
self.stop_button.config(state=stop_button_state)
|
|
||||||
|
|
||||||
generic_button_state = 'normal' if job else 'disabled'
|
|
||||||
self.show_files_button.config(state=generic_button_state)
|
|
||||||
self.delete_button.config(state=generic_button_state)
|
|
||||||
self.logs_button.config(state=generic_button_state)
|
|
||||||
|
|
||||||
def show_files(self):
|
|
||||||
if not self.selected_job_ids():
|
|
||||||
return
|
|
||||||
|
|
||||||
job = next((d for d in self.current_server_proxy.get_all_jobs() if d.get('id') == self.selected_job_ids()[0]), None)
|
|
||||||
output_path = os.path.dirname(job['output_path']) # check local filesystem
|
|
||||||
if not os.path.exists(output_path):
|
|
||||||
output_path = file_exists_in_mounts(output_path) # check any attached network shares
|
|
||||||
if not output_path:
|
|
||||||
return messagebox.showerror("File Not Found", "The file could not be found. Check your network mounts.")
|
|
||||||
launch_url(output_path)
|
|
||||||
|
|
||||||
def open_logs(self):
|
|
||||||
if self.selected_job_ids():
|
|
||||||
url = f'http://{self.current_server_proxy.hostname}:{self.current_server_proxy.port}/api/job/{self.selected_job_ids()[0]}/logs'
|
|
||||||
launch_url(url)
|
|
||||||
|
|
||||||
def mainloop(self):
|
|
||||||
self.root.mainloop()
|
|
||||||
|
|
||||||
def __background_update(self):
|
|
||||||
while True:
|
|
||||||
self.update_servers()
|
|
||||||
self.update_jobs()
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
def update_servers(self):
|
|
||||||
|
|
||||||
def update_row(tree, id, new_values, tags=None):
|
|
||||||
for item in tree.get_children():
|
|
||||||
values = tree.item(item, "values")
|
|
||||||
if values[0] == id:
|
|
||||||
if tags:
|
|
||||||
tree.item(item, values=new_values, tags=tags)
|
|
||||||
else:
|
|
||||||
tree.item(item, values=new_values)
|
|
||||||
break
|
|
||||||
|
|
||||||
current_servers = list(set(ZeroconfServer.found_clients() + self.added_hostnames))
|
|
||||||
for hostname in current_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
|
|
||||||
|
|
||||||
try:
|
|
||||||
for hostname, proxy in self.server_proxies.items():
|
|
||||||
if hostname not in self.server_tree.get_children():
|
|
||||||
self.server_tree.insert("", tk.END, iid=hostname, values=(hostname, proxy.status(), ))
|
|
||||||
else:
|
|
||||||
update_row(self.server_tree, hostname, new_values=(hostname, proxy.status()))
|
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# remove any servers that don't belong
|
|
||||||
for row in self.server_tree.get_children():
|
|
||||||
if row not in current_servers:
|
|
||||||
self.server_tree.delete(row)
|
|
||||||
proxy = self.server_proxies.get(row, None)
|
|
||||||
if proxy:
|
|
||||||
proxy.stop_background_update()
|
|
||||||
self.server_proxies.pop(row)
|
|
||||||
|
|
||||||
def update_jobs(self, clear_table=False):
|
|
||||||
|
|
||||||
if not self.current_server_proxy:
|
|
||||||
return
|
|
||||||
|
|
||||||
def update_row(tree, id, new_values, tags=None):
|
|
||||||
for item in tree.get_children():
|
|
||||||
values = tree.item(item, "values")
|
|
||||||
if values[0] == id:
|
|
||||||
tree.item(item, values=new_values, tags=tags)
|
|
||||||
break
|
|
||||||
|
|
||||||
if clear_table:
|
|
||||||
self.job_tree.delete(*self.job_tree.get_children())
|
|
||||||
|
|
||||||
job_fetch = self.current_server_proxy.get_all_jobs(ignore_token=clear_table)
|
|
||||||
if job_fetch:
|
|
||||||
for job in 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)
|
|
||||||
|
|
||||||
values = (job['id'],
|
|
||||||
job['name'] or os.path.basename(job['input_path']),
|
|
||||||
job['renderer'] + "-" + job['renderer_version'],
|
|
||||||
job['priority'],
|
|
||||||
display_status,
|
|
||||||
time_elapsed,
|
|
||||||
job['total_frames'],
|
|
||||||
job['date_created'],
|
|
||||||
job['parent'])
|
|
||||||
try:
|
|
||||||
if self.job_tree.exists(job['id']):
|
|
||||||
update_row(self.job_tree, job['id'], new_values=values, tags=tags)
|
|
||||||
else:
|
|
||||||
self.job_tree.insert("", tk.END, iid=job['id'], values=values, tags=tags)
|
|
||||||
except tk.TclError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# remove any jobs that don't belong
|
|
||||||
all_job_ids = [job['id'] for job in job_fetch]
|
|
||||||
for row in self.job_tree.get_children():
|
|
||||||
if row not in all_job_ids:
|
|
||||||
self.job_tree.delete(row)
|
|
||||||
|
|
||||||
def show_new_job_window(self):
|
|
||||||
new_window = tk.Toplevel(self.root)
|
|
||||||
new_window.title("New Window")
|
|
||||||
new_window.geometry("500x600+300+300")
|
|
||||||
new_window.resizable(False, height=True)
|
|
||||||
x = NewJobWindow(parent=new_window, clients=list(self.server_tree.get_children()))
|
|
||||||
x.pack()
|
|
||||||
|
|
||||||
|
|
||||||
def start_client():
|
|
||||||
|
|
||||||
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(module)s: %(message)s', datefmt='%d-%b-%y %H:%M:%S',
|
|
||||||
level='INFO'.upper())
|
|
||||||
|
|
||||||
x = DashboardWindow()
|
|
||||||
x.mainloop()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
start_client()
|
|
||||||
@@ -1,452 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import copy
|
|
||||||
import logging
|
|
||||||
import os.path
|
|
||||||
import pathlib
|
|
||||||
import socket
|
|
||||||
import threading
|
|
||||||
from tkinter import *
|
|
||||||
from tkinter import filedialog, messagebox
|
|
||||||
from tkinter.ttk import Frame, Label, Entry, Combobox, Progressbar
|
|
||||||
|
|
||||||
import psutil
|
|
||||||
|
|
||||||
from src.api.server_proxy import RenderServerProxy
|
|
||||||
from src.engines.blender.blender_worker import Blender
|
|
||||||
from src.engines.ffmpeg.ffmpeg_worker import FFMPEG
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
|
||||||
|
|
||||||
label_width = 9
|
|
||||||
header_padding = 6
|
|
||||||
|
|
||||||
|
|
||||||
# CheckListBox source - https://stackoverflow.com/questions/50398649/python-tkinter-tk-support-checklist-box
|
|
||||||
class ChecklistBox(Frame):
|
|
||||||
def __init__(self, parent, choices, **kwargs):
|
|
||||||
super().__init__(parent, **kwargs)
|
|
||||||
|
|
||||||
self.vars = []
|
|
||||||
for choice in choices:
|
|
||||||
var = StringVar(value="")
|
|
||||||
self.vars.append(var)
|
|
||||||
cb = Checkbutton(self, text=choice, onvalue=choice, offvalue="", anchor="w", width=20,
|
|
||||||
relief="flat", highlightthickness=0, variable=var)
|
|
||||||
cb.pack(side="top", fill="x", anchor="w")
|
|
||||||
|
|
||||||
def getCheckedItems(self):
|
|
||||||
values = []
|
|
||||||
for var in self.vars:
|
|
||||||
value = var.get()
|
|
||||||
if value:
|
|
||||||
values.append(value)
|
|
||||||
return values
|
|
||||||
|
|
||||||
def resetCheckedItems(self):
|
|
||||||
values = []
|
|
||||||
for var in self.vars:
|
|
||||||
var.set(value='')
|
|
||||||
return values
|
|
||||||
|
|
||||||
|
|
||||||
class NewJobWindow(Frame):
|
|
||||||
|
|
||||||
def __init__(self, parent=None, clients=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
self.root = parent
|
|
||||||
self.clients = clients or []
|
|
||||||
self.server_proxy = RenderServerProxy(hostname=clients[0] if clients else None)
|
|
||||||
self.chosen_file = None
|
|
||||||
self.project_info = {}
|
|
||||||
self.presets = {}
|
|
||||||
self.renderer_info = {}
|
|
||||||
self.priority = IntVar(value=2)
|
|
||||||
|
|
||||||
self.master.title("New Job")
|
|
||||||
self.pack(fill=BOTH, expand=True)
|
|
||||||
|
|
||||||
# project frame
|
|
||||||
job_frame = LabelFrame(self, text="Job Settings")
|
|
||||||
job_frame.pack(fill=X, padx=5, pady=5)
|
|
||||||
|
|
||||||
# project frame
|
|
||||||
project_frame = Frame(job_frame)
|
|
||||||
project_frame.pack(fill=X)
|
|
||||||
|
|
||||||
project_label = Label(project_frame, text="Project", width=label_width)
|
|
||||||
project_label.pack(side=LEFT, padx=5, pady=5)
|
|
||||||
|
|
||||||
self.project_button = Button(project_frame, text="no file selected", width=6, command=self.choose_file_button)
|
|
||||||
self.project_button.pack(fill=X, padx=5, expand=True)
|
|
||||||
|
|
||||||
# client frame
|
|
||||||
client_frame = Frame(job_frame)
|
|
||||||
client_frame.pack(fill=X)
|
|
||||||
|
|
||||||
Label(client_frame, text="Client", width=label_width).pack(side=LEFT, padx=5, pady=5)
|
|
||||||
|
|
||||||
self.client_combo = Combobox(client_frame, state="readonly")
|
|
||||||
self.client_combo.pack(fill=X, padx=5, expand=True)
|
|
||||||
self.client_combo.bind('<<ComboboxSelected>>', self.client_picked)
|
|
||||||
self.client_combo['values'] = self.clients
|
|
||||||
if self.clients:
|
|
||||||
self.client_combo.current(0)
|
|
||||||
|
|
||||||
# renderer frame
|
|
||||||
renderer_frame = Frame(job_frame)
|
|
||||||
renderer_frame.pack(fill=X)
|
|
||||||
|
|
||||||
Label(renderer_frame, text="Renderer", width=label_width).pack(side=LEFT, padx=5, pady=5)
|
|
||||||
self.renderer_combo = Combobox(renderer_frame, state="readonly")
|
|
||||||
self.renderer_combo.pack(fill=X, padx=5, expand=True)
|
|
||||||
self.renderer_combo.bind('<<ComboboxSelected>>', self.refresh_renderer_settings)
|
|
||||||
|
|
||||||
# priority frame
|
|
||||||
priority_frame = Frame(job_frame)
|
|
||||||
priority_frame.pack(fill=X)
|
|
||||||
|
|
||||||
Label(priority_frame, text="Priority", width=label_width).pack(side=LEFT, padx=5, pady=5)
|
|
||||||
Radiobutton(priority_frame, text="1", value=1, variable=self.priority).pack(anchor=W, side=LEFT, padx=5)
|
|
||||||
Radiobutton(priority_frame, text="2", value=2, variable=self.priority).pack(anchor=W, side=LEFT, padx=5)
|
|
||||||
Radiobutton(priority_frame, text="3", value=3, variable=self.priority).pack(anchor=W, side=LEFT, padx=5)
|
|
||||||
|
|
||||||
# presets
|
|
||||||
presets_frame = Frame(job_frame)
|
|
||||||
presets_frame.pack(fill=X)
|
|
||||||
|
|
||||||
Label(presets_frame, text="Presets", width=label_width).pack(side=LEFT, padx=5, pady=5)
|
|
||||||
self.presets_combo = Combobox(presets_frame, state="readonly")
|
|
||||||
self.presets_combo.pack(side=LEFT, padx=5, pady=5, expand=True, fill=X)
|
|
||||||
self.presets_combo.bind('<<ComboboxSelected>>', self.chose_preset)
|
|
||||||
|
|
||||||
# output frame
|
|
||||||
output_frame = Frame(job_frame)
|
|
||||||
output_frame.pack(fill=X)
|
|
||||||
|
|
||||||
Label(output_frame, text="Output", width=label_width).pack(side=LEFT, padx=5, pady=5)
|
|
||||||
|
|
||||||
self.output_entry = Entry(output_frame)
|
|
||||||
self.output_entry.pack(side=LEFT, padx=5, expand=True, fill=X)
|
|
||||||
|
|
||||||
self.output_format = Combobox(output_frame, state="readonly", values=['JPG', 'MOV', 'PNG'], width=9)
|
|
||||||
self.output_format.pack(side=LEFT, padx=5, pady=5)
|
|
||||||
self.output_format['state'] = DISABLED
|
|
||||||
|
|
||||||
# frame_range frame
|
|
||||||
frame_range_frame = Frame(job_frame)
|
|
||||||
frame_range_frame.pack(fill=X)
|
|
||||||
|
|
||||||
Label(frame_range_frame, text="Frames", width=label_width).pack(side=LEFT, padx=5, pady=5, expand=False)
|
|
||||||
|
|
||||||
self.start_frame_spinbox = Spinbox(frame_range_frame, from_=0, to=50000, width=5)
|
|
||||||
self.start_frame_spinbox.pack(side=LEFT, expand=False, padx=5, pady=5)
|
|
||||||
|
|
||||||
Label(frame_range_frame, text="to").pack(side=LEFT, pady=5, expand=False)
|
|
||||||
self.end_frame_spinbox = Spinbox(frame_range_frame, from_=0, to=50000, width=5)
|
|
||||||
self.end_frame_spinbox.pack(side=LEFT, expand=False, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Blender
|
|
||||||
self.blender_frame = None
|
|
||||||
self.blender_cameras_frame = None
|
|
||||||
self.blender_engine = StringVar(value='CYCLES')
|
|
||||||
self.blender_pack_textures = BooleanVar(value=False)
|
|
||||||
self.blender_multiple_cameras = BooleanVar(value=False)
|
|
||||||
self.blender_cameras_list = None
|
|
||||||
|
|
||||||
# Custom Args / Submit Button
|
|
||||||
self.custom_args_frame = None
|
|
||||||
self.custom_args_entry = None
|
|
||||||
self.submit_frame = None
|
|
||||||
|
|
||||||
self.progress_frame = None
|
|
||||||
self.progress_label = None
|
|
||||||
self.progress_bar = None
|
|
||||||
self.upload_status = None
|
|
||||||
|
|
||||||
self.fetch_server_data()
|
|
||||||
|
|
||||||
def client_picked(self, event=None):
|
|
||||||
self.server_proxy.hostname = self.client_combo.get()
|
|
||||||
self.fetch_server_data()
|
|
||||||
|
|
||||||
def fetch_server_data(self):
|
|
||||||
self.renderer_info = self.server_proxy.request_data('renderer_info', timeout=3) or {}
|
|
||||||
self.presets = self.server_proxy.request_data('presets', timeout=3) or {}
|
|
||||||
|
|
||||||
# update available renders
|
|
||||||
self.renderer_combo['values'] = list(self.renderer_info.keys())
|
|
||||||
if self.renderer_info.keys():
|
|
||||||
self.renderer_combo.current(0)
|
|
||||||
|
|
||||||
self.refresh_renderer_settings()
|
|
||||||
|
|
||||||
def choose_file_button(self):
|
|
||||||
self.chosen_file = filedialog.askopenfilename()
|
|
||||||
button_text = os.path.basename(self.chosen_file) if self.chosen_file else "no file selected"
|
|
||||||
self.project_button.configure(text=button_text)
|
|
||||||
|
|
||||||
# Update the output label
|
|
||||||
self.output_entry.delete(0, END)
|
|
||||||
if self.chosen_file:
|
|
||||||
# Generate a default output name
|
|
||||||
output_name = os.path.splitext(os.path.basename(self.chosen_file))[-1].strip('.')
|
|
||||||
self.output_entry.insert(0, os.path.basename(output_name))
|
|
||||||
|
|
||||||
# Try to determine file type
|
|
||||||
extension = os.path.splitext(self.chosen_file)[-1].strip('.') # not the best way to do this
|
|
||||||
for renderer, renderer_info in self.renderer_info.items():
|
|
||||||
supported = [x.lower().strip('.') for x in renderer_info.get('supported_extensions', [])]
|
|
||||||
if extension.lower().strip('.') in supported:
|
|
||||||
if renderer in self.renderer_combo['values']:
|
|
||||||
self.renderer_combo.set(renderer)
|
|
||||||
|
|
||||||
self.refresh_renderer_settings()
|
|
||||||
|
|
||||||
def chose_preset(self, event=None):
|
|
||||||
preset_name = self.presets_combo.get()
|
|
||||||
renderer = self.renderer_combo.get()
|
|
||||||
|
|
||||||
presets_to_show = {key: value for key, value in self.presets.items() if value.get("renderer") == renderer}
|
|
||||||
matching_dict = next((value for value in presets_to_show.values() if value.get("name") == preset_name), None)
|
|
||||||
if matching_dict:
|
|
||||||
self.custom_args_entry.delete(0, END)
|
|
||||||
self.custom_args_entry.insert(0, matching_dict['args'])
|
|
||||||
|
|
||||||
def refresh_renderer_settings(self, event=None):
|
|
||||||
renderer = self.renderer_combo.get()
|
|
||||||
|
|
||||||
# clear old settings
|
|
||||||
if self.blender_frame:
|
|
||||||
self.blender_frame.pack_forget()
|
|
||||||
|
|
||||||
if not self.chosen_file:
|
|
||||||
return
|
|
||||||
|
|
||||||
if renderer == 'blender':
|
|
||||||
self.project_info = Blender().get_scene_info(self.chosen_file)
|
|
||||||
self.draw_blender_settings()
|
|
||||||
elif renderer == 'ffmpeg':
|
|
||||||
f = FFMPEG.get_frame_count(self.chosen_file)
|
|
||||||
self.project_info['frame_end'] = f
|
|
||||||
|
|
||||||
# set frame start / end numbers fetched from fils
|
|
||||||
if self.project_info.get('frame_start'):
|
|
||||||
self.start_frame_spinbox.delete(0, 'end')
|
|
||||||
self.start_frame_spinbox.insert(0, self.project_info['frame_start'])
|
|
||||||
if self.project_info.get('frame_end'):
|
|
||||||
self.end_frame_spinbox.delete(0, 'end')
|
|
||||||
self.end_frame_spinbox.insert(0, self.project_info['frame_end'])
|
|
||||||
|
|
||||||
# redraw lower ui
|
|
||||||
self.draw_custom_args()
|
|
||||||
self.draw_submit_button()
|
|
||||||
|
|
||||||
# check supported export formats
|
|
||||||
if self.renderer_info.get(renderer, {}).get('supported_export_formats', None):
|
|
||||||
formats = self.renderer_info[renderer]['supported_export_formats']
|
|
||||||
if formats and isinstance(formats[0], dict):
|
|
||||||
formats = [x.get('name', str(x)) for x in formats]
|
|
||||||
formats.sort()
|
|
||||||
self.output_format['values'] = formats
|
|
||||||
self.output_format['state'] = NORMAL
|
|
||||||
self.output_format.current(0)
|
|
||||||
else:
|
|
||||||
self.output_format['values'] = []
|
|
||||||
self.output_format['state'] = DISABLED
|
|
||||||
|
|
||||||
# update presets
|
|
||||||
presets_to_show = {key: value for key, value in self.presets.items() if value.get("renderer") == renderer}
|
|
||||||
self.presets_combo['values'] = [value['name'] for value in presets_to_show.values()]
|
|
||||||
|
|
||||||
def draw_custom_args(self):
|
|
||||||
if hasattr(self, 'custom_args_frame') and self.custom_args_frame:
|
|
||||||
self.custom_args_frame.forget()
|
|
||||||
self.custom_args_frame = LabelFrame(self, text="Advanced")
|
|
||||||
self.custom_args_frame.pack(side=TOP, fill=X, expand=False, padx=5, pady=5)
|
|
||||||
Label(self.custom_args_frame, text="Custom Args", width=label_width).pack(side=LEFT, padx=5, pady=5)
|
|
||||||
self.custom_args_entry = Entry(self.custom_args_frame)
|
|
||||||
self.custom_args_entry.pack(side=TOP, padx=5, expand=True, fill=X)
|
|
||||||
|
|
||||||
def draw_submit_button(self):
|
|
||||||
if hasattr(self, 'submit_frame') and self.submit_frame:
|
|
||||||
self.submit_frame.forget()
|
|
||||||
self.submit_frame = Frame(self)
|
|
||||||
self.submit_frame.pack(fill=BOTH, expand=True)
|
|
||||||
# Label(self.submit_frame, text="").pack(fill=BOTH, expand=True)
|
|
||||||
submit_button = Button(self.submit_frame, text="Submit", command=self.submit_job)
|
|
||||||
submit_button.pack(fill=Y, anchor="s", pady=header_padding)
|
|
||||||
|
|
||||||
def draw_progress_frame(self):
|
|
||||||
if hasattr(self, 'progress_frame') and self.progress_frame:
|
|
||||||
self.progress_frame.forget()
|
|
||||||
self.progress_frame = LabelFrame(self, text="Job Submission")
|
|
||||||
self.progress_frame.pack(side=TOP, fill=X, expand=False, padx=5, pady=5)
|
|
||||||
self.progress_bar = Progressbar(self.progress_frame, length=300, mode="determinate")
|
|
||||||
self.progress_bar.pack()
|
|
||||||
self.progress_label = Label(self.progress_frame, text="Starting Up")
|
|
||||||
self.progress_label.pack(pady=5, padx=5)
|
|
||||||
|
|
||||||
def draw_blender_settings(self):
|
|
||||||
|
|
||||||
# blender settings
|
|
||||||
self.blender_frame = LabelFrame(self, text="Blender Settings")
|
|
||||||
self.blender_frame.pack(fill=X, padx=5)
|
|
||||||
|
|
||||||
blender_engine_frame = Frame(self.blender_frame)
|
|
||||||
blender_engine_frame.pack(fill=X)
|
|
||||||
|
|
||||||
Label(blender_engine_frame, text="Engine", width=label_width).pack(side=LEFT, padx=5, pady=5)
|
|
||||||
|
|
||||||
Radiobutton(blender_engine_frame, text="Cycles", value="CYCLES", variable=self.blender_engine).pack(
|
|
||||||
anchor=W, side=LEFT, padx=5)
|
|
||||||
Radiobutton(blender_engine_frame, text="Eevee", value="BLENDER_EEVEE", variable=self.blender_engine).pack(
|
|
||||||
anchor=W, side=LEFT, padx=5)
|
|
||||||
|
|
||||||
# options
|
|
||||||
pack_frame = Frame(self.blender_frame)
|
|
||||||
pack_frame.pack(fill=X)
|
|
||||||
|
|
||||||
Label(pack_frame, text="Options", width=label_width).pack(side=LEFT, padx=5, pady=5)
|
|
||||||
|
|
||||||
Checkbutton(pack_frame, text="Pack Textures", variable=self.blender_pack_textures, onvalue=True, offvalue=False
|
|
||||||
).pack(anchor=W, side=LEFT, padx=5)
|
|
||||||
|
|
||||||
# multi cams
|
|
||||||
def draw_scene_cams(event=None):
|
|
||||||
if self.project_info:
|
|
||||||
show_cams_checkbutton['state'] = NORMAL
|
|
||||||
if self.blender_multiple_cameras.get():
|
|
||||||
self.blender_cameras_frame = Frame(self.blender_frame)
|
|
||||||
self.blender_cameras_frame.pack(fill=X)
|
|
||||||
|
|
||||||
Label(self.blender_cameras_frame, text="Cameras", width=label_width).pack(side=LEFT, padx=5, pady=5)
|
|
||||||
|
|
||||||
choices = [f"{x['name']} - {int(float(x['lens']))}mm" for x in self.project_info['cameras']]
|
|
||||||
choices.sort()
|
|
||||||
self.blender_cameras_list = ChecklistBox(self.blender_cameras_frame, choices, relief="sunken")
|
|
||||||
self.blender_cameras_list.pack(padx=5, fill=X)
|
|
||||||
elif self.blender_cameras_frame:
|
|
||||||
self.blender_cameras_frame.pack_forget()
|
|
||||||
else:
|
|
||||||
show_cams_checkbutton['state'] = DISABLED
|
|
||||||
if self.blender_cameras_frame:
|
|
||||||
self.blender_cameras_frame.pack_forget()
|
|
||||||
|
|
||||||
# multiple cameras checkbox
|
|
||||||
camera_count = len(self.project_info.get('cameras', [])) if self.project_info else 0
|
|
||||||
show_cams_checkbutton = Checkbutton(pack_frame, text=f'Multiple Cameras ({camera_count})', offvalue=False,
|
|
||||||
onvalue=True,
|
|
||||||
variable=self.blender_multiple_cameras, command=draw_scene_cams)
|
|
||||||
show_cams_checkbutton.pack(side=LEFT, padx=5)
|
|
||||||
show_cams_checkbutton['state'] = NORMAL if camera_count > 1 else DISABLED
|
|
||||||
|
|
||||||
def submit_job(self):
|
|
||||||
|
|
||||||
def submit_job_worker():
|
|
||||||
|
|
||||||
self.draw_progress_frame()
|
|
||||||
self.progress_bar['value'] = 0
|
|
||||||
self.progress_bar.configure(mode='determinate')
|
|
||||||
self.progress_bar.start()
|
|
||||||
self.progress_label.configure(text="Preparing files...")
|
|
||||||
|
|
||||||
# start the progress UI
|
|
||||||
client = self.client_combo.get()
|
|
||||||
|
|
||||||
renderer = self.renderer_combo.get()
|
|
||||||
job_json = {'owner': psutil.Process().username() + '@' + socket.gethostname(),
|
|
||||||
'renderer': renderer,
|
|
||||||
'client': client,
|
|
||||||
'output_path': os.path.join(os.path.dirname(self.chosen_file), self.output_entry.get()),
|
|
||||||
'args': {'raw': self.custom_args_entry.get()},
|
|
||||||
'start_frame': self.start_frame_spinbox.get(),
|
|
||||||
'end_frame': self.end_frame_spinbox.get(),
|
|
||||||
'name': None}
|
|
||||||
job_list = []
|
|
||||||
|
|
||||||
input_path = self.chosen_file
|
|
||||||
|
|
||||||
temp_files = []
|
|
||||||
if renderer == 'blender':
|
|
||||||
if self.blender_pack_textures.get():
|
|
||||||
self.progress_label.configure(text="Packing Blender file...")
|
|
||||||
new_path = Blender().pack_project_file(project_path=input_path, timeout=300)
|
|
||||||
if new_path:
|
|
||||||
logger.info(f"New Path is now {new_path}")
|
|
||||||
input_path = new_path
|
|
||||||
temp_files.append(new_path)
|
|
||||||
else:
|
|
||||||
err_msg = f'Failed to pack Blender file: {input_path}'
|
|
||||||
messagebox.showinfo("Error", err_msg)
|
|
||||||
return
|
|
||||||
# add all Blender args
|
|
||||||
job_json['args']['engine'] = self.blender_engine.get()
|
|
||||||
job_json['args']['export_format'] = self.output_format.get()
|
|
||||||
|
|
||||||
# multiple camera rendering
|
|
||||||
if self.blender_cameras_list and self.blender_multiple_cameras.get():
|
|
||||||
selected_cameras = self.blender_cameras_list.getCheckedItems()
|
|
||||||
for cam in selected_cameras:
|
|
||||||
job_copy = copy.deepcopy(job_json)
|
|
||||||
job_copy['args']['camera'] = cam.rsplit('-', 1)[0].strip()
|
|
||||||
job_copy['name'] = pathlib.Path(input_path).stem.replace(' ', '_') + "-" + cam.replace(' ', '')
|
|
||||||
job_list.append(job_copy)
|
|
||||||
|
|
||||||
# Submit to server
|
|
||||||
job_list = job_list or [job_json]
|
|
||||||
self.progress_label.configure(text="Posting to server...")
|
|
||||||
self.progress_bar.stop()
|
|
||||||
self.progress_bar.configure(mode='determinate')
|
|
||||||
self.progress_bar.start()
|
|
||||||
|
|
||||||
def create_callback(encoder):
|
|
||||||
encoder_len = encoder.len
|
|
||||||
|
|
||||||
def callback(monitor):
|
|
||||||
percent = f"{monitor.bytes_read / encoder_len * 100:.0f}"
|
|
||||||
self.progress_label.configure(text=f"Transferring to {client} - {percent}%")
|
|
||||||
self.progress_bar['value'] = int(percent)
|
|
||||||
return callback
|
|
||||||
|
|
||||||
result = self.server_proxy.post_job_to_server(file_path=input_path, job_list=job_list,
|
|
||||||
callback=create_callback)
|
|
||||||
|
|
||||||
self.progress_bar.stop()
|
|
||||||
# clean up
|
|
||||||
for temp in temp_files:
|
|
||||||
os.remove(temp)
|
|
||||||
|
|
||||||
def finish_on_main():
|
|
||||||
if result.ok:
|
|
||||||
message = "Job successfully submitted to server."
|
|
||||||
self.progress_label.configure(text=message)
|
|
||||||
messagebox.showinfo("Success", message)
|
|
||||||
logger.info(message)
|
|
||||||
else:
|
|
||||||
message = result.text or "Unknown error"
|
|
||||||
self.progress_label.configure(text=message)
|
|
||||||
logger.warning(message)
|
|
||||||
messagebox.showinfo("Error", message)
|
|
||||||
self.progress_label.configure(text="")
|
|
||||||
self.progress_frame.forget()
|
|
||||||
|
|
||||||
self.root.after(0, finish_on_main)
|
|
||||||
|
|
||||||
# Start the job submit task as a bg thread
|
|
||||||
bg_thread = threading.Thread(target=submit_job_worker)
|
|
||||||
bg_thread.start()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
|
|
||||||
root = Tk()
|
|
||||||
root.geometry("500x600+300+300")
|
|
||||||
root.maxsize(width=1000, height=2000)
|
|
||||||
root.minsize(width=600, height=600)
|
|
||||||
app = NewJobWindow(root)
|
|
||||||
root.mainloop()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
from src.client.dashboard_window import start_client
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
start_client()
|
|
||||||
Reference in New Issue
Block a user