diff --git a/lib/job_server.py b/lib/job_server.py index a59cb6b..c6a6d4e 100755 --- a/lib/job_server.py +++ b/lib/job_server.py @@ -5,11 +5,12 @@ import logging import os import pathlib import shutil +import json2html from datetime import datetime from zipfile import ZipFile import requests -from flask import Flask, request, render_template, send_file, after_this_request, Response +from flask import Flask, request, render_template, send_file, after_this_request, Response, redirect, url_for from werkzeug.utils import secure_filename from lib.render_job import RenderJob @@ -17,18 +18,33 @@ from lib.render_queue import RenderQueue from utilities.render_worker import RenderWorkerFactory, string_to_status logger = logging.getLogger() -server = Flask(__name__) +server = Flask(__name__, template_folder='../templates') + + +@server.route('/') +@server.route('/index') +def index(): + return render_template('index.html', all_jobs=RenderQueue.job_queue, hostname=RenderQueue.host_name) + + +@server.route('/ui/job//full_details') +def job_detail(job_id): + found_job = RenderQueue.job_with_id(job_id) + if found_job: + table_html = json2html.json2html.convert(json=found_job.json()) + return render_template('details.html', detail_table=table_html) + return f'Cannot find job with ID {job_id}', 400 @server.get('/api/jobs') def jobs_json(): - return [x.json_safe_copy() for x in RenderQueue.job_queue] + return [x.json() for x in RenderQueue.job_queue] @server.get('/api/jobs/') def filtered_jobs_json(status_val): state = string_to_status(status_val) - jobs = [x.json_safe_copy() for x in RenderQueue.jobs_with_status(state)] + jobs = [x.json() for x in RenderQueue.jobs_with_status(state)] if jobs: return jobs else: @@ -39,7 +55,7 @@ def filtered_jobs_json(status_val): def get_job_status(job_id): found_job = RenderQueue.job_with_id(job_id) if found_job: - return found_job.json_safe_copy() + return found_job.json() else: return f'Cannot find job with ID {job_id}', 400 @@ -58,17 +74,16 @@ def get_job_logs(job_id): return f'Cannot find job with ID {job_id}', 400 -@server.get('/api/file_list/') +@server.get('/api/job//file_list') def get_file_list(job_id): found_job = RenderQueue.job_with_id(job_id) if found_job: - job_dir = os.path.dirname(found_job.worker.output_path) - return os.listdir(job_dir) + return '\n'.join(found_job.file_list()) else: return f'Cannot find job with ID {job_id}', 400 -@server.route('/api/download_all/') +@server.route('/api/job//download_all') def download_all(job_id): zip_filename = None @@ -83,7 +98,7 @@ def download_all(job_id): if found_job: output_dir = os.path.dirname(found_job.worker.output_path) if os.path.exists(output_dir): - zip_filename = pathlib.Path(found_job.worker.input_path).stem + '.zip' + zip_filename = os.path.join('/tmp', pathlib.Path(found_job.worker.input_path).stem + '.zip') with ZipFile(zip_filename, 'w') as zipObj: for f in os.listdir(output_dir): zipObj.write(filename=os.path.join(output_dir, f), @@ -144,7 +159,7 @@ def full_status(): @server.get('/api/snapshot') def snapshot(): server_status = RenderQueue.status() - server_jobs = [x.json_safe_copy() for x in RenderQueue.job_queue] + server_jobs = [x.json() for x in RenderQueue.job_queue] server_data = {'status': server_status, 'jobs': server_jobs, 'timestamp': datetime.now().isoformat()} return server_data @@ -229,7 +244,7 @@ def add_job(job_params, remove_job_dir_on_failure=False): render_job = RenderJob(renderer, input_path, output_path, args, priority, job_owner, client, notify=False, custom_id=custom_id, name=name) RenderQueue.add_to_render_queue(render_job, force_start=force_start) - return render_job.json_safe_copy() + return render_job.json() except Exception as e: err_msg = f"Error creating job: {str(e)}" logger.exception(err_msg) @@ -272,22 +287,51 @@ def add_job(job_params, remove_job_dir_on_failure=False): return {'error': err_msg, 'code': 400} -@server.get('/api/cancel_job') -def cancel_job(): - job_id = request.args.get('id', None) - confirm = request.args.get('confirm', False) - if not job_id: - return 'job id not found', 400 - elif not confirm: - return 'confirmation required', 400 +@server.get('/api/job//cancel') +def cancel_job(job_id): + found_job = RenderQueue.job_with_id(job_id) + if not found_job: + return f'Cannot find job with ID {job_id}', 400 + elif not request.args.get('confirm', False): + return 'Confirmation required to cancel job', 400 + + if RenderQueue.cancel_job(found_job): + return redirect(url_for('index')) else: - found = [x for x in RenderQueue.job_queue if x.id == job_id] - if len(found) > 1: - return f'multiple jobs found for ID {job_id}', 400 - elif found: - success = RenderQueue.cancel_job(found[0]) - return success - return 'job not found', 400 + return "Unknown error", 500 + + +@server.route('/api/job//delete', methods=['POST', 'GET']) +def delete_job(job_id): + try: + found_job = RenderQueue.job_with_id(job_id) + if not found_job: + return f'Cannot find job with ID {job_id}', 400 + elif not request.args.get('confirm', False): + return 'Confirmation required to delete job', 400 + + # First, remove all render files and logs + files_to_delete = found_job.file_list() + files_to_delete.append(found_job.log_path()) + for d in files_to_delete: + if os.path.exists(d): + os.remove(d) + + # Check if we can remove the 'output' directory + output_dir = os.path.dirname(files_to_delete[0]) + if server.config['UPLOAD_FOLDER'] in output_dir and os.path.exists(output_dir): + shutil.rmtree(output_dir) + + # See if we own the input file (i.e. was it uploaded) + input_dir = os.path.dirname(found_job.worker.input_path) + if server.config['UPLOAD_FOLDER'] in input_dir and os.path.exists(input_dir): + shutil.rmtree(input_dir) + + RenderQueue.delete_job(found_job) + return redirect(url_for('index')) + + except Exception as e: + return "Unknown error", 500 @server.get('/api/clear_history') @@ -313,11 +357,6 @@ def renderer_info(): return renderer_data -@server.route('/') -def default(): - return "Server running" - - @server.route('/upload') def upload_file_page(): return render_template('upload.html', render_clients=RenderQueue.render_clients, diff --git a/lib/render_job.py b/lib/render_job.py index 0270b2e..e037bc1 100644 --- a/lib/render_job.py +++ b/lib/render_job.py @@ -1,3 +1,4 @@ +import glob import hashlib import json import logging @@ -38,7 +39,7 @@ class RenderJob: return hashlib.md5(open(self.worker.input_path, 'rb').read()).hexdigest() return None - def json_safe_copy(self): + def json(self): """Converts RenderJob into JSON-friendly dict""" import numbers job_dict = None @@ -97,6 +98,13 @@ class RenderJob: def frame_count(self): return self.worker.total_frames + def file_list(self): + job_dir = os.path.dirname(self.worker.output_path) + return glob.glob(os.path.join(job_dir, '*')) + + def log_path(self): + return self.worker.log_path + @classmethod def generate_id(cls): return str(uuid.uuid4()).split('-')[0] \ No newline at end of file diff --git a/lib/render_queue.py b/lib/render_queue.py index 75c7abd..d5cf502 100755 --- a/lib/render_queue.py +++ b/lib/render_queue.py @@ -126,7 +126,7 @@ class RenderQueue: try: logger.debug("Saving Render History") output = {'timestamp': datetime.now().isoformat(), - 'jobs': [j.json_safe_copy() for j in cls.job_queue], + 'jobs': [j.json() for j in cls.job_queue], 'clients': cls.render_clients} output_path = json_path or JSON_FILE with open(output_path, 'w') as f: @@ -167,9 +167,16 @@ class RenderQueue: @classmethod def cancel_job(cls, job): - logger.info('Cancelling job ID: {}'.format(job.id)) + logger.info(f'Cancelling job ID: {job.id}') job.stop() - return job.render_status == RenderStatus.CANCELLED + return job.render_status() == RenderStatus.CANCELLED + + @classmethod + def delete_job(cls, job): + logger.info(f"Deleting job ID: {job.id}") + job.stop() + cls.job_queue.remove(job) + return True @classmethod def renderer_instances(cls): diff --git a/requirements.txt b/requirements.txt index 5b370eb..6862f32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ rich==12.6.0 ffmpeg-python Werkzeug~=2.2.2 tkinterdnd2~=0.3.0 -future~=0.18.2 \ No newline at end of file +future~=0.18.2 +json2html~=1.3.0 \ No newline at end of file diff --git a/start_server.py b/start_server.py index 9dadec6..40cfd05 100755 --- a/start_server.py +++ b/start_server.py @@ -49,7 +49,7 @@ def start_server(background_thread=False): server_thread.join() else: server.run(host='0.0.0.0', port=RenderQueue.port, debug=config.get('flask_debug_enable', False), - use_reloader=False) + use_reloader=True, threaded=True) if __name__ == '__main__': diff --git a/templates/details.html b/templates/details.html new file mode 100644 index 0000000..a1c8bce --- /dev/null +++ b/templates/details.html @@ -0,0 +1,32 @@ + + + + + + Zordon Dashboard + + + + + + + +
+{{detail_table|safe}} +
+ + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..823c9f7 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,127 @@ + + + + + + Zordon Dashboard + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + {% for job in all_jobs %} + + + + + + + + + + + + + + {% endfor %} +
PreviewNameRendererPriorityStatusTime ElapsedFrame CountClientCommands
Image Here{{job.name}}{{job.renderer}}-{{job.worker.renderer_version}}{{job.priority}}{{job.render_status().value}}{{job.time_elapsed()}}{{job.frame_count()}}{{job.client}} +
+ + + {% if job.render_status().value in ['running', 'scheduled', 'not_started']: %} + + {% elif job.render_status().value == 'completed': %} + + {% endif %} + +
+
+
+ + + \ No newline at end of file