Compute render output path when evaluating job settings

Compute render output path when evaluating job settings, which is done
within the Flamenco add-on, instead of in the job compiler script. This
allows the UI to show the render path, rather than it only being known
after the job has been submitted.
This commit is contained in:
Sybren A. Stüvel 2022-03-15 16:56:44 +01:00
parent 7bfde1df0b
commit 09a476e11a
15 changed files with 287 additions and 108 deletions

View File

@ -1,8 +1,22 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# <pep8 compliant> # <pep8 compliant>
from typing import Optional, TYPE_CHECKING
from flamenco import job_submission
from flamenco.job_types_propgroup import JobTypePropertyGroup
import bpy import bpy
if TYPE_CHECKING:
from flamenco.manager.models import (
AvailableJobSetting as _AvailableJobSetting,
SubmittedJob as _SubmittedJob,
)
else:
_AvailableJobSetting = object
_SubmittedJob = object
class FLAMENCO_PT_job_submission(bpy.types.Panel): class FLAMENCO_PT_job_submission(bpy.types.Panel):
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
@ -10,6 +24,10 @@ class FLAMENCO_PT_job_submission(bpy.types.Panel):
bl_context = "output" bl_context = "output"
bl_label = "Flamenco 3" bl_label = "Flamenco 3"
# A temporary job can be constructed so that dynamic, read-only properties can be evaluated.
# This is only scoped to a single draw() call.
job: Optional[_SubmittedJob] = None
def draw(self, context: bpy.types.Context) -> None: def draw(self, context: bpy.types.Context) -> None:
from . import job_types from . import job_types
@ -24,14 +42,20 @@ class FLAMENCO_PT_job_submission(bpy.types.Panel):
row.prop(context.scene, "flamenco_job_type", text="") row.prop(context.scene, "flamenco_job_type", text="")
row.operator("flamenco.fetch_job_types", text="", icon="FILE_REFRESH") row.operator("flamenco.fetch_job_types", text="", icon="FILE_REFRESH")
layout.separator()
col = layout.column(align=True)
col.prop(context.scene, "flamenco_job_name", text="Job Name")
layout.separator()
self.draw_job_settings(context, layout.column(align=True)) self.draw_job_settings(context, layout.column(align=True))
layout.separator() layout.separator()
col = layout.column(align=True)
col.prop(context.scene, "flamenco_job_name", text="Job Name")
self.draw_flamenco_status(context, layout) self.draw_flamenco_status(context, layout)
self.job = None
def draw_job_settings( def draw_job_settings(
self, context: bpy.types.Context, layout: bpy.types.UILayout self, context: bpy.types.Context, layout: bpy.types.UILayout
) -> None: ) -> None:
@ -48,17 +72,47 @@ class FLAMENCO_PT_job_submission(bpy.types.Panel):
layout.label(text="Job Settings:") layout.label(text="Job Settings:")
layout.use_property_split = True layout.use_property_split = True
for setting in job_type.settings: for setting in job_type.settings:
if not setting.get("visible", True): self.draw_setting(context, layout, propgroup, setting)
continue
row = layout.row(align=True) def draw_setting(
row.prop(propgroup, setting.key) self,
setting_eval = setting.get("eval", "") context: bpy.types.Context,
if setting_eval: layout: bpy.types.UILayout,
props = row.operator( propgroup: JobTypePropertyGroup,
"flamenco.eval_setting", text="", icon="SCRIPTPLUGINS" setting: _AvailableJobSetting,
) ) -> None:
props.setting_key = setting.key if not setting.get("visible", True):
props.setting_eval = setting_eval return
if setting.get("editable", True):
self.draw_setting_editable(layout, propgroup, setting)
else:
self.draw_setting_readonly(context, layout, propgroup, setting)
def draw_setting_editable(
self,
layout: bpy.types.UILayout,
propgroup: JobTypePropertyGroup,
setting: _AvailableJobSetting,
) -> None:
row = layout.row(align=True)
row.prop(propgroup, setting.key)
setting_eval = setting.get("eval", "")
if not setting_eval:
return
props = row.operator("flamenco.eval_setting", text="", icon="SCRIPTPLUGINS")
props.setting_key = setting.key
props.setting_eval = setting_eval
def draw_setting_readonly(
self,
context: bpy.types.Context,
layout: bpy.types.UILayout,
propgroup: JobTypePropertyGroup,
setting: _AvailableJobSetting,
) -> None:
layout.prop(propgroup, setting.key)
def draw_flamenco_status( def draw_flamenco_status(
self, context: bpy.types.Context, layout: bpy.types.UILayout self, context: bpy.types.Context, layout: bpy.types.UILayout

View File

@ -83,7 +83,9 @@ def eval_hidden_settings(
setting_eval = setting.get("eval", "") setting_eval = setting.get("eval", "")
if setting_eval: if setting_eval:
value = JobTypePropertyGroup.eval_setting(context, setting_eval) value = JobTypePropertyGroup.eval_setting(
context, job, setting.key, setting_eval
)
elif "default" in setting: elif "default" in setting:
value = setting.default value = setting.default
else: else:

View File

@ -3,7 +3,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import logging import logging
from typing import TYPE_CHECKING, Callable, Optional, Any import pprint
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Optional, Any, Union
import bpy import bpy
@ -14,11 +16,38 @@ if TYPE_CHECKING:
AvailableJobType as _AvailableJobType, AvailableJobType as _AvailableJobType,
AvailableJobSetting as _AvailableJobSetting, AvailableJobSetting as _AvailableJobSetting,
JobSettings as _JobSettings, JobSettings as _JobSettings,
SubmittedJob as _SubmittedJob,
) )
else: else:
_AvailableJobType = object _AvailableJobType = object
_AvailableJobSetting = object _AvailableJobSetting = object
_JobSettings = object _JobSettings = object
_SubmittedJob = object
class SettingEvalError(Exception):
"""Raised when the evaluation of a setting fails."""
def __init__(
self,
setting_key: str,
setting_eval: str,
eval_locals: dict[str, Any],
exception: Exception,
) -> None:
self.setting_key = setting_key
self.setting_eval = setting_eval
self.locals = eval_locals.copy()
self.exception = exception
print("Evaluation error of setting %r:" % setting_key)
print("Expression: %s" % setting_eval)
print("Local variables:")
pprint.pprint(eval_locals)
print("Exception: %s" % exception)
msg = "Evaluation error of setting %r: %s" % (setting_key, exception)
super().__init__(msg)
class JobTypePropertyGroup: class JobTypePropertyGroup:
@ -47,24 +76,74 @@ class JobTypePropertyGroup:
return js return js
def label(self, setting_key: str) -> str:
"""Return the UI label for this setting."""
return self.bl_rna.properties[setting_key].name
def eval_and_assign( def eval_and_assign(
self, context: bpy.types.Context, setting_key: str, setting_eval: str self,
context: bpy.types.Context,
job: _SubmittedJob,
setting_key: str,
setting_eval: str,
) -> None: ) -> None:
"""Evaluate `setting_eval` and assign the result to the job setting.""" """Evaluate `setting_eval` and assign the result to the job setting."""
value = self.eval_setting(context, setting_eval) value = self.eval_setting(context, setting_key, setting_eval)
setattr(self, setting_key, value) setattr(self, setting_key, value)
@staticmethod def eval_setting(
def eval_setting(context: bpy.types.Context, setting_eval: str) -> Any: self,
context: bpy.types.Context,
setting_key: str,
setting_eval: str,
) -> Any:
"""Evaluate `setting_eval` and return the result.""" """Evaluate `setting_eval` and return the result."""
eval_globals = { eval_locals = {
"bpy": bpy, "bpy": bpy,
"C": context, "C": context,
"jobname": context.scene.flamenco_job_name,
"Path": Path,
"last_n_dir_parts": self.last_n_dir_parts,
"settings": self,
} }
value = eval(setting_eval, eval_globals, {}) try:
value = eval(setting_eval, {}, eval_locals)
except Exception as ex:
raise SettingEvalError(setting_key, setting_eval, eval_locals, ex) from ex
return value return value
@staticmethod
def last_n_dir_parts(n: int, filepath: Union[str, Path, None] = None) -> Path:
"""Return the last `n` parts of the directory of `filepath`.
If `n` is 0, returns an empty `Path()`.
If `filepath` is None, uses bpy.data.filepath instead.
>>> str(lastNDirParts(2, "/path/to/some/file.blend"))
"to/some"
Always returns a relative path:
>>> str(lastNDirParts(200, "C:\\path\\to\\some\\file.blend"))
"path\\to\\some"
"""
if n <= 0:
return Path()
if filepath is None:
filepath = Path(bpy.data.filepath)
elif isinstance(filepath, str):
filepath = Path(filepath)
dirpath = filepath.parent
if n >= len(dirpath.parts):
all_parts = dirpath.relative_to(dirpath.anchor)
return all_parts
subset = Path(*dirpath.parts[-n:])
return subset
# Mapping from AvailableJobType.setting.type to a callable that converts a value # Mapping from AvailableJobType.setting.type to a callable that converts a value
# to the appropriate type. This is necessary due to the ambiguity between floats # to the appropriate type. This is necessary due to the ambiguity between floats
@ -137,6 +216,19 @@ def _create_property(job_type: _AvailableJobType, setting: _AvailableJobSetting)
# Remove the 'ANIMATABLE' option. # Remove the 'ANIMATABLE' option.
prop_kwargs.setdefault("options", set()) prop_kwargs.setdefault("options", set())
# Add any extra arguments.
propargs = setting.get("propargs", {})
coerce_keys = {"min", "max"}
for key, value in propargs.items():
if key in coerce_keys:
propargs[key] = value_coerce(value)
prop_kwargs.update(propargs)
# Construct a getter if it's a non-editable property. By having a getter and
# not a setter, the property automatically becomes read-only in the UI.
if not setting.get("editable", True):
prop_kwargs["get"] = _create_prop_getter(job_type, setting)
prop_name = _job_setting_key_to_label(setting.key) prop_name = _job_setting_key_to_label(setting.key)
prop = prop_type(name=prop_name, **prop_kwargs) prop = prop_type(name=prop_name, **prop_kwargs)
return prop return prop
@ -229,3 +321,22 @@ def _set_if_available(
some_dict[key] = value some_dict[key] = value
else: else:
some_dict[key] = transform(value) some_dict[key] = transform(value)
def _create_prop_getter(
job_type: _AvailableJobType, setting: _AvailableJobSetting
) -> Callable[[JobTypePropertyGroup], Any]:
def evaluate_setting(propgroup: JobTypePropertyGroup) -> Any:
value = propgroup.eval_setting(
bpy.context,
setting.key,
setting.eval,
)
return value
def default_value(propgroup: JobTypePropertyGroup) -> Any:
return setting.default
if setting.get("eval"):
return evaluate_setting
return default_value

View File

@ -10,7 +10,7 @@
""" """
__version__ = "4196460c-dirty" __version__ = "7bfde1df-dirty"
# import ApiClient # import ApiClient
from flamenco.manager.api_client import ApiClient from flamenco.manager.api_client import ApiClient

View File

@ -76,7 +76,7 @@ class ApiClient(object):
self.default_headers[header_name] = header_value self.default_headers[header_name] = header_value
self.cookie = cookie self.cookie = cookie
# Set default User-Agent. # Set default User-Agent.
self.user_agent = 'Flamenco/4196460c-dirty (Blender add-on)' self.user_agent = 'Flamenco/7bfde1df-dirty (Blender add-on)'
def __enter__(self): def __enter__(self):
return self return self

View File

@ -404,7 +404,7 @@ conf = flamenco.manager.Configuration(
"OS: {env}\n"\ "OS: {env}\n"\
"Python Version: {pyversion}\n"\ "Python Version: {pyversion}\n"\
"Version of the API: 1.0.0\n"\ "Version of the API: 1.0.0\n"\
"SDK Package Version: 4196460c-dirty".\ "SDK Package Version: 7bfde1df-dirty".\
format(env=sys.platform, pyversion=sys.version) format(env=sys.platform, pyversion=sys.version)
def get_host_settings(self): def get_host_settings(self):

View File

@ -9,6 +9,7 @@ Name | Type | Description | Notes
**type** | [**AvailableJobSettingType**](AvailableJobSettingType.md) | | **type** | [**AvailableJobSettingType**](AvailableJobSettingType.md) | |
**subtype** | [**AvailableJobSettingSubtype**](AvailableJobSettingSubtype.md) | | [optional] **subtype** | [**AvailableJobSettingSubtype**](AvailableJobSettingSubtype.md) | | [optional]
**choices** | **[str]** | When given, limit the valid values to these choices. Only usable with string type. | [optional] **choices** | **[str]** | When given, limit the valid values to these choices. Only usable with string type. | [optional]
**propargs** | **{str: (bool, date, datetime, dict, float, int, list, str, none_type)}** | Any extra arguments to the bpy.props.SomeProperty() call used to create this property. | [optional]
**description** | **bool, date, datetime, dict, float, int, list, str, none_type** | The description/tooltip shown in the user interface. | [optional] **description** | **bool, date, datetime, dict, float, int, list, str, none_type** | The description/tooltip shown in the user interface. | [optional]
**default** | **bool, date, datetime, dict, float, int, list, str, none_type** | The default value shown to the user when determining this setting. | [optional] **default** | **bool, date, datetime, dict, float, int, list, str, none_type** | The default value shown to the user when determining this setting. | [optional]
**eval** | **str** | Python expression to be evaluated in order to determine the default value for this setting. | [optional] **eval** | **str** | Python expression to be evaluated in order to determine the default value for this setting. | [optional]

View File

@ -93,6 +93,7 @@ class AvailableJobSetting(ModelNormal):
'type': (AvailableJobSettingType,), # noqa: E501 'type': (AvailableJobSettingType,), # noqa: E501
'subtype': (AvailableJobSettingSubtype,), # noqa: E501 'subtype': (AvailableJobSettingSubtype,), # noqa: E501
'choices': ([str],), # noqa: E501 'choices': ([str],), # noqa: E501
'propargs': ({str: (bool, date, datetime, dict, float, int, list, str, none_type)},), # noqa: E501
'description': (bool, date, datetime, dict, float, int, list, str, none_type,), # noqa: E501 'description': (bool, date, datetime, dict, float, int, list, str, none_type,), # noqa: E501
'default': (bool, date, datetime, dict, float, int, list, str, none_type,), # noqa: E501 'default': (bool, date, datetime, dict, float, int, list, str, none_type,), # noqa: E501
'eval': (str,), # noqa: E501 'eval': (str,), # noqa: E501
@ -111,6 +112,7 @@ class AvailableJobSetting(ModelNormal):
'type': 'type', # noqa: E501 'type': 'type', # noqa: E501
'subtype': 'subtype', # noqa: E501 'subtype': 'subtype', # noqa: E501
'choices': 'choices', # noqa: E501 'choices': 'choices', # noqa: E501
'propargs': 'propargs', # noqa: E501
'description': 'description', # noqa: E501 'description': 'description', # noqa: E501
'default': 'default', # noqa: E501 'default': 'default', # noqa: E501
'eval': 'eval', # noqa: E501 'eval': 'eval', # noqa: E501
@ -166,6 +168,7 @@ class AvailableJobSetting(ModelNormal):
_visited_composed_classes = (Animal,) _visited_composed_classes = (Animal,)
subtype (AvailableJobSettingSubtype): [optional] # noqa: E501 subtype (AvailableJobSettingSubtype): [optional] # noqa: E501
choices ([str]): When given, limit the valid values to these choices. Only usable with string type.. [optional] # noqa: E501 choices ([str]): When given, limit the valid values to these choices. Only usable with string type.. [optional] # noqa: E501
propargs ({str: (bool, date, datetime, dict, float, int, list, str, none_type)}): Any extra arguments to the bpy.props.SomeProperty() call used to create this property.. [optional] # noqa: E501
description (bool, date, datetime, dict, float, int, list, str, none_type): The description/tooltip shown in the user interface.. [optional] # noqa: E501 description (bool, date, datetime, dict, float, int, list, str, none_type): The description/tooltip shown in the user interface.. [optional] # noqa: E501
default (bool, date, datetime, dict, float, int, list, str, none_type): The default value shown to the user when determining this setting.. [optional] # noqa: E501 default (bool, date, datetime, dict, float, int, list, str, none_type): The default value shown to the user when determining this setting.. [optional] # noqa: E501
eval (str): Python expression to be evaluated in order to determine the default value for this setting.. [optional] # noqa: E501 eval (str): Python expression to be evaluated in order to determine the default value for this setting.. [optional] # noqa: E501
@ -261,6 +264,7 @@ class AvailableJobSetting(ModelNormal):
_visited_composed_classes = (Animal,) _visited_composed_classes = (Animal,)
subtype (AvailableJobSettingSubtype): [optional] # noqa: E501 subtype (AvailableJobSettingSubtype): [optional] # noqa: E501
choices ([str]): When given, limit the valid values to these choices. Only usable with string type.. [optional] # noqa: E501 choices ([str]): When given, limit the valid values to these choices. Only usable with string type.. [optional] # noqa: E501
propargs ({str: (bool, date, datetime, dict, float, int, list, str, none_type)}): Any extra arguments to the bpy.props.SomeProperty() call used to create this property.. [optional] # noqa: E501
description (bool, date, datetime, dict, float, int, list, str, none_type): The description/tooltip shown in the user interface.. [optional] # noqa: E501 description (bool, date, datetime, dict, float, int, list, str, none_type): The description/tooltip shown in the user interface.. [optional] # noqa: E501
default (bool, date, datetime, dict, float, int, list, str, none_type): The default value shown to the user when determining this setting.. [optional] # noqa: E501 default (bool, date, datetime, dict, float, int, list, str, none_type): The default value shown to the user when determining this setting.. [optional] # noqa: E501
eval (str): Python expression to be evaluated in order to determine the default value for this setting.. [optional] # noqa: E501 eval (str): Python expression to be evaluated in order to determine the default value for this setting.. [optional] # noqa: E501

View File

@ -4,7 +4,7 @@ Render Farm manager API
The `flamenco.manager` package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: The `flamenco.manager` package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.0.0 - API version: 1.0.0
- Package version: 4196460c-dirty - Package version: 7bfde1df-dirty
- Build package: org.openapitools.codegen.languages.PythonClientCodegen - Build package: org.openapitools.codegen.languages.PythonClientCodegen
For more information, please visit [https://flamenco.io/](https://flamenco.io/) For more information, please visit [https://flamenco.io/](https://flamenco.io/)

View File

@ -5,6 +5,8 @@ import logging
from pathlib import Path from pathlib import Path
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from flamenco.job_types_propgroup import JobTypePropertyGroup
import bpy import bpy
from . import job_types, job_submission from . import job_types, job_submission
@ -115,8 +117,13 @@ class FLAMENCO_OT_eval_setting(FlamencoOpMixin, bpy.types.Operator):
setting_eval: bpy.props.StringProperty(name="Python Expression") # type: ignore setting_eval: bpy.props.StringProperty(name="Python Expression") # type: ignore
def execute(self, context: bpy.types.Context) -> set[str]: def execute(self, context: bpy.types.Context) -> set[str]:
propgroup = context.scene.flamenco_job_settings job = job_submission.job_for_scene(context.scene)
propgroup.eval_and_assign(context, self.setting_key, self.setting_eval) if job is None:
self.report({"ERROR"}, "This Scene has no Flamenco job")
return {"CANCELLED"}
propgroup: JobTypePropertyGroup = context.scene.flamenco_job_settings
propgroup.eval_and_assign(context, job, self.setting_key, self.setting_eval)
return {"FINISHED"} return {"FINISHED"}

View File

@ -28,7 +28,9 @@ func exampleSubmittedJob() api.SubmittedJob {
"images_or_video": "images", "images_or_video": "images",
"image_file_extension": ".png", "image_file_extension": ".png",
"video_container_format": "", "video_container_format": "",
"render_output": "/render/sprites/farm_output/promo/square_ellie/square_ellie.lighting_light_breakdown2/######", "render_output_root": "/render/sprites/farm_output/promo/square_ellie",
"add_path_components": 1,
"render_output_path": "/render/sprites/farm_output/promo/square_ellie/square_ellie.lighting_light_breakdown2/######",
}} }}
metadata := api.JobMetadata{ metadata := api.JobMetadata{
AdditionalProperties: map[string]string{ AdditionalProperties: map[string]string{
@ -144,7 +146,7 @@ func TestSimpleBlenderRenderWindowsPaths(t *testing.T) {
// Adjust the job to get paths in Windows notation. // Adjust the job to get paths in Windows notation.
sj.Settings.AdditionalProperties["blendfile"] = "R:\\sf\\jobs\\scene123.blend" sj.Settings.AdditionalProperties["blendfile"] = "R:\\sf\\jobs\\scene123.blend"
sj.Settings.AdditionalProperties["render_output"] = "R:\\sprites\\farm_output\\promo\\square_ellie\\square_ellie.lighting_light_breakdown2\\######" sj.Settings.AdditionalProperties["render_output_path"] = "R:\\sprites\\farm_output\\promo\\square_ellie\\square_ellie.lighting_light_breakdown2\\######"
aj, err := s.Compile(ctx, sj) aj, err := s.Compile(ctx, sj)
if err != nil { if err != nil {

View File

@ -7,14 +7,19 @@ const JOB_TYPE = {
{ key: "chunk_size", type: "int32", default: 1, description: "Number of frames to render in one Blender render task" }, { key: "chunk_size", type: "int32", default: 1, description: "Number of frames to render in one Blender render task" },
{ key: "frames", type: "string", required: true, eval: "f'{C.scene.frame_start}-{C.scene.frame_end}'", { key: "frames", type: "string", required: true, eval: "f'{C.scene.frame_start}-{C.scene.frame_end}'",
description: "Frame range to render. Examples: '47', '1-30', '3, 5-10, 47-327'" }, description: "Frame range to render. Examples: '47', '1-30', '3, 5-10, 47-327'" },
// render_output_root + add_path_components determine the value of render_output_path.
{ key: "render_output_root", type: "string", subtype: "dir_path", required: true, { key: "render_output_root", type: "string", subtype: "dir_path", required: true,
description: "Base directory of where render output is stored. Will have some job-specific parts appended to it"}, description: "Base directory of where render output is stored. Will have some job-specific parts appended to it"},
{ key: "add_path_components", type: "int32", required: true, default: 0, propargs: {min: 0, max: 32},
description: "Number of path components of the current blend file to use in the render output path"},
{ key: "render_output_path", type: "string", subtype: "file_path", editable: false,
eval: "str(Path(settings.render_output_root) / last_n_dir_parts(settings.add_path_components) / jobname / '{timestamp}' / '######.{ext}')",
description: "Final file path of where render output will be saved"},
// Automatically evaluated settings: // Automatically evaluated settings:
{ key: "blender_cmd", type: "string", default: "{blender}", visible: false }, { key: "blender_cmd", type: "string", default: "{blender}", visible: false },
{ key: "blendfile", type: "string", required: true, description: "Path of the Blend file to render", visible: false }, { key: "blendfile", type: "string", required: true, description: "Path of the Blend file to render", visible: false },
{ key: "render_output_path", type: "string", subtype: "file_path", visible: false,
description: "Final file path of where render output is stored, set by the job compiler"},
{ key: "fps", type: "float", eval: "C.scene.render.fps / C.scene.render.fps_base", visible: false }, { key: "fps", type: "float", eval: "C.scene.render.fps / C.scene.render.fps_base", visible: false },
{ {
key: "images_or_video", key: "images_or_video",
@ -55,15 +60,14 @@ const videoContainerToExtension = {
function compileJob(job) { function compileJob(job) {
print("Blender Render job submitted"); print("Blender Render job submitted");
const renderOutput = renderOutputPath(job);
job.settings.render_output_path = renderOutput;
print("job: ", job); print("job: ", job);
const settings = job.settings;
const renderOutput = settings.render_output_path;
const finalDir = path.dirname(renderOutput); const finalDir = path.dirname(renderOutput);
const renderDir = intermediatePath(job, finalDir); const renderDir = intermediatePath(job, finalDir);
const settings = job.settings;
const renderTasks = authorRenderTasks(settings, renderDir, renderOutput); const renderTasks = authorRenderTasks(settings, renderDir, renderOutput);
const videoTask = authorCreateVideoTask(settings, renderDir); const videoTask = authorCreateVideoTask(settings, renderDir);
@ -79,19 +83,6 @@ function compileJob(job) {
} }
} }
// Return the intended render output path.
function renderOutputPath(job) {
// {DIR}/{job name}/{date and time of job submission}/######.{extension}
const pathSafeJobName = job.name.replace(/[/\\:?*]/, "-");
const extension = guessOutputFileExtension(job.settings);
return path.join(
job.settings.render_output_root,
pathSafeJobName,
formatTimestampLocal(job.created),
`######${extension}`
);
}
// Determine the intermediate render output path. // Determine the intermediate render output path.
function intermediatePath(job, finalDir) { function intermediatePath(job, finalDir) {
const basename = path.basename(finalDir); const basename = path.basename(finalDir);

View File

@ -403,6 +403,9 @@ components:
description: When given, limit the valid values to these choices. Only usable with string type. description: When given, limit the valid values to these choices. Only usable with string type.
type: array type: array
items: {type: string} items: {type: string}
"propargs":
description: Any extra arguments to the bpy.props.SomeProperty() call used to create this property.
type: object
"description": "description":
description: The description/tooltip shown in the user interface. description: The description/tooltip shown in the user interface.
"default": "default":

View File

@ -18,65 +18,66 @@ import (
// Base64 encoded, gzipped, json marshaled Swagger object // Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{ var swaggerSpec = []string{
"H4sIAAAAAAAC/9xb3W4bN/Z/FWL6B9riP5KcOLvA+mrTpEkd5MOonfYiMWRq5khDm0NOSI4UbWCgD7Fv", "H4sIAAAAAAAC/9xb224cN5N+FaL/BZJge2Zky7vA6mr127EjwwchIycXtjDidNdMU2KTbZI9o1lDQB5i",
"sltgL7ZX+wLuGy0OP+ZDQ1l266Tt5iIYzZCH5xyezx/pD0kmy0oKEEYnBx8SnRVQUvv4UGu2EJCfUH2B", "32Q3wF5srvYFlDdaFA99mObokMhO8vvC6OnmoapYVV/xI/UpyWRZSQHC6OTgU6KzAkpqHw+1ZksB+QnV",
"v3PQmWKVYVIkB72vhGlCicEnqgkz+FtBBmwJOZmtiSmAfC/VBahxkiaVkhUow8CuksmypCK3z8xAaR/+", "F/g7B50pVhkmRXLQ+0qYJpQYfKKaMIO/FWTAVpCT+YaYAsiPUl2AGidpUilZgTIM7CyZLEsqcvvMDJT2",
"T8E8OUg+m7TMTTxnk0duQnKZJmZdQXKQUKXoGn+fyxnO9q+1UUws/PtppZhUzKw7A5gwsAAVRri3kemC", "4Z8ULJKD5G+TVriJl2zy1HVIrtLEbCpIDhKqFN3g73M5x97+tTaKiaV/P6sUk4qZTacBEwaWoEIL9zbS",
"lvEP19PUhpp6pziov2M3EiWi+mI7I3XNcvwwl6qkJjlwL9LNgZdpouBdzRTkycGbMAiV42VpeOuIsKGl", "XdAy/uHmMbWhpr5VHbTf1LVEjai+2C1IXbMcPyykKqlJDtyLdLvhVZoo+FgzBXly8D40QuN4XRrZOips",
"jkq6XKXtfp0268rZOWQGGXy4pIzTGYdncnYMxiA7A8s5ZmLBgWj3ncg5oeSZnBGkpiMGUkiWucc+ne8L", "Waljkq5Uabtep828cn4OmUEBD1eUcTrn8FLOp2AMijPwnCkTSw5Eu+9ELgglL+Wc4Gg64iCFZJl77I/z",
"EGTBliBSwlnJjLWzJeUsx/9r0MRIfKeBeCJj8krwNak18khWzBTEKc0ujms3JjhQ/qax5TCnNTdDvk4K", "YwGCLNkKREo4K5mxfrainOX4fw2aGInvNBA/yJi8FXxDao0ykjUzBXFGs5Pj3I0LDoy/7Ww5LGjNzVCu",
"IP6j44PoQq6EZ4bUGhRZIe85GFAlE3b9gumgkrEj36EZX6J5MzFScsMqvxAT7UJoj2pOM7BEIWcGRXcU", "kwKI/+jkILqQa+GFIbUGRdYoew4GVMmEnb9gOphk7IbvjBmfonkzMVJywyo/ERPtROiPakEzsINCzgyq",
"Pf9zyjWkQ+WaAhQyTTmXK4JTNxkldG5wTAHkXM5IQTWZAQii61nJjIF8TL6XNc8JKyu+JjlwcNM4J/Ce", "7kb08i8o15AOjWsKUCg05VyuCXbdFpTQhcE2BZBzOScF1WQOIIiu5yUzBvIx+VHWPCesrPiG5MDBdeOc",
"aUeQ6gtN5lI50udylhIqcgwgsqwYxzHMjN+K1tBnUnKgwkq0pHyon6O1KaQg8L5SoDWTVvkzIDi6pgZy", "wCXTbkCqLzRZSOWGPpfzlFCRYwKRZcU4tmFm/EG0jj6XkgMVVqMV5UP7HG9MIQWBy0qB1kxa48+BYOua",
"1JFUuRMw7ANYSfpb1/DV7E06NI0LWA95OMxBGDZnoDyRxuRTUtbaID+1YO9qZ4h+0869I0TXab36FtvH", "GsjRRlLlTsGwDmA16S9dI1ezNunQNS5gM5ThKAdh2IKB8oM0Lp+SstYG5akF+1g7R/SLdu4DIToPBgZV",
"yhJyRg3wNVGAnkqoXSaHORMMJ6TohFZKXDK1/MjauFcVVYZlNaeq0cGWvdD1LISu6yJeJEgc+5mNm92a", "y0gsHIoNgUujKKFqWZeYYYK/zavNGDvq8VSWcOxia/P1NyTDZag15NgyU0ANOFV9/G06MrQh3maWe7gQ",
"womfvmSabRq4UfV1CkKn6Zu134vXhy44obKCSSvyBWcXQCj5ioNAA6J5PpLiyzE5BoPkzuyGnDkXd7mQ", "K0vIGTXAN0QBDkWoVTWHBRMMO6SYCOz0OGVqbSJr4yWiyrCs5lQ167DDH3Q9D+nzpqwbSVRT37MJ9XuP",
"CueHgvJmDVNQg0vXPBefW2NoogSI3Dqvjit6I7yj8flBNwzJx+0+bUTmejbCL84cnDGGPSePaqVAGL4m", "cOK7r5hm20FmVH2TgTBw+6Hl/eHdkUuQaKwQVop8zdkFEEr+zkGgE9M8H0nxzZhMweBwZ3ZBzlyacXhM",
"EmMoDXStdXeiqB6Ts28eHn/z9ePpk8PnX0+PHp58c+YqhJwpyIxUa1JRU5D/J2dvk8ln9t/b5IzQqkKV", "hcsFgvJmDlNQg1PXPBdfWYdsMhWI3CYQHTf0FsRgAPhGd4SFabtOW+hQz0f4xbmDC4iw5uRprRQIwzdE",
"5k5sEHWJ8s0ZhymOT9IkZyo82tc+mxVUF5BP25GnEefZZjTD4Oo10JG+47EudVBNDh8fuTy1tmKj0XiT", "Yh6nYVwbYZ1Mrsfk7LvD6XffPps9P3r17ez48OS7M1el5ExBZqTakIqagvwzOfuQTP5m/31IzgitKjRp",
"GJOXkgjQGGe0UXVmagWafGFTh05JzjJciioG+ktCFRBdV5VUZlN0z3yKVcX+fRSaS2qS1NrCTiHj0oVM", "7tQGUZeo34JxmGH7JE1ypsKjfe0RtaC6gHzWtjyNBPAupxkmeG+BjvadrOHgi2py9CzEs1Ubnca7xJi8",
"267pKjSmyQsq6AKUC7/MWNenJQbHSFrmdAb8duWSV+bNS71YOTHIxBvu4E3CsddZc5dvoLYiRcZzpk0w", "kUSAxlynjaozUyvQ5GsLXzolOctwKqoY6G8IVUB0XVVSmW3VvfApVjb7j1FpLqlJUusLtyoZ1y6gfTun",
"Bmvd2/U21FEooX6ZxCe9iLhF3HaJmIChVh6I5T8QBZghkQVCiXaFma/wbCR6D1ltYFcNv71Abgyo8zmw", "qxKZJq+poEtQDgKYsaFPS0zQkdKA0znw+5Vs3ph3LzdjJc2gGtgKB+8STrzOnLfFBlorktxfMW2CM1jv",
"F9+4zpSYRF8rJRUS2+wicuhVxsFjhmV5CVrTRYzfDYYszXZ8jJsnnJYgMvkdKO0LtRtqZtnOuJ6LMND7", "3m23oY1CGffbND7pZcQd6rZTxBQM9fpALf+BKECUtpBFiXbFoa8ybSa6hKw2cNs+YneR3jhQ53MQL75w",
"VYyLZ67toZy/micHb663sONQm+Gsy3SgSAVYH0UsBj/YSoqVoA0tK4xHQd05NTDCL7GyhUXIvX59+Dik", "nS4xjb5VSiocbHsnk0OvOg8RM9walKA1Xcbk3RLIjtm2j0nznNMSRCZ/AKV9sXhHy6zaHjdLERr6uIpJ",
"mWe2M9nR1Ny0n8JQ0bRTdZXfsTQbu2M5DTpr12uYPb08dRv0AgzNqaF2o/Lcll2UH/V0P5B4o+NWM2YU", "8dJtvSjnbxfJwfubPWwa6kPsdZUODGlrkZjH4AdbzbEStKFlhfkomDunBkb4JVY6schw794dPQsw89Lu",
"VWtSemI+7eoxeSGVddyKw/tuzsmowKxVSqy9bcSq0cvJGR3PxtkZEdI4PYQS9QLW6N/wniItb9DW0A6S", "jm7ZWN11T4epotnS1VX+wNpsrY6VNNisna8R9vTq1C3QazA0p4bahcpzW3ZRftyz/UDjrTpTzZlRVG1I",
"40oxA+SJYosCsxDWKGMoKePI9XqmQPx15lOgVIswwvlAcmwHkGPzn38vgXcCW8+Qjzs5Iq4nV81F5zYG", "6QfzsKvH5LVUNnArDpddzMmoQNQqJdb/NmPVGOXkjI7n4+yMCGmcHUKZfAG29IRLimN5h7aOdpBMK8UM",
"EhIozQxb2q6Vigw14BrYioPxz8Ipi0kxmlPmRjQPFa21fXhXQ20fqMoKtuw8uvzsyI/QMmza90R6L+yz", "kOeKLQtEIaxRxlBSxlHqzVyB+Pe5h0CplqGFi4FkahuQqfm//10B7yS2niNPOxgRt5Or5qJ9GwcJAEoz",
"o1KjikbdxZM0WVHbYI3mUo2wktHRBP8tLJg2oCB3wXgYcmieY9MTNShOtZlapfRRi07yZtnF9nDOqUEn", "w1Z250xFhhZwm+iKg/HPwhmLSTFaUOZaNA8VxRI9SZOPNdT2gaqsYKvOo8NnN/wIPcPCvh+k98I+u1Fq",
"iWd3OTcrqrak/hv5rhOpdd8m1U4bBKKfSnc26b8KMWl0kTZK7SInQRlpkrnS2HKZbGq5o5ktEsVi+jFk", "NNGoO3mSJmtqN3mjhVQjrGR0FOC/hyXTBhTkLhkPUw7Nc9x4RR2KU21m1ih95qQD3iy72J3OOTUYJHF0",
"tWJmvSXf3TiJXZe9eqkgWii2LWLb02NdEPLeRqgoO0Hu44UN/2H/6u/k5x+ufrz66eqfVz/+/MPVv65+", "lwuzpmoH9N8pdp1Kbfg2UDtrWJA+lN5KFPwu1qaxRdoYtcveBGOkSeZKYytlsm3ljmV2aBTL6VPIasXM",
"uvpHF9I6+NNev+j0q0yzMk8Okg/+5yXuYFGLi6lmf4PkYB9lMopmZkrrnMkQctApbXdxkEyUnTnR88m5", "Zgfe3RnEbkKvHhREC8V2i9jyClgXBNzbShVlJ8l9vrThP+xf/yf59afrn69/uf7v659//en6f65/uf6v",
"nKEBg4B79/fHlmQ3lRy9fIo/K50c3H+QJnMsbnRykNwb3dvDwr6kC9BTqaZLloPESsW+SdJE1qaqjWtq", "Lq128C97/aLTzzLLyjw5SD75n1e4gkUtLmaa/QckB/uok1E0MzNa50yGlINBaXcXB8lE2Z4TvZicyzk6",
"4L0B4eqFZFzZkOM4mLpRfZbcIg1THb/QDLdq5AUfuSkOyexbV7uPO3Jtk9duipM2XTluTgQ07WzXrjQf", "MAh49Hh/bIfsQsnxmxf4s9LJweMnabLA4kYnB8mj0aM9LOxLugQ9k2q2YjlIrFTsmyRNZG2q2rhNDVwa",
"hnZQg+udwTuzRzIbrmK+0YFlb5FPmszRhHr0/TazRPKEzzGxWI88vLYVRaRJbb4RC1wIg8md+hIdfdTV", "EK5eSMaVTTlOgplr1RfJTdII1YkLzXCpRl7xkesS2I2ud7XreAvWNrh2V6622ZXj4kSI285y3QbzoWmH",
"Ig53soKQt/Xe3v0/Ey4X2gEbFrFn5nPtC32Pb23kk0666PPwSsCIM+FhJpGzDBdcFRQpZg1cUNi+HqsO", "Nbg5GHwweza1kSoWGx1q+B540iBHk+ox9ltkieCEx5hYrkcZ3tmKIrJJbb4RS1wIg+BOfYmOMepqEcd9",
"C7giQ7jwmLxaglphbNCkUrBkstZ87WQJizYVTqwg5DKCLj+XC4JMdSBFXC0lK8Y51kIBZUCmrSrsgkAV", "WUXIh3pv7/G/Ei6X2hEb9tSAma+0L/Q9x7aFJx246MvwVsCIM+FpJpGzDCdcFxRHzBq6oLD7eqw6LOmL",
"Z663GSaVni3cFMyPFThud1wOV9TEW4ZfnoEhU2Din35lJt1wJL9SLwlGl+gk0dOt+jhmC/HqtpoISXW6", "AuHEY/J2BWqNuUGTSsGKyVrzjdMlTNpUOLGCkMsIw/1KLgkK1aE1cbaUrBnnWAsFlgGFtqawEwJVnLm9",
"vZO6c7E7BcEWaQdcXSO1oQYeFVQsYCi689hpGyhuVTlt7tYmsRsxlW/j6g542cFBP+hqQ5VxdTZd0Qtb", "zRBUer5w1wOFWIHjVsdhuKImvmX47QgMmQIT//Q7kXQrkPxMPRCMTtEB0dOd9piypXh7X0sEUJ3t3kk9",
"jmkOgC0b2PIoTXRRm1yuLFwK2o+W8zlGgkhsdc5iC6xj5NqJt7IMTGmNOX7QsGpQuPcYbjGEucHk8HFK", "uNqdgmCHtgOpbtDaUANPCyqWMFTdReysTRT3qpy2V2t7sDsJle+S6gFkuUWCftLVhirj6my6phe2HNMc",
"Kqr1Sqo8fHLe4Y6mCDVhqOq4PcYZqy+L7FLNsjbwFMZUySXyyMRcOnRDGJqZFlBogAdyAhSdr1bcz9QH", "ALdsYMujNNFFbXK5tnQpaN9aLhaYCSK51QWLLbCmKLVTb20FmNEaMX6wYdWgcO0x3WIKc43J0bOUVFTr",
"k8k8lGdMToZ95LcOt35CVUlKB12Rh0eHWLiyDISGzjpPj54v9wf0V6vVeCFqrNYmfo6eLCo+2h/vjUGM", "tVR5+OSiwx2PEWpCU9UJe8wz1l6W2aWaZW3iKYypkiuUkYmFdOyGMDQzLaHQEA/kBCgGX62476kPJpNF",
"C1O6Bo8Z3uPWL5d08I/k3nhvvIejZQWCVgxLO/sKc6Mp7M5MaMVspWVtUmqrCrRMq8zD3GHXJTMOSvCW", "KM+YnAz3kd873vo5VSUpHXVFDo+PsHBlGQgNnXleHL9a7Q/GX6/X46WosVqb+D56sqz4aH+8NwYxLkzp",
"/pXM10F9IOwcWlUc0xSTYnKuXdRwdrvLqvu4yeVAqxZXlb5MTrpGj9Wj9QJdSdQUrnR/b+/OOLuGoRXV", "NnjM8J60frqkw38kj8Z74z1sLSsQtGJY2tlXiI2msCszoRWzlZb1SamtKdAzrTGPcsddl8w4KsF7+t9l",
"RNdZBnpec74m7kTNHn/5lL1keU25O4Qbbxxr3gl3roGJ8Gc/kNCfWJesy5KqdbOZhBIBKwu9Yi5vrMjj", "vgnmA2H70KriCFNMism5dlnD+e1tXt3nTa4GVrW8qvRlctJ1eqwebRToSqKlcKbHe3sPJtkNAq2pJrrO",
"rR2A0qZtilWjRUR1ctoj9ywc4LizQBB5JZkwVt7GtCZNdlhAxL6egmlQ4o+4mUNIOqK6ZlALS28o8CkY", "MtCLmvMNcad69gjOQ/aK5TXl7iBwvHW0+iDSuQ1MRD77gYT9iQ3Juiyp2jSLSSgRsLbUK2J540Web+0Q",
"wgfQtUV1C2BqA9m/RnXtUo36z9uz+p7+PpzL2ZTll1tV+ARMVjgP7QLDbz4kDKXyBzs+8jhiA0dKO3rc", "lBa2KVaNlhHVyWlvuJfhAMedR4LIK8mEsfo2rjVp0GEJEf96AaZhiT/jYg4p6YjpmkYtLb1lwBdgCB9Q",
"1dSf/jZOZ6N2fzus5PYDoTN3smr37gZ26yaJ3MfOEjkPau+UPtts9rsGPv5oqtgEwSNqEbhTnAQWIsaK", "15bVLYCpLWb/BtO1UzXmP2/vC/Ts9+lczmcsv9ppwudgssJFaJcYfv8pYaiVP9jxmccNNgiktGPH2zb1",
"CmkszMvVnEq/aNJGUBZ2qBvKcuWDgzxrDb70lyQrILtwv5jGxqKmGAppu5wGtcTSP6jV5euJ8lDbaNUi", "p39M0Nms3V8Oq7n9QOjcnazatbuD37pOIve5s0TJg9k7pc8un/2hoY8/mym2SfCIWQSuFCdBhIizokEa",
"bdHUEzA5j8h9nPwTaR0iim7bv8D9J01FA3TyJrbwCXNOLeB9BZmBnIAf0zWhwL5PPKuwn8Hq/IvTyCS3", "D/N6NafSrxvYCMbCHeqWsVz54CjPWvvjdCNJVkB24X4xjRuLmmIqpO10GtQKS/9gVofXE+WpttG6Zdqi",
"JWix7Uy9aVGaLcRIzufXVDHYCs3nQ3d9MKxIf3+K9CW1Dem9YvrNKQbjVmcvqLroVtFUk1Cs79D2I8r9", "0BM4Oc/IfR78iWwdIoZut39B+i8KRQN28i6+8AUxpxZwWUFmICfg23RdKIjvgWcd1jN4nX9xGunklgQ9",
"QUbwd2zjfQAJhcGFsDc6YP25ArKQ7paZJT+Ob4nYsSPiozq1X2K7Ozd43Kf05WGX+odw5hvb4MPaFCCM", "tu2ptz1Ks6UYycXihioGt0KLxTBcnwwr0j+fIX1JbVN6r5h+f4rJuLXZa6ouulU01SQU67dY+ynl/iAj",
"A608NIbWEG7/rJrD7js2SAU0X+MopOcuW/TgOtZu+NBcjUcDo/m+s2XJb20ZllOS2e+khR4u023BjGyf", "xDtu430CCYXBhbA3OmDzlQKylO6mmx1+HF8SccuKiM8a1H6K3eHc8HFfMpaHu9S/RDDf2QcPa1OAMI60",
"8fs2qdubhytJVuEKWgEK3DWx9RYlxO1glHWAmmjwioA6HzWQdReKqPdlkxqdnDeIZ/9bec/Hc79vTglj", "8tQYekO4/bNuDrsf2CEV0HyDrXA8d9miR9exdsGH7mo8GxjF+86SJX+0Z1hJSWa/k5Z6uEp3JTOyu8ef",
"coK1aWYvys7s1TKaYcDgkLt634H1Ppa0hwc9W0mJVBi5glZCfAE14jKj3IY2yvVdx7Ml9KSp9cBUjf/z", "26Xu7x6uJFmHK2gFKHDXxDY7jBD3g1HWIWqiyStC6nzWRNadKGLeNw00Oj3vkM/+sXDP53O/bs4IY3KC",
"gS3pNSsgrzmcuKPTj9dXd/+YIbKx9s8YuoDCtkD1Uvoby/0LkLa/CPejLtPkwd7+3UFPvbPgCPNHoAK2", "tWlmL+vO7dUymmHC4JC7et+R9T6XtIcHPV9JiVSYuYJVQn4BNeIyo9ymNsr1Q+ezFfS0qfXAVY3/E4Yd",
"8RgEc0Hzwd5fInfmnQEyTYQ0IdO5Uy1nTinRMny2F7+hdxHMiW5PcomQKyfq/f1Pm1qCF1GBXMqZoUzY", "8JoVkNccTtzR6efbV3f/oCKysPZPKbqEwq5E9Ub6W9P9C5B2fxHuR12lyZO9/YejnnpnwRHhj0EFbuMZ",
"sttyl5JZbdx9zYW099eFtHHWedstPfaVo04b+h1t7HIla1PaG7iKwE4dD5l8sOcIHj6J+0rnPPAmCIon", "COaS5pO9f4vc23cOyDQR0gSkc6dazp1SomX4bC+fQ+8imFPdnuQSIddO1cf7XxZaQhRRgVLKuaFM2LLb",
"+OshlLtPFx1Jtvmir4eYcCwGDOPW2eKkgEBrZUNrBlXIqFEXOfHnkzYj+6jRNSO3adZPTJ+29Zku/T9K", "SpeSeW3cfc2ltHfohbR51kXbPSP2rRudNuN3rHFbKFmf0t7BVYR26kTI5JM9R/D0STxWOueBd2FQ/IC/",
"WnrdHhW7s1KzrlhmYZLuyW6l5EKB1qm/aeb/dECROWW8VrAzt4SMokHkPTQM1R2oYxTDiih4qloGG3eH", "n0J5eLjoaLIrFn09xIQTMXAY90aLkwLCWGubWjOoAqJGQ+TEn09aRPZZo+tGbtFsnJj+2DZmuuP/VWDp",
"EJOkU3QN/uiiD7ENIGNmNPD5uHUSCyRdprHLL74nsMy9q0Ex0GkHRk43ULlxD7vUEaIPjw77QHa3JJRl", "XXtU7M5KzaZimaVJuie7lZJLBVqn/qaZ/9MBRRaU8VrBrdgSEEWDyHtsGJo7jI5ZDCuiEKlqFXzcHUJM",
"WQt/gs5MMWC9Q97r9vL08r8BAAD//yU2/SO+NwAA", "kk7RNfjDjz7FNqCMmdHAF+M2SCyRdJXGLr/4PYEV7mMNioFOOzRyusXKjXvcpY4Menh81CeyuyWhLMta",
"+BN0ZoqB6J3hvW2vTq/+PwAA///UdlNGQjgAAA==",
} }
// GetSwagger returns the content of the embedded swagger specification file // GetSwagger returns the content of the embedded swagger specification file

View File

@ -135,6 +135,9 @@ type AvailableJobSetting struct {
// Identifier for the setting, must be unique within the job type. // Identifier for the setting, must be unique within the job type.
Key string `json:"key"` Key string `json:"key"`
// Any extra arguments to the bpy.props.SomeProperty() call used to create this property.
Propargs *map[string]interface{} `json:"propargs,omitempty"`
// Whether to immediately reject a job definition, of this type, without this particular setting. // Whether to immediately reject a job definition, of this type, without this particular setting.
Required *bool `json:"required,omitempty"` Required *bool `json:"required,omitempty"`