Files
Zordon/lib/client/client.py
2023-06-03 13:19:32 -05:00

288 lines
11 KiB
Python

import subprocess
import logging
import requests
import tkinter as tk
import threading
import time
import socket
import os, sys
from tkinter import ttk, messagebox
from PIL import Image, ImageTk
from lib.client.new_job_window import NewJobWindow
from lib.server.server_proxy import RenderServerProxy
sys.path.append("../")
from lib.server.zeroconf_server import ZeroconfServer
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))
def available_servers():
return [socket.gethostname()]
class ZordonClient:
lib_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
image_path = os.path.join(lib_path, 'server', 'static', 'images', 'desktop.png')
default_image = Image.open(image_path)
def __init__(self):
# Create a Treeview widget
self.root = tk.Tk()
self.root.title("Zordon Render Client")
self.local_host = socket.gethostname()
self.server_proxy = RenderServerProxy(hostname=self.local_host)
self.job_cache = []
# Setup zeroconf
self.zeroconf = ZeroconfServer("_zordon._tcp.local.", socket.gethostname(), 8080)
self.zeroconf.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
self.server_tree = ttk.Treeview(server_frame, show="headings")
self.server_tree.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
self.server_tree["columns"] = ("Server")
self.server_tree.column("Server", width=50)
self.server_tree.bind("<<TreeviewSelect>>", self.server_picked)
self.server_tree.column("Server", width=200)
make_sortable(self.server_tree)
# 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", height=10)
self.job_tree.tag_configure('running', background='lawn green', font=('', 0, 'bold'))
self.job_tree.bind("<<TreeviewSelect>>", self.on_row_select)
self.job_tree["columns"] = ("id", "Name", "Renderer", "Priority", "Status", "Time Elapsed", "Frames", "")
# 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)
# 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.BOTH, expand=True)
# Create buttons
logs_button = tk.Button(button_frame, text="Logs", command=self.open_logs)
finder_button = tk.Button(button_frame, text="Reveal in Finder", command=self.reveal_in_finder)
self.stop_button = tk.Button(button_frame, text="Stop", command=self.stop_job)
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)
self.stop_button.config(state='disabled')
# Pack the buttons in the frame
self.stop_button.pack(side=tk.LEFT)
delete_button.pack(side=tk.LEFT)
finder_button.pack(side=tk.LEFT)
logs_button.pack(side=tk.LEFT)
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)
# update jobs
self.update_jobs()
try:
selected_job = self.job_tree.get_children()[0]
self.job_tree.selection_set(selected_job)
except IndexError:
pass
# update servers
self.update_servers()
try:
selected_server = self.server_tree.get_children()[0]
self.server_tree.selection_set(selected_server)
except IndexError:
pass
# start background update
x = threading.Thread(target=self.__background_update)
x.daemon = True
x.start()
def server_picked(self, event):
try:
new_hostname = self.server_tree.selection()[0]
self.server_proxy.hostname = new_hostname
except IndexError:
pass
self.job_cache.clear()
self.update_jobs(clear_table=True)
def selected_job_id(self):
try:
selected_item = self.job_tree.selection()[0] # Get the selected item
row_data = self.job_tree.item(selected_item) # Get the text of the selected item
job_id = row_data['values'][0]
return job_id
except Exception as e:
return None
def stop_job(self):
job_id = self.selected_job_id()
self.server_proxy.request_data(f'job/{job_id}/cancel?confirm=true')
def delete_job(self):
job_id = self.selected_job_id()
job = next((d for d in self.job_cache if d.get('id') == job_id), None)
display_name = job['name'] or os.path.basename(job['input_path'])
result = messagebox.askyesno("Confirmation", f"Are you sure you want to delete the job:\n{display_name}?")
if result:
self.server_proxy.request_data(f'job/{job_id}/delete?confirm=true')
self.job_cache.clear()
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 on_row_select(self, event):
job_id = self.selected_job_id()
if job_id:
# update thumb
response = self.server_proxy.request(f'job/{job_id}/thumbnail?size=big&video_ok=false')
if response.ok:
try:
import io
image_data = response.content
image = Image.open(io.BytesIO(image_data))
self.set_image(image)
except Exception as e:
print(f"error getting image: {e}")
else:
self.set_image(self.default_image)
# update button status
job = next((d for d in self.job_cache if d.get('id') == job_id), None)
button_state = 'normal' if job and job['status'] == 'running' else 'disabled'
self.stop_button.config(state=button_state)
def reveal_in_finder(self):
job = next((d for d in self.job_cache if d.get('id') == self.selected_job_id()), None)
output_dir = os.path.dirname(job['output_path'])
subprocess.run(['open', output_dir])
def open_logs(self):
url = f'http://{self.server_proxy.hostname}:{self.server_proxy.port}/api/job/{self.selected_job_id()}/logs'
subprocess.run(['open', url])
def mainloop(self):
self.root.mainloop()
def __background_update(self):
while True:
self.update_jobs()
self.update_servers()
time.sleep(3)
def update_servers(self):
servers = self.zeroconf.found_clients()
if len(servers) < len(self.server_tree.get_children()):
self.server_tree.delete(*self.server_tree.get_children())
for hostname in servers:
if hostname not in self.server_tree.get_children():
self.server_tree.insert("", tk.END, iid=hostname, values=(hostname,))
def update_jobs(self, clear_table=False):
def update_jobs_inner():
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
job_fetch = self.server_proxy.get_jobs()
if job_fetch is not None:
if len(job_fetch) < len(self.job_cache) or len(job_fetch) == 0:
self.job_tree.delete(*self.job_tree.get_children())
self.job_cache = job_fetch # update the cache only if its good data
for job in self.job_cache:
display_status = job['status'] if job['status'] != 'running' else \
('%.0f%%' % (job['percent_complete'] * 100)) # if running, show percent, otherwise just show status
tags = (job['status'],)
values = (job['id'],
job['name'] or os.path.basename(job['input_path']),
job['renderer'] + "-" + job['renderer_version'],
job['priority'],
display_status,
job['time_elapsed'],
job['total_frames'])
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
if clear_table:
self.job_tree.delete(*self.job_tree.get_children())
x = threading.Thread(target=update_jobs_inner)
x.daemon = True
x.start()
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, hostname=self.server_proxy.hostname)
x.pack()
def start_client():
x = ZordonClient()
x.mainloop()
if __name__ == '__main__':
start_client()