From eb9e719f47dc108b9ac5f43047eec1bc11b28411 Mon Sep 17 00:00:00 2001 From: Brett Williams Date: Wed, 26 Oct 2022 18:27:13 -0700 Subject: [PATCH] Add ability to get file list and download files from project output --- lib/render_job.py | 2 +- lib/render_queue.py | 4 ++- requirements.txt | 4 ++- server.py | 57 ++++++++++++++++++++++++++++++------ utilities/aerender_worker.py | 6 ++-- utilities/blender_worker.py | 4 +-- utilities/ffmpeg_worker.py | 4 +-- utilities/render_worker.py | 18 ++++++------ 8 files changed, 71 insertions(+), 28 deletions(-) diff --git a/lib/render_job.py b/lib/render_job.py index 91474a2..da4feda 100644 --- a/lib/render_job.py +++ b/lib/render_job.py @@ -21,7 +21,7 @@ class RenderJob: self.date_created = datetime.now() self.scheduled_start = None self.renderer = render.renderer - self.name = os.path.basename(render.input) + '_' + self.date_created.isoformat() + self.name = os.path.basename(render.input_path) + '_' + self.date_created.isoformat() self.archived = False def render_status(self): diff --git a/lib/render_queue.py b/lib/render_queue.py index b96a025..d8e8678 100755 --- a/lib/render_queue.py +++ b/lib/render_queue.py @@ -92,7 +92,9 @@ class RenderQueue: for job in saved_state.get('jobs', []): # Identify renderer type and recreate Renderer object - job_render_object = RenderWorkerFactory.create_worker(job['renderer'], input_path=job['render']['input'], output_path=job['render']['output']) + job_render_object = RenderWorkerFactory.create_worker(job['renderer'], + input_path=job['render']['input_path'], + output_path=job['render']['output_path']) # Load Renderer values for key, val in job['render'].items(): diff --git a/requirements.txt b/requirements.txt index 92cd820..5b370eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,6 @@ PyYAML~=6.0 Flask==2.2.2 rich==12.6.0 ffmpeg-python -Werkzeug~=2.2.2 \ No newline at end of file +Werkzeug~=2.2.2 +tkinterdnd2~=0.3.0 +future~=0.18.2 \ No newline at end of file diff --git a/server.py b/server.py index 2bad2b1..af666d1 100755 --- a/server.py +++ b/server.py @@ -3,15 +3,17 @@ import json import logging import os +import pathlib import shutil import socket import threading import time from datetime import datetime +from zipfile import ZipFile import requests import yaml -from flask import Flask, request, render_template +from flask import Flask, request, render_template, send_file, after_this_request from werkzeug.utils import secure_filename from lib.render_job import RenderJob @@ -34,17 +36,53 @@ def filtered_jobs_json(status_val): if jobs: return jobs else: - return {'error', f'Cannot find jobs with status {status_val}'}, 400 + return f'Cannot find jobs with status {status_val}', 400 @app.get('/job_status/') def get_job_status(job_id): found_job = RenderQueue.job_with_id(job_id) if found_job: - logger.info("Founbd jobs") return found_job.json() else: - return {'error': f'Cannot find job with ID {job_id}'}, 400 + return f'Cannot find job with ID {job_id}', 400 + + +@app.get('/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.render.output_path) + return os.listdir(job_dir) + else: + return f'Cannot find job with ID {job_id}', 400 + + +@app.route('/download_all/') +def download_all(job_id): + + zip_filename = None + + @after_this_request + def clear_zip(response): + if zip_filename and os.path.exists(zip_filename): + os.remove(zip_filename) + return response + + found_job = RenderQueue.job_with_id(job_id) + if found_job: + output_dir = os.path.dirname(found_job.render.output_path) + if os.path.exists(output_dir): + zip_filename = pathlib.Path(found_job.render.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), + arcname=os.path.basename(f)) + return send_file(zip_filename, mimetype="zip", as_attachment=True, ) + else: + return f'Cannot find project files for job {job_id}', 500 + else: + return f'Cannot find job with ID {job_id}', 400 @app.post('/register_client') @@ -103,7 +141,6 @@ def snapshot(): @app.post('/add_job') def add_job(): - def remove_job_dir(): if job_dir and os.path.exists(job_dir): logger.debug(f"Removing job dir: {job_dir}") @@ -146,14 +183,17 @@ def add_job(): local_path = os.path.join(job_dir, secure_filename(uploaded_file.filename)) uploaded_file.save(local_path) input_path = local_path - output_path = os.path.join(job_dir, os.path.basename(output_path)) + output_dir = os.path.join(job_dir, 'output') + os.makedirs(output_dir, exist_ok=True) + output_path = os.path.join(output_dir, os.path.basename(output_path)) # local renders if client == RenderQueue.host_name: logger.info(f"Creating job locally - {input_path}") try: render_job = RenderWorkerFactory.create_worker(renderer, input_path, output_path, args) - except ValueError as e: + render_job.log_path = os.path.join(os.path.dirname(input_path), os.path.basename(input_path) + '.log') + except Exception as e: err_msg = f"Error creating job: {str(e)}" logger.exception(err_msg) remove_job_dir() @@ -268,7 +308,6 @@ def post_job_to_server(input_path, job_json, client, server_port=8080): def start_server(background_thread=False): - def eval_loop(delay_sec=1): while True: RenderQueue.evaluate_queue() @@ -311,4 +350,4 @@ def start_server(background_thread=False): if __name__ == '__main__': - start_server() \ No newline at end of file + start_server() diff --git a/utilities/aerender_worker.py b/utilities/aerender_worker.py index 50a51ba..e46bba1 100644 --- a/utilities/aerender_worker.py +++ b/utilities/aerender_worker.py @@ -75,15 +75,15 @@ class AERenderWorker(BaseRenderWorker): # } job = {'template': { - 'src': 'file://' + self.input, 'composition': self.comp.replace('"', ''), + 'src': 'file://' + self.input_path, 'composition': self.comp.replace('"', ''), 'settingsTemplate': self.render_settings.replace('"', ''), 'outputModule': self.omsettings.replace('"', ''), 'outputExt': 'mov'} } x = ['./nexrender-cli-macos', "'{}'".format(json.dumps(job))] else: logging.info('nexrender not found') - x = [aerender_path(), '-project', self.input, '-comp', self.comp, '-RStemplate', self.render_settings, - '-OMtemplate', self.omsettings, '-output', self.output] + x = [aerender_path(), '-project', self.input_path, '-comp', self.comp, '-RStemplate', self.render_settings, + '-OMtemplate', self.omsettings, '-output', self.output_path] return x def _parse_stdout(self, line): diff --git a/utilities/blender_worker.py b/utilities/blender_worker.py index 54a8d19..5cdd693 100644 --- a/utilities/blender_worker.py +++ b/utilities/blender_worker.py @@ -44,12 +44,12 @@ class BlenderRenderWorker(BaseRenderWorker): def _generate_subprocess(self): - cmd = [self.renderer_path(), '-b', self.input] + cmd = [self.renderer_path(), '-b', self.input_path] if self.camera: cmd.extend(['--python-expr', f"import bpy;bpy.context.scene.camera = bpy.data.objects['{self.camera}'];"]) - cmd.extend(['-E', self.engine, '-o', self.output, '-F', self.export_format]) + cmd.extend(['-E', self.engine, '-o', self.output_path, '-F', self.export_format]) # all frames or single cmd.extend(['-a'] if self.render_all_frames else ['-f', str(self.frame)]) diff --git a/utilities/ffmpeg_worker.py b/utilities/ffmpeg_worker.py index e544a68..f4d9099 100644 --- a/utilities/ffmpeg_worker.py +++ b/utilities/ffmpeg_worker.py @@ -41,10 +41,10 @@ class FFMPEGRenderWorker(BaseRenderWorker): def _generate_subprocess(self): - cmd = [self.renderer_path(), '-y', '-stats', '-i', self.input] + cmd = [self.renderer_path(), '-y', '-stats', '-i', self.input_path] if self.args: cmd.extend(self.args) - cmd.append(self.output) + cmd.append(self.output_path) return cmd diff --git a/utilities/render_worker.py b/utilities/render_worker.py index 30a8f7a..fa4eddc 100644 --- a/utilities/render_worker.py +++ b/utilities/render_worker.py @@ -45,8 +45,8 @@ class BaseRenderWorker(object): raise ValueError(err_meg) # Essential Info - self.input = input_path - self.output = output_path + self.input_path = input_path + self.output_path = output_path self.args = args or {} self.date_created = datetime.now() self.renderer_version = self.version() @@ -96,9 +96,9 @@ class BaseRenderWorker(object): def start(self): - if not os.path.exists(self.input): + if not os.path.exists(self.input_path): self.status = RenderStatus.ERROR - msg = 'Cannot find input path: {}'.format(self.input) + msg = 'Cannot find input path: {}'.format(self.input_path) logger.error(msg) self.errors.append(msg) return @@ -111,7 +111,7 @@ class BaseRenderWorker(object): return self.status = RenderStatus.RUNNING - logger.info('Starting {0} {1} Render for {2}'.format(self.renderer, self.version(), self.input)) + logger.info('Starting {0} {1} Render for {2}'.format(self.renderer, self.version(), self.input_path)) self.thread.start() def run(self): @@ -119,10 +119,10 @@ class BaseRenderWorker(object): # Setup logging try: if not self.log_path: - log_dir = os.path.join(os.path.dirname(self.input), 'logs') + log_dir = os.path.join(os.path.dirname(self.input_path), 'logs') if not os.path.exists(log_dir): os.makedirs(log_dir) - self.log_path = os.path.join(log_dir, os.path.basename(self.input)) + '.log' + self.log_path = os.path.join(log_dir, os.path.basename(self.input_path)) + '.log' logger.info('Logs saved in {}'.format(self.log_path)) except Exception as e: logger.error("Error setting up logging: {}".format(e)) @@ -141,7 +141,7 @@ class BaseRenderWorker(object): with open(self.log_path, "a") as f: - f.write("{3} - Starting {0} {1} Render for {2}\n".format(self.renderer, self.version(), self.input, + f.write("{3} - Starting {0} {1} Render for {2}\n".format(self.renderer, self.version(), self.input_path, self.start_time.isoformat())) f.write(f"Running command: {' '.join(subprocess_cmds)}\n") for c in io.TextIOWrapper(self.process.stdout, encoding="utf-8"): # or another encoding @@ -173,7 +173,7 @@ class BaseRenderWorker(object): f.write(message) if self.failed_attempts >= self.maximum_attempts and self.status is not RenderStatus.CANCELLED: - logger.error('{} Render of {} failed after {} attempts'.format(self.renderer, self.input, self.failed_attempts)) + logger.error('{} Render of {} failed after {} attempts'.format(self.renderer, self.input_path, self.failed_attempts)) self.status = RenderStatus.ERROR if not self.errors: self.errors = [self.last_output]