Files
Zordon/lib/client/client.py
Brett Williams b027a19352 Client cleanup
2023-06-01 15:50:09 -05:00

266 lines
9.9 KiB
Python

import subprocess
import logging
import requests
import tkinter as tk
import threading
import time
import socket
import os
from tkinter import ttk, messagebox
from PIL import Image, ImageTk
from new_job_window import NewJobWindow
from server_proxy import RenderServerProxy
def request_data(server_ip, payload, server_port=8080, timeout=2):
try:
req = requests.get(f'http://{server_ip}:{server_port}/api/{payload}', timeout=timeout)
if req.ok:
return req.json()
except Exception as e:
pass
return None
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:
default_image = Image.open("../server/static/images/desktop.png")
def __init__(self):
servers = available_servers()
# Create a Treeview widget
self.root = tk.Tk()
self.root.title("Zordon Render Client")
self.local_host = socket.gethostname()
self.server_proxy = RenderServerProxy(hostname=servers[0])
self.job_cache = []
# 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)
# 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=20)
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)
# 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=1000)
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.populate_server_tree()
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 populate_server_tree(self):
servers = available_servers()
self.server_tree.delete(*self.server_tree.get_children())
for hostname in servers:
self.server_tree.insert("", tk.END, iid=hostname, values=(hostname,))
def server_picked(self, event):
new_hostname = self.server_tree.selection()[0]
self.server_proxy.hostname = new_hostname
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')
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}")
# 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)
else:
self.set_image(self.default_image)
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):
job = next((d for d in self.job_cache if d.get('id') == self.selected_job_id()), None)
subprocess.run(['open', job['log_path']])
def mainloop(self):
self.root.mainloop()
def __background_update(self):
while True:
self.update_jobs()
time.sleep(1)
def update_jobs(self, clear_table=False):
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:
clear_table = True
self.job_cache = job_fetch # update the cache only if its good data
if clear_table:
self.job_tree.delete(*self.job_tree.get_children())
for job in self.job_cache:
display_status = job['status'] if job['status'] != 'running' else \
('%.0f%%' % (job['percent_complete'] * 100)) # if running, show percentage, 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'])
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)
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()
if __name__ == '__main__':
x = ZordonClient()
x.mainloop()