mirror of
https://github.com/blw1138/Zordon.git
synced 2026-06-09 13:39:24 -05:00
Compare commits
5 Commits
v0.8.0
...
250aa22557
| Author | SHA1 | Date | |
|---|---|---|---|
| 250aa22557 | |||
| 95341e815c | |||
| f3d469af53 | |||
| b8f025ccba | |||
| 076eebcdac |
@@ -83,18 +83,24 @@ The system works by:
|
||||
Jobs can be submitted via the desktop UI or programmatically via the API:
|
||||
|
||||
- **Via UI**: Use the desktop interface to upload project files, specify render settings, and queue jobs.
|
||||
- **Via API**: Send POST requests to `/api/jobs` with job configuration in JSON format.
|
||||
- **Via API**: Send `POST` requests to `/api/add_job` with job configuration in JSON format.
|
||||
|
||||
Example API request:
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/api/jobs \
|
||||
curl -X POST http://localhost:8080/api/add_job \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"engine": "blender",
|
||||
"project_path": "/path/to/project.blend",
|
||||
"output_path": "/path/to/output",
|
||||
"frames": "1-100",
|
||||
"settings": {"resolution": "1920x1080"}
|
||||
"name": "example-render",
|
||||
"engine_name": "blender",
|
||||
"local_path": "/path/to/project.blend",
|
||||
"output_path": "example-output",
|
||||
"start_frame": 1,
|
||||
"end_frame": 100,
|
||||
"args": {
|
||||
"export_format": "PNG",
|
||||
"resolution": [1920, 1080]
|
||||
},
|
||||
"enable_split_jobs": false
|
||||
}'
|
||||
```
|
||||
|
||||
@@ -103,9 +109,14 @@ curl -X POST http://localhost:5000/api/jobs \
|
||||
- **UI**: View job status, progress, logs, and worker availability in real-time.
|
||||
- **API Endpoints**:
|
||||
- `GET /api/jobs`: List all jobs
|
||||
- `GET /api/jobs/{id}`: Get job details
|
||||
- `DELETE /api/jobs/{id}`: Cancel a job
|
||||
- `GET /api/workers`: List connected workers
|
||||
- `GET /api/job/<job_id>`: Get job details
|
||||
- `POST /api/job/<job_id>/cancel`: Cancel a job
|
||||
- `POST /api/job/<job_id>/delete`: Delete a job
|
||||
- `GET /api/status`: Get server and queue status
|
||||
- `GET /api/engines`: List engine information
|
||||
|
||||
For the full endpoint reference, see [`docs/api.html`](docs/api.html) or
|
||||
[`docs/API.md`](docs/API.md).
|
||||
|
||||
#### Worker Management
|
||||
|
||||
@@ -193,4 +204,4 @@ Zordon is licensed under the MIT License. See the [LICENSE](LICENSE.txt) file fo
|
||||
|
||||
## Notice
|
||||
|
||||
This software is in beta and intended for casual/hobbyist use. Not recommended for mission-critical environments.
|
||||
This software is in beta and intended for casual/hobbyist use. Not recommended for mission-critical environments.
|
||||
|
||||
+67
-79
@@ -17,6 +17,7 @@ import psutil
|
||||
import yaml
|
||||
from flask import Flask, request, send_file, after_this_request, Response, redirect, url_for
|
||||
from sqlalchemy.orm.exc import DetachedInstanceError
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from src.api.job_import_handler import JobImportHandler
|
||||
from src.api.preview_manager import PreviewManager
|
||||
@@ -306,7 +307,7 @@ def add_job_handler():
|
||||
return 'unknown error', 500
|
||||
|
||||
|
||||
@server.get('/api/job/<job_id>/cancel')
|
||||
@server.post('/api/job/<job_id>/cancel')
|
||||
def cancel_job(job_id):
|
||||
if not request.args.get('confirm', False):
|
||||
return 'Confirmation required to cancel job', 400
|
||||
@@ -320,7 +321,7 @@ def cancel_job(job_id):
|
||||
return "Unknown error", 500
|
||||
|
||||
|
||||
@server.route('/api/job/<job_id>/delete', methods=['POST', 'GET'])
|
||||
@server.post('/api/job/<job_id>/delete')
|
||||
def delete_job(job_id):
|
||||
try:
|
||||
if not request.args.get("confirm", False):
|
||||
@@ -379,93 +380,38 @@ def get_engine_for_filename():
|
||||
return f"Error: cannot find a suitable engine for '{filename}'", 400
|
||||
return found_engine.name()
|
||||
|
||||
@server.get('/api/installed_engines')
|
||||
def get_installed_engines():
|
||||
result = {}
|
||||
for engine_class in EngineManager.supported_engines():
|
||||
data = EngineManager.all_version_data_for_engine(engine_class.name())
|
||||
if data:
|
||||
result[engine_class.name()] = data
|
||||
return result
|
||||
|
||||
|
||||
@server.get('/api/engine_info')
|
||||
def engine_info():
|
||||
def _validated_engine_response_type():
|
||||
response_type = request.args.get('response_type', 'standard')
|
||||
if response_type not in ['full', 'standard']:
|
||||
raise ValueError(f"Invalid response_type: {response_type}")
|
||||
|
||||
def process_engine(engine):
|
||||
try:
|
||||
# Get all installed versions of the engine
|
||||
installed_versions = EngineManager.all_version_data_for_engine(engine.name())
|
||||
if not installed_versions:
|
||||
return None
|
||||
|
||||
system_installed_versions = [v for v in installed_versions if v['type'] == 'system']
|
||||
install_path = system_installed_versions[0]['path'] if system_installed_versions else installed_versions[0]['path']
|
||||
|
||||
en = engine(install_path)
|
||||
engine_name = en.name()
|
||||
result = {
|
||||
engine_name: {
|
||||
'is_available': RenderQueue.is_available_for_job(engine_name),
|
||||
'versions': installed_versions
|
||||
}
|
||||
}
|
||||
|
||||
if response_type == 'full':
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
future_results = {
|
||||
'supported_extensions': executor.submit(en.supported_extensions),
|
||||
'supported_export_formats': executor.submit(en.get_output_formats),
|
||||
'system_info': executor.submit(en.system_info)
|
||||
}
|
||||
|
||||
for key, future in future_results.items():
|
||||
result[engine_name][key] = future.result()
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc(e)
|
||||
logger.error(f"Error fetching details for engine '{engine.name()}': {e}")
|
||||
return {}
|
||||
|
||||
engine_data = {}
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
futures = {executor.submit(process_engine, engine): engine.name() for engine in EngineManager.supported_engines()}
|
||||
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
result = future.result()
|
||||
if result:
|
||||
engine_data.update(result)
|
||||
|
||||
return engine_data
|
||||
return response_type
|
||||
|
||||
|
||||
@server.get('/api/<engine_name>/info')
|
||||
def get_engine_info(engine_name):
|
||||
def _engine_info_for_engine(engine_class, response_type='standard'):
|
||||
try:
|
||||
response_type = request.args.get('response_type', 'standard')
|
||||
# Get all installed versions of the engine
|
||||
installed_versions = EngineManager.all_version_data_for_engine(engine_name)
|
||||
installed_versions = EngineManager.all_version_data_for_engine(engine_class.name())
|
||||
if not installed_versions:
|
||||
return {}
|
||||
return None
|
||||
|
||||
result = { 'is_available': RenderQueue.is_available_for_job(engine_name),
|
||||
'versions': installed_versions
|
||||
}
|
||||
system_installed_versions = [v for v in installed_versions if v['type'] == 'system']
|
||||
install_path = (
|
||||
system_installed_versions[0]['path'] if system_installed_versions else installed_versions[0]['path']
|
||||
)
|
||||
|
||||
engine = engine_class(install_path)
|
||||
engine_name = engine.name()
|
||||
result = {
|
||||
'is_available': RenderQueue.is_available_for_job(engine_name),
|
||||
'versions': installed_versions
|
||||
}
|
||||
|
||||
if response_type == 'full':
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
engine_class = EngineManager.engine_class_with_name(engine_name)
|
||||
en = EngineManager.get_latest_engine_instance(engine_class)
|
||||
future_results = {
|
||||
'supported_extensions': executor.submit(en.supported_extensions),
|
||||
'supported_export_formats': executor.submit(en.get_output_formats),
|
||||
'system_info': executor.submit(en.system_info),
|
||||
'options': executor.submit(en.ui_options)
|
||||
'supported_extensions': executor.submit(engine.supported_extensions),
|
||||
'supported_export_formats': executor.submit(engine.get_output_formats),
|
||||
'system_info': executor.submit(engine.system_info),
|
||||
'options': executor.submit(engine.ui_options)
|
||||
}
|
||||
|
||||
for key, future in future_results.items():
|
||||
@@ -473,13 +419,52 @@ def get_engine_info(engine_name):
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching details for engine '{engine_class.name()}': {e}")
|
||||
return {}
|
||||
|
||||
|
||||
@server.get('/api/engines')
|
||||
def get_engines_info():
|
||||
response_type = _validated_engine_response_type()
|
||||
engine_data = {}
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
futures = {
|
||||
executor.submit(_engine_info_for_engine, engine, response_type): engine.name()
|
||||
for engine in EngineManager.supported_engines()
|
||||
}
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
result = future.result()
|
||||
if result:
|
||||
engine_data[futures[future]] = result
|
||||
|
||||
return engine_data
|
||||
|
||||
|
||||
@server.get('/api/engines/names')
|
||||
def get_engine_names():
|
||||
result = []
|
||||
for engine_class in EngineManager.supported_engines():
|
||||
data = EngineManager.all_version_data_for_engine(engine_class.name())
|
||||
if data:
|
||||
result.append(engine_class.name())
|
||||
return result
|
||||
|
||||
|
||||
@server.get('/api/engines/<engine_name>')
|
||||
def get_engine(engine_name):
|
||||
try:
|
||||
response_type = _validated_engine_response_type()
|
||||
engine_class = EngineManager.engine_class_with_name(engine_name)
|
||||
return _engine_info_for_engine(engine_class, response_type) or {}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching details for engine '{engine_name}': {e}")
|
||||
return {}
|
||||
|
||||
|
||||
@server.get('/api/<engine_name>/is_available')
|
||||
def is_engine_available(engine_name):
|
||||
def get_engine_availability(engine_name):
|
||||
return {'engine': engine_name, 'available': RenderQueue.is_available_for_job(engine_name),
|
||||
'cpu_count': int(psutil.cpu_count(logical=False)),
|
||||
'versions': EngineManager.all_version_data_for_engine(engine_name),
|
||||
@@ -632,6 +617,9 @@ def handle_404(error):
|
||||
|
||||
@server.errorhandler(Exception)
|
||||
def handle_general_error(general_error):
|
||||
if isinstance(general_error, HTTPException):
|
||||
return general_error.description, general_error.code
|
||||
|
||||
traceback.print_exception(type(general_error), general_error, general_error.__traceback__)
|
||||
err_msg = f"Server error: {general_error}"
|
||||
logger.error(err_msg)
|
||||
@@ -649,7 +637,7 @@ def detected_clients():
|
||||
return ZeroconfServer.found_hostnames()
|
||||
|
||||
|
||||
@server.get('/api/_debug/clear_history')
|
||||
@server.post('/api/_debug/clear_history')
|
||||
def clear_history():
|
||||
RenderQueue.clear_history()
|
||||
return 'success'
|
||||
|
||||
+22
-16
@@ -109,6 +109,11 @@ class RenderServerProxy:
|
||||
return requests.get(f'http://{self.hostname}:{self.port}/api/{payload}', timeout=timeout,
|
||||
headers={"X-API-Version": str(API_VERSION)})
|
||||
|
||||
def _post(self, payload, timeout=5, **kwargs):
|
||||
from src.api.api_server import API_VERSION
|
||||
return requests.post(f'http://{self.hostname}:{self.port}/api/{payload}', timeout=timeout,
|
||||
headers={"X-API-Version": str(API_VERSION)}, **kwargs)
|
||||
|
||||
# --------------------------------------------
|
||||
# Background Updates:
|
||||
# --------------------------------------------
|
||||
@@ -225,10 +230,10 @@ class RenderServerProxy:
|
||||
return response
|
||||
|
||||
def cancel_job(self, job_id, confirm=False):
|
||||
return self.request_data(f'job/{job_id}/cancel?confirm={confirm}')
|
||||
return self._post(f'job/{job_id}/cancel', params={'confirm': confirm})
|
||||
|
||||
def delete_job(self, job_id, confirm=False):
|
||||
return self.request_data(f'job/{job_id}/delete?confirm={confirm}')
|
||||
return self._post(f'job/{job_id}/delete', params={'confirm': confirm})
|
||||
|
||||
def send_subjob_update_notification(self, parent_id, subjob):
|
||||
"""
|
||||
@@ -252,15 +257,15 @@ class RenderServerProxy:
|
||||
response = self.request(f'engine_for_filename?filename={os.path.basename(filename)}', timeout)
|
||||
return response.text
|
||||
|
||||
def get_installed_engines(self, timeout=5):
|
||||
return self.request_data(f'installed_engines', timeout)
|
||||
|
||||
def is_engine_available(self, engine_name:str, timeout=5):
|
||||
def get_engine_availability(self, engine_name:str, timeout=5):
|
||||
return self.request_data(f'{engine_name}/is_available', timeout)
|
||||
|
||||
def get_all_engine_info(self, response_type='standard', timeout=5):
|
||||
def get_engine_names(self, timeout=5):
|
||||
return self.request_data('engines/names', timeout=timeout)
|
||||
|
||||
def get_engines(self, response_type='standard', timeout=5):
|
||||
"""
|
||||
Fetches all engine information from the server.
|
||||
Fetches engine information from the server.
|
||||
|
||||
Args:
|
||||
response_type (str, optional): Returns standard or full version of engine info
|
||||
@@ -269,10 +274,10 @@ class RenderServerProxy:
|
||||
Returns:
|
||||
dict: A dictionary containing the engine information.
|
||||
"""
|
||||
all_data = self.request_data(f"engine_info?response_type={response_type}", timeout=timeout)
|
||||
all_data = self.request_data(f'engines?response_type={response_type}', timeout=timeout)
|
||||
return all_data
|
||||
|
||||
def get_engine_info(self, engine_name:str, response_type='standard', timeout=5):
|
||||
def get_engine(self, engine_name:str, response_type='standard', timeout=5):
|
||||
"""
|
||||
Fetches specific engine information from the server.
|
||||
|
||||
@@ -284,22 +289,23 @@ class RenderServerProxy:
|
||||
Returns:
|
||||
dict: A dictionary containing the engine information.
|
||||
"""
|
||||
return self.request_data(f'{engine_name}/info?response_type={response_type}', timeout)
|
||||
return self.request_data(f'engines/{engine_name}?response_type={response_type}', timeout)
|
||||
|
||||
def delete_engine(self, engine_name:str, version:str, system_cpu=None):
|
||||
def delete_engine_download(self, engine_name:str, version:str, system_os=None, cpu=None):
|
||||
"""
|
||||
Sends a request to the server to delete a specific engine.
|
||||
Sends a request to the server to delete a specific engine download.
|
||||
|
||||
Args:
|
||||
engine_name (str): The name of the engine to delete.
|
||||
version (str): The version of the engine to delete.
|
||||
system_cpu (str, optional): The system CPU type. Defaults to None.
|
||||
system_os (str, optional): The system OS. Defaults to None.
|
||||
cpu (str, optional): The system CPU type. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Response: The response from the server.
|
||||
"""
|
||||
form_data = {'engine': engine_name, 'version': version, 'system_cpu': system_cpu}
|
||||
return requests.post(f'http://{self.hostname}:{self.port}/api/delete_engine', json=form_data)
|
||||
form_data = {'engine': engine_name, 'version': version, 'system_os': system_os, 'cpu': cpu}
|
||||
return self._post('delete_engine', json=form_data)
|
||||
|
||||
# --------------------------------------------
|
||||
# Download Files:
|
||||
|
||||
@@ -276,7 +276,7 @@ class DistributedJobManager:
|
||||
host_properties = ZeroconfServer.get_hostname_properties(hostname)
|
||||
if host_properties.get('api_version') == API_VERSION:
|
||||
if not system_os or (system_os and system_os == host_properties.get('system_os')):
|
||||
response = RenderServerProxy(hostname).is_engine_available(engine_name)
|
||||
response = RenderServerProxy(hostname).get_engine_availability(engine_name)
|
||||
if response and response.get('available', False):
|
||||
found_available_servers.append(response)
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ class NewRenderJobForm(QWidget):
|
||||
# Job / Server Data
|
||||
self.server_proxy = RenderServerProxy(socket.gethostname())
|
||||
self.project_info = None
|
||||
self.installed_engines = {}
|
||||
self.installed_engines = []
|
||||
self.preferred_engine = None
|
||||
|
||||
# Setup
|
||||
@@ -345,7 +345,7 @@ class NewRenderJobForm(QWidget):
|
||||
self.engine_version_combo.addItem('latest')
|
||||
self.file_format_combo.clear()
|
||||
if current_engine:
|
||||
engine_info = self.server_proxy.get_engine_info(current_engine, 'full', timeout=10)
|
||||
engine_info = self.server_proxy.get_engine(current_engine, 'full', timeout=10)
|
||||
self.current_engine_options = engine_info.get('options', [])
|
||||
if not engine_info:
|
||||
raise FileNotFoundError(f"Cannot get information about engine '{current_engine}'")
|
||||
@@ -404,7 +404,7 @@ class NewRenderJobForm(QWidget):
|
||||
"""Called by the GetProjectInfoWorker - Do not call directly."""
|
||||
try:
|
||||
|
||||
self.engine_type.addItems(self.installed_engines.keys())
|
||||
self.engine_type.addItems(self.installed_engines)
|
||||
self.engine_type.setCurrentText(self.preferred_engine)
|
||||
self.engine_changed()
|
||||
|
||||
@@ -633,7 +633,7 @@ class GetProjectInfoWorker(QThread):
|
||||
def run(self):
|
||||
try:
|
||||
# get the engine info and add them all to the ui
|
||||
self.window.installed_engines = self.window.server_proxy.get_installed_engines()
|
||||
self.window.installed_engines = self.window.server_proxy.get_engine_names()
|
||||
# select the best engine for the file type
|
||||
self.window.preferred_engine = self.window.server_proxy.get_engine_for_filename(self.project_path)
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ class EngineBrowserWindow(QMainWindow):
|
||||
def update_table(self):
|
||||
|
||||
def update_table_worker():
|
||||
raw_server_data = RenderServerProxy(self.hostname).get_all_engine_info()
|
||||
raw_server_data = RenderServerProxy(self.hostname).get_engines()
|
||||
if not raw_server_data:
|
||||
return
|
||||
|
||||
@@ -158,7 +158,9 @@ class EngineBrowserWindow(QMainWindow):
|
||||
if reply is not QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
|
||||
result = RenderServerProxy(self.hostname).delete_engine(engine_info['engine'], engine_info['version'])
|
||||
result = RenderServerProxy(self.hostname).delete_engine_download(
|
||||
engine_info['engine'], engine_info['version'], engine_info.get('system_os'), engine_info.get('cpu'),
|
||||
)
|
||||
if result.ok:
|
||||
self.update_table()
|
||||
else:
|
||||
|
||||
@@ -37,7 +37,7 @@ class GetEngineInfoWorker(QThread):
|
||||
self.parent = parent
|
||||
|
||||
def run(self):
|
||||
data = RenderServerProxy(socket.gethostname()).get_all_engine_info()
|
||||
data = RenderServerProxy(socket.gethostname()).get_engines()
|
||||
self.done.emit(data)
|
||||
|
||||
class SettingsWindow(QMainWindow):
|
||||
@@ -549,4 +549,4 @@ if __name__ == "__main__":
|
||||
app = QApplication([])
|
||||
window = SettingsWindow()
|
||||
window.show()
|
||||
app.exec()
|
||||
app.exec()
|
||||
|
||||
Reference in New Issue
Block a user