Files
Zordon/lib/client/client.py
2023-05-31 23:29:04 -05:00

243 lines
9.5 KiB
Python

import subprocess
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:
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)
server_frame = tk.LabelFrame(self.root, text="Server")
server_frame.pack(fill=tk.BOTH, pady=5, padx=5, expand=True)
server_picker_frame = tk.Frame(server_frame)
server_picker_frame.pack(fill=tk.X, expand=True)
server_label = tk.Label(server_picker_frame, text="Current Server:")
server_label.pack(side=tk.LEFT, padx=5)
self.server_combo = tk.ttk.Combobox(server_picker_frame)
self.server_combo.pack(fill=tk.X)
self.server_combo.bind("<<ComboboxSelected>>", self.server_picked)
self.server_combo['values'] = servers
self.server_combo.current(0)
# Setup the Tree
self.tree = ttk.Treeview(server_frame, show="headings", height=20)
self.tree.tag_configure('running', background='lawn green', font=('', 0, 'bold'))
self.tree.bind("<<TreeviewSelect>>", self.on_row_select)
self.tree["columns"] = ("id", "Name", "Renderer", "Priority", "Status", "Time Elapsed", "Frames")
# Format the columns
self.tree.column("id", width=0, stretch=False)
self.tree.column("Name", width=50)
self.tree.column("Renderer", width=100, stretch=False)
self.tree.column("Priority", width=50, stretch=False)
self.tree.column("Status", width=100, stretch=False)
self.tree.column("Time Elapsed", width=100, stretch=False)
self.tree.column("Frames", width=50, stretch=False)
# Create the column headings
for name in self.tree['columns']:
self.tree.heading(name, text=name)
# Pack the Treeview widget
self.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.tree)
self.update_jobs()
selected_item = self.tree.get_children()[0]
self.tree.selection_set(selected_item)
self.start_update_thread()
def server_picked(self, event):
new_hostname = self.server_combo.get()
self.server_proxy.hostname = new_hostname
self.update_jobs(clear_table=True)
def stop_job(self):
selected_item = self.tree.selection()[0] # Get the selected item
row_data = self.tree.item(selected_item) # Get the text of the selected item
job_id = row_data['values'][0]
self.server_proxy.request_data(f'job/{job_id}/cancel?confirm=true')
def delete_job(self):
selected_item = self.tree.selection()[0] # Get the selected item
row_data = self.tree.item(selected_item) # Get the text of the selected item
job_id = row_data['values'][0]
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.update_jobs(clear_table=True)
def on_row_select(self, event):
if self.tree.selection():
selected_item = self.tree.selection()[0] # Get the selected item
row_data = self.tree.item(selected_item) # Get the text of the selected item
job_id = row_data['values'][0]
# update thumb
thumb_url = f'http://{self.server_proxy.hostname}:{self.server_proxy.port}/ui/job/{job_id}/thumbnail?size=big'
response = requests.get(thumb_url)
if response.status_code == 200:
try:
import io
image_data = response.content
image = Image.open(io.BytesIO(image_data))
thumb_image = ImageTk.PhotoImage(image)
if thumb_image:
self.photo_label.configure(image=thumb_image)
self.photo_label.image = thumb_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['status'] == 'running' else 'disabled'
self.stop_button.config(state=button_state)
def reveal_in_finder(self):
selected_item = self.tree.selection()[0] # Get the selected item
row_data = self.tree.item(selected_item) # Get the text of the selected item
job_id = row_data['values'][0]
job = next((d for d in self.job_cache if d.get('id') == job_id), None)
output_dir = os.path.dirname(job['output_path'])
subprocess.run(['open', output_dir])
def open_logs(self):
selected_item = self.tree.selection()[0] # Get the selected item
row_data = self.tree.item(selected_item) # Get the text of the selected item
job_id = row_data['values'][0]
job = next((d for d in self.job_cache if d.get('id') == job_id), None)
subprocess.run(['open', job['log_path']])
def mainloop(self):
self.root.mainloop()
def start_update_thread(self):
x = threading.Thread(target=self.__background_update)
x.daemon = True
x.start()
def __background_update(self):
while True:
self.update_jobs(clear_table=False)
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
if clear_table:
self.tree.delete(*self.tree.get_children())
self.job_cache.clear()
job_fetch = self.server_proxy.get_jobs()
if job_fetch:
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 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.tree.exists(job['id']):
update_row(self.tree, job['id'], new_values=values, tags=tags)
else:
self.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()