From 93b42f2717d0ffed62b5f7548afaa18c1b8ecfd1 Mon Sep 17 00:00:00 2001 From: Brett Williams Date: Wed, 31 May 2023 17:20:00 -0500 Subject: [PATCH 1/7] New client work --- dashboard.py | 2 +- lib/client/client.py | 118 ++++++++++++++++++ .../client/new_job_window.py | 35 ++---- lib/client/server_proxy.py | 54 ++++++++ 4 files changed, 183 insertions(+), 26 deletions(-) create mode 100644 lib/client/client.py rename scheduler_gui.py => lib/client/new_job_window.py (92%) create mode 100644 lib/client/server_proxy.py diff --git a/dashboard.py b/dashboard.py index 6320193..b1f08c7 100755 --- a/dashboard.py +++ b/dashboard.py @@ -30,7 +30,7 @@ status_colors = {RenderStatus.ERROR: "red", RenderStatus.CANCELLED: 'orange1', R RenderStatus.RUNNING: 'cyan'} categories = [RenderStatus.RUNNING, RenderStatus.ERROR, RenderStatus.NOT_STARTED, RenderStatus.SCHEDULED, - RenderStatus.COMPLETED, RenderStatus.CANCELLED] + RenderStatus.COMPLETED, RenderStatus.CANCELLED, RenderStatus.UNDEFINED] renderer_colors = {'ffmpeg': '[magenta]', 'blender': '[orange1]', 'aerender': '[purple]'} diff --git a/lib/client/client.py b/lib/client/client.py new file mode 100644 index 0000000..53890ed --- /dev/null +++ b/lib/client/client.py @@ -0,0 +1,118 @@ +import requests +import tkinter as tk +import threading +import time +import os +from tkinter import ttk +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)) + + +class ZordonClient: + + def __init__(self): + + # Create a Treeview widget + self.root = tk.Tk() + self.tree = ttk.Treeview(self.root, show="headings") + self.server_proxy = RenderServerProxy(hostname='localhost') + self.job_cache = [] + + # Define the columns + 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=False) + + new_job_button = tk.Button(self.root, text="New Job", command=self.show_new_job_window) + new_job_button.pack() + + # Start the Tkinter event loop + self.root.geometry("500x600+300+300") + self.root.maxsize(width=2000, height=1200) + self.root.minsize(width=600, height=600) + + make_sortable(self.tree) + self.start_update_thread() + + 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): + for item in tree.get_children(): + values = tree.item(item, "values") + if values[0] == id: + tree.item(item, values=new_values) + break + if clear_table: + self.tree.delete(*self.tree.get_children()) + self.job_cache = self.server_proxy.get_jobs() + all_jobs = self.job_cache + for job in all_jobs: + display_status = job['status'] if job['status'] != 'running' else job['percent_complete'] + 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) + else: + self.tree.insert("", tk.END, iid=job['id'], values=values) + + 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, False) + x = NewJobWindow(parent=new_window, hostname=self.server_proxy.hostname) + x.pack() + + +if __name__ == '__main__': + x = ZordonClient() + x.mainloop() diff --git a/scheduler_gui.py b/lib/client/new_job_window.py similarity index 92% rename from scheduler_gui.py rename to lib/client/new_job_window.py index 33c00ad..87f273e 100755 --- a/scheduler_gui.py +++ b/lib/client/new_job_window.py @@ -13,7 +13,7 @@ import psutil import requests from lib.render_workers.blender_worker import Blender -from lib.utilities.server_helper import post_job_to_server +from server_proxy import RenderServerProxy logger = logging.getLogger() @@ -61,12 +61,12 @@ class ChecklistBox(Frame): return values -class ScheduleJob(Frame): +class NewJobWindow(Frame): - def __init__(self): - super().__init__() + def __init__(self, parent=None, hostname=None): + super().__init__(parent) - self.server_hostname = None + self.server_proxy = RenderServerProxy(hostname=hostname) self.chosen_file = None self.clients = [] self.presets = {} @@ -76,9 +76,6 @@ class ScheduleJob(Frame): self.master.title("Schedule Job") self.pack(fill=BOTH, expand=True) - self.server_button = Button(self, text="", width=6, command=self.request_new_hostname) - self.server_button.pack(fill=X, padx=5, expand=False) - # project frame job_frame = LabelFrame(self, text="Job Settings") job_frame.pack(fill=X, padx=5, pady=5) @@ -156,24 +153,12 @@ class ScheduleJob(Frame): self.custom_args_entry = None self.submit_frame = None - if os.path.exists(prefs_name): - with open(prefs_name, 'r') as file: - hostname = file.read() - server_data = request_data(hostname, 'status', timeout=server_setup_timeout) - if server_data: - self.set_hostname(hostname) - if not self.server_hostname: - server_data = request_data('localhost', 'status', timeout=server_setup_timeout) - if server_data: - self.set_hostname(server_data['host_name']) - else: - self.request_new_hostname() self.fetch_server_data() def fetch_server_data(self): - self.clients = request_data(self.server_hostname, 'clients', timeout=3) or [] - self.renderer_info = request_data(self.server_hostname, 'renderer_info', timeout=3) or {} - self.presets = request_data(self.server_hostname, 'presets', timeout=3) or {} + self.clients = self.server_proxy.request_data('clients', timeout=3) or [] + 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 clients self.client_combo['values'] = self.clients @@ -388,7 +373,7 @@ class ScheduleJob(Frame): # Submit to server job_list = job_list or [job_json] - result = post_job_to_server(input_path=input_path, job_list=job_list, hostname=client) + result = self.server_proxy.post_job_to_server(input_path=input_path, job_list=job_list) if result.ok: messagebox.showinfo("Success", "Job successfully submitted to server.") else: @@ -405,7 +390,7 @@ def main(): root.geometry("500x600+300+300") root.maxsize(width=1000, height=2000) root.minsize(width=600, height=600) - app = ScheduleJob() + app = NewJobWindow(root) root.mainloop() diff --git a/lib/client/server_proxy.py b/lib/client/server_proxy.py new file mode 100644 index 0000000..91fc3c5 --- /dev/null +++ b/lib/client/server_proxy.py @@ -0,0 +1,54 @@ +import os +import json +import requests +from lib.render_workers.base_worker import RenderStatus + +status_colors = {RenderStatus.ERROR: "red", RenderStatus.CANCELLED: 'orange1', RenderStatus.COMPLETED: 'green', + RenderStatus.NOT_STARTED: "yellow", RenderStatus.SCHEDULED: 'purple', + RenderStatus.RUNNING: 'cyan'} + +categories = [RenderStatus.RUNNING, RenderStatus.ERROR, RenderStatus.NOT_STARTED, RenderStatus.SCHEDULED, + RenderStatus.COMPLETED, RenderStatus.CANCELLED, RenderStatus.UNDEFINED] + + +class RenderServerProxy: + + def __init__(self, hostname=None, server_port="8080"): + self.hostname = hostname + self.port = server_port + self.fetched_status_data = None + + def connect(self): + status = self.request_data('status') + return status + + def request_data(self, payload, timeout=5): + try: + req = requests.get(f'http://{self.hostname}:{self.port}/api/{payload}', timeout=timeout) + if req.ok: + return req.json() + except Exception as e: + pass + return None + + def get_jobs(self): + all_jobs = self.request_data('jobs') + sorted_jobs = [] + if all_jobs: + for status_category in categories: + found_jobs = [x for x in all_jobs if x['status'] == status_category.value] + if found_jobs: + sorted_jobs.extend(found_jobs) + return sorted_jobs + + def get_data(self, timeout=5): + all_data = self.request_data('full_status', timeout=timeout) + return all_data + + def post_job_to_server(self, input_path, job_list): + # Pack job data and submit to server + job_files = {'file': (os.path.basename(input_path), open(input_path, 'rb'), 'application/octet-stream'), + 'json': (None, json.dumps(job_list), 'application/json')} + + req = requests.post(f'http://{self.hostname}:{self.port}/api/add_job', files=job_files) + return req \ No newline at end of file From 4cb1bff76e0773137968fd16dac49c1609ccfd1e Mon Sep 17 00:00:00 2001 From: Brett Williams Date: Wed, 31 May 2023 20:14:18 -0500 Subject: [PATCH 2/7] Added preview image and action buttons to UI --- lib/client/client.py | 98 +++++++++++++++++++++++++++---- lib/client/new_job_window.py | 2 +- lib/render_workers/base_worker.py | 3 +- 3 files changed, 90 insertions(+), 13 deletions(-) diff --git a/lib/client/client.py b/lib/client/client.py index 53890ed..dac5244 100644 --- a/lib/client/client.py +++ b/lib/client/client.py @@ -1,9 +1,12 @@ +import subprocess + import requests import tkinter as tk import threading import time import os from tkinter import ttk +from PIL import Image, ImageTk from new_job_window import NewJobWindow from server_proxy import RenderServerProxy @@ -36,11 +39,17 @@ class ZordonClient: # Create a Treeview widget self.root = tk.Tk() - self.tree = ttk.Treeview(self.root, show="headings") + self.root.title("Zordon Render Client") self.server_proxy = RenderServerProxy(hostname='localhost') self.job_cache = [] - # Define the columns + # Setup photo preview + self.photo_label = tk.Label(self.root, width=400, height=300) + self.photo_label.pack() + + # Setup the Tree + self.tree = ttk.Treeview(self.root, show="headings") + self.tree.bind("<>", self.on_row_select) self.tree["columns"] = ("id", "Name", "Renderer", "Priority", "Status", "Time Elapsed", "Frames") # Format the columns @@ -57,10 +66,22 @@ class ZordonClient: self.tree.heading(name, text=name) # Pack the Treeview widget - self.tree.pack(fill=tk.BOTH, expand=False) + self.tree.pack(fill=tk.BOTH, expand=True) - new_job_button = tk.Button(self.root, text="New Job", command=self.show_new_job_window) - new_job_button.pack() + button_frame = tk.Frame(self.root) + button_frame.pack(pady=10) + + # 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 Job", command=self.stop_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) + 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") @@ -68,8 +89,57 @@ class ZordonClient: self.root.minsize(width=600, height=600) 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 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 on_row_select(self, event): + 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' + response = requests.get(thumb_url) + if response.status_code == 200: + 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 + + # 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() @@ -93,12 +163,18 @@ class ZordonClient: break if clear_table: self.tree.delete(*self.tree.get_children()) - self.job_cache = self.server_proxy.get_jobs() - all_jobs = self.job_cache - for job in all_jobs: + 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 job['percent_complete'] - 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']) + 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) else: @@ -108,7 +184,7 @@ class ZordonClient: new_window = tk.Toplevel(self.root) new_window.title("New Window") new_window.geometry("500x600+300+300") - new_window.resizable(False, False) + new_window.resizable(False, height=True) x = NewJobWindow(parent=new_window, hostname=self.server_proxy.hostname) x.pack() diff --git a/lib/client/new_job_window.py b/lib/client/new_job_window.py index 87f273e..b2b3d77 100755 --- a/lib/client/new_job_window.py +++ b/lib/client/new_job_window.py @@ -73,7 +73,7 @@ class NewJobWindow(Frame): self.renderer_info = {} self.priority = IntVar(value=2) - self.master.title("Schedule Job") + self.master.title("New Job") self.pack(fill=BOTH, expand=True) # project frame diff --git a/lib/render_workers/base_worker.py b/lib/render_workers/base_worker.py index ef4bf9f..a2ed0c0 100644 --- a/lib/render_workers/base_worker.py +++ b/lib/render_workers/base_worker.py @@ -324,7 +324,8 @@ class BaseRenderWorker(Base): 'renderer_version': self.renderer_version, 'errors': getattr(self, 'errors', None), 'total_frames': self.total_frames, - 'last_output': getattr(self, 'last_output', None) + 'last_output': getattr(self, 'last_output', None), + 'log_path': self.log_path() } # convert to json and back to auto-convert dates to iso format From f039bfae357b880c80af78312e07271852b225c3 Mon Sep 17 00:00:00 2001 From: Brett Williams Date: Wed, 31 May 2023 22:17:47 -0500 Subject: [PATCH 3/7] Misc UI work. Colored jobs. Error handling. --- lib/client/client.py | 60 ++++++++++++++++++++++++---------------- lib/server/job_server.py | 6 +++- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/lib/client/client.py b/lib/client/client.py index dac5244..87ebd26 100644 --- a/lib/client/client.py +++ b/lib/client/client.py @@ -49,6 +49,7 @@ class ZordonClient: # Setup the Tree self.tree = ttk.Treeview(self.root, show="headings") + self.tree.tag_configure('running', background='lawn green', font=('', 0, 'bold')) self.tree.bind("<>", self.on_row_select) self.tree["columns"] = ("id", "Name", "Renderer", "Priority", "Status", "Time Elapsed", "Frames") @@ -87,7 +88,6 @@ class ZordonClient: self.root.geometry("500x600+300+300") self.root.maxsize(width=2000, height=1200) self.root.minsize(width=600, height=600) - make_sortable(self.tree) self.update_jobs() @@ -95,6 +95,11 @@ class ZordonClient: 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 @@ -102,26 +107,30 @@ class ZordonClient: self.server_proxy.request_data(f'job/{job_id}/cancel?confirm=true') def on_row_select(self, event): - 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] + 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' - response = requests.get(thumb_url) - if response.status_code == 200: - 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 + # update thumb + thumb_url = f'http://{self.server_proxy.hostname}:{self.server_proxy.port}/ui/job/{job_id}/thumbnail' + 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) + # 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 @@ -155,19 +164,22 @@ class ZordonClient: def update_jobs(self, clear_table=False): - def update_row(tree, id, new_values): + 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) + 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 job['percent_complete'] + 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'], @@ -176,9 +188,9 @@ class ZordonClient: job['time_elapsed'], job['total_frames']) if self.tree.exists(job['id']): - update_row(self.tree, job['id'], new_values=values) + update_row(self.tree, job['id'], new_values=values, tags=tags) else: - self.tree.insert("", tk.END, iid=job['id'], values=values) + self.tree.insert("", tk.END, iid=job['id'], values=values, tags=tags) def show_new_job_window(self): new_window = tk.Toplevel(self.root) diff --git a/lib/server/job_server.py b/lib/server/job_server.py index d2ae020..7dbaeb6 100755 --- a/lib/server/job_server.py +++ b/lib/server/job_server.py @@ -110,7 +110,11 @@ def get_job_file(job_id, filename): @server.get('/api/jobs') def jobs_json(): - return [x.json() for x in RenderQueue.all_jobs()] + try: + return [x.json() for x in RenderQueue.all_jobs()] + except Exception as e: + logger.exception(f"Exception fetching all_jobs: {e}") + return [], 500 @server.get('/api/jobs/') From 8c19a50d601c3e7d2be6046a5e38f33031e48e7b Mon Sep 17 00:00:00 2001 From: Brett Williams Date: Wed, 31 May 2023 23:11:37 -0500 Subject: [PATCH 4/7] Get big thumbnails now --- lib/client/client.py | 23 +++++++++++++++++++---- lib/server/job_server.py | 10 ++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/client/client.py b/lib/client/client.py index 87ebd26..91e82d2 100644 --- a/lib/client/client.py +++ b/lib/client/client.py @@ -44,8 +44,23 @@ class ZordonClient: self.job_cache = [] # Setup photo preview - self.photo_label = tk.Label(self.root, width=400, height=300) - self.photo_label.pack() + 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.X, 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("<>", self.server_picked) + self.server_combo['values'] = servers + self.server_combo.current(0) # Setup the Tree self.tree = ttk.Treeview(self.root, show="headings") @@ -87,7 +102,7 @@ class ZordonClient: # Start the Tkinter event loop self.root.geometry("500x600+300+300") self.root.maxsize(width=2000, height=1200) - self.root.minsize(width=600, height=600) + self.root.minsize(width=900, height=1000) make_sortable(self.tree) self.update_jobs() @@ -113,7 +128,7 @@ class ZordonClient: 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' + 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: diff --git a/lib/server/job_server.py b/lib/server/job_server.py index 7dbaeb6..8917d00 100755 --- a/lib/server/job_server.py +++ b/lib/server/job_server.py @@ -70,17 +70,27 @@ def job_detail(job_id): @server.route('/ui/job//thumbnail') def job_thumbnail(job_id): + big_thumb = request.args.get('size', False) == "big" found_job = RenderQueue.job_with_id(job_id, none_ok=True) if found_job: os.makedirs(server.config['THUMBS_FOLDER'], exist_ok=True) thumb_video_path = os.path.join(server.config['THUMBS_FOLDER'], found_job.id + '.mp4') thumb_image_path = os.path.join(server.config['THUMBS_FOLDER'], found_job.id + '.jpg') + big_video_path = os.path.join(server.config['THUMBS_FOLDER'], found_job.id + '_big.mp4') + big_image_path = os.path.join(server.config['THUMBS_FOLDER'], found_job.id + '_big.jpg') if not os.path.exists(thumb_video_path) and not os.path.exists(thumb_video_path + '_IN-PROGRESS') and \ found_job.status not in [RenderStatus.CANCELLED, RenderStatus.ERROR]: generate_thumbnail_for_job(found_job, thumb_video_path, thumb_image_path, max_width=240) + if big_thumb and os.path.exists(big_video_path) and not os.path.exists(big_video_path + '_IN-PROGRESS'): + return send_file(big_video_path, mimetype="video/mp4") + elif big_thumb and os.path.exists(big_image_path): + return send_file(big_image_path, mimetype='image/jpeg') + elif not os.path.exists(big_video_path) and not os.path.exists(big_image_path + '_IN-PROGRESS') and \ + found_job.status not in [RenderStatus.CANCELLED, RenderStatus.ERROR]: + generate_thumbnail_for_job(found_job, big_video_path, big_image_path, max_width=800) if os.path.exists(thumb_video_path) and not os.path.exists(thumb_video_path + '_IN-PROGRESS'): return send_file(thumb_video_path, mimetype="video/mp4") elif os.path.exists(thumb_image_path): From 985e89432b38ac88a14c0b463bf27a624ed3fbf5 Mon Sep 17 00:00:00 2001 From: Brett Williams Date: Wed, 31 May 2023 23:14:21 -0500 Subject: [PATCH 5/7] Server picker wip --- lib/client/client.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/client/client.py b/lib/client/client.py index 91e82d2..17e9833 100644 --- a/lib/client/client.py +++ b/lib/client/client.py @@ -4,6 +4,7 @@ import requests import tkinter as tk import threading import time +import socket import os from tkinter import ttk from PIL import Image, ImageTk @@ -33,14 +34,21 @@ def make_sortable(tree): tree.heading(col, text=col, command=lambda c=col: sort_column(tree, c)) +def available_servers(): + return [socket.gethostname(), 'deathstar.local'] + + class ZordonClient: def __init__(self): + servers = available_servers() + # Create a Treeview widget self.root = tk.Tk() self.root.title("Zordon Render Client") - self.server_proxy = RenderServerProxy(hostname='localhost') + self.local_host = socket.gethostname() + self.server_proxy = RenderServerProxy(hostname=servers[0]) self.job_cache = [] # Setup photo preview @@ -50,7 +58,7 @@ class ZordonClient: self.photo_label.pack(fill=tk.BOTH, expand=True) server_frame = tk.LabelFrame(self.root, text="Server") - server_frame.pack(fill=tk.X, pady=5, padx=5, expand=True) + 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:") @@ -63,7 +71,7 @@ class ZordonClient: self.server_combo.current(0) # Setup the Tree - self.tree = ttk.Treeview(self.root, show="headings") + self.tree = ttk.Treeview(server_frame, show="headings", height=20) self.tree.tag_configure('running', background='lawn green', font=('', 0, 'bold')) self.tree.bind("<>", self.on_row_select) self.tree["columns"] = ("id", "Name", "Renderer", "Priority", "Status", "Time Elapsed", "Frames") @@ -84,8 +92,8 @@ class ZordonClient: # Pack the Treeview widget self.tree.pack(fill=tk.BOTH, expand=True) - button_frame = tk.Frame(self.root) - button_frame.pack(pady=10) + 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) From 998be45f3fa16e4da9952da51773dd258ba37363 Mon Sep 17 00:00:00 2001 From: Brett Williams Date: Wed, 31 May 2023 23:17:00 -0500 Subject: [PATCH 6/7] Take out temp server --- lib/client/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/client/client.py b/lib/client/client.py index 17e9833..c41cecc 100644 --- a/lib/client/client.py +++ b/lib/client/client.py @@ -35,7 +35,7 @@ def make_sortable(tree): def available_servers(): - return [socket.gethostname(), 'deathstar.local'] + return [socket.gethostname()] class ZordonClient: From c5c9165733b9cf6d632270e610cba1f961e60600 Mon Sep 17 00:00:00 2001 From: Brett Williams Date: Wed, 31 May 2023 23:29:04 -0500 Subject: [PATCH 7/7] Added delete button --- lib/client/client.py | 17 +++++++++++++++-- lib/render_workers/base_worker.py | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/client/client.py b/lib/client/client.py index c41cecc..63f4a8d 100644 --- a/lib/client/client.py +++ b/lib/client/client.py @@ -6,7 +6,7 @@ import threading import time import socket import os -from tkinter import ttk +from tkinter import ttk, messagebox from PIL import Image, ImageTk from new_job_window import NewJobWindow from server_proxy import RenderServerProxy @@ -98,11 +98,13 @@ class ZordonClient: # 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 Job", command=self.stop_job) + 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) @@ -129,6 +131,17 @@ class ZordonClient: 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 diff --git a/lib/render_workers/base_worker.py b/lib/render_workers/base_worker.py index a2ed0c0..fd27b15 100644 --- a/lib/render_workers/base_worker.py +++ b/lib/render_workers/base_worker.py @@ -244,7 +244,7 @@ class BaseRenderWorker(Base): self.stop(is_error=True) def stop(self, is_error=False): - if self.__process: + if hasattr(self, '__process'): try: if self.status in [RenderStatus.RUNNING, RenderStatus.NOT_STARTED, RenderStatus.SCHEDULED]: if is_error: