diff --git a/openpype/hosts/traypublisher/api/batch_lib.py b/openpype/hosts/traypublisher/api/batch_lib.py new file mode 100644 index 0000000000..2486d405bd --- /dev/null +++ b/openpype/hosts/traypublisher/api/batch_lib.py @@ -0,0 +1,61 @@ +# Helper functions to find matching asset for (multiple) processed source files +import os +import collections + +from openpype.client import get_assets + + +def get_children_assets_by_name(project_name, top_asset_doc): + """ Get all children for 'top_asset_doc' by theirs name + + Args: + project_name (str) + top_asset_doc (asset doc) (eg dict) + Returns: + (dict) {"shot1": shot1_asset_doc} + """ + assets_by_parent_id = get_asset_docs_by_parent_id(project_name) + _children_docs = get_children_docs( + assets_by_parent_id, top_asset_doc + ) + children_docs = { + children_doc["name"].lower(): children_doc + for children_doc in _children_docs + } + return children_docs + + +def get_asset_docs_by_parent_id(project_name): + """ Query all assets for project and store them by parent's id to list + + Args: + project_name (str) + Returns: + (dict) { _id of parent :[asset_doc1, asset_doc2]} + """ + asset_docs_by_parent_id = collections.defaultdict(list) + for asset_doc in get_assets(project_name): + parent_id = asset_doc["data"]["visualParent"] + asset_docs_by_parent_id[parent_id].append(asset_doc) + return asset_docs_by_parent_id + + +def get_children_docs(documents_by_parent_id, parent_doc): + """ Recursively find all children in reverse order + + Last children first. + Args: + documents_by_parent_id (dict) + parent_doc (asset doc, eg dict) + Returns + (list) of asset docs + """ + output = [] + children = documents_by_parent_id.get(parent_doc["_id"]) or tuple() + for child in children: + output.extend( + get_children_docs(documents_by_parent_id, child) + ) + output.append(parent_doc) + return output + diff --git a/openpype/hosts/traypublisher/plugins/create/create_mov_batch.py b/openpype/hosts/traypublisher/plugins/create/create_mov_batch.py new file mode 100644 index 0000000000..5297d73ba9 --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/create/create_mov_batch.py @@ -0,0 +1,191 @@ +import copy +import os +import re + +from openpype.client import get_assets +from openpype.hosts.traypublisher.api import pipeline +from openpype.lib import FileDef, TextDef, get_subset_name_with_asset_doc +from openpype.pipeline import ( + CreatedInstance +) + +from openpype.hosts.traypublisher.api.plugin import TrayPublishCreator + + +class BatchMovCreator(TrayPublishCreator): + """Creates instances from .mov file(s).""" + identifier = "render_mov_batch" + label = "Batch Mov" + family = "render" + description = "Publish batch of movs" + host_name = "traypublisher" + + create_allow_context_change = False + version_regex = re.compile(r"^(.+)_v([0-9]+)$") + + default_tasks = ["Compositing"] + + extensions = [".mov"] + + def __init__(self, project_settings, *args, **kwargs): + super(BatchMovCreator, self).__init__(project_settings, + *args, **kwargs) + self._default_variants = (project_settings["traypublisher"] + ["BatchMovCreator"] + ["default_variants"]) + + def get_icon(self): + return "fa.file" + + def create(self, subset_name, data, pre_create_data): + file_paths = pre_create_data.get("filepath") + if not file_paths: + return + + for file_info in file_paths: + instance_data = copy.deepcopy(data) + file_name = file_info["filenames"][0] + filepath = os.path.join(file_info["directory"], file_name) + instance_data["creator_attributes"] = {"filepath": filepath} + + asset_doc, version = self.get_asset_doc_from_file_name( + file_name, self.project_name) + + subset_name, task_name = self._get_subset_and_task( + asset_doc, data["variant"], self.project_name) + + instance_data["task"] = task_name + instance_data["asset"] = asset_doc["name"] + + # Create new instance + new_instance = CreatedInstance(self.family, subset_name, + instance_data, self) + # Host implementation of storing metadata about instance + pipeline.HostContext.add_instance(new_instance.data_to_store()) + # Add instance to current context + self._add_instance_to_context(new_instance) + + def get_asset_doc_from_file_name(self, source_filename, project_name): + """Try to parse out asset name from file name provided. + + Artists might provide various file name formats. + Currently handled: + - chair.mov + - chair_v001.mov + - my_chair_to_upload.mov + """ + version = None + asset_name = os.path.splitext(source_filename)[0] + # Always first check if source filename is in assets + matching_asset_doc = self._get_asset_by_name_case_not_sensitive( + project_name, asset_name) + + if matching_asset_doc is None: + matching_asset_doc, version = ( + self._parse_with_version(project_name, asset_name)) + + if matching_asset_doc is None: + matching_asset_doc = self._parse_containing(project_name, + asset_name) + + if matching_asset_doc is None: + raise ValueError( + "Cannot guess asset name from {}".format(source_filename)) + + return matching_asset_doc, version + + def _parse_with_version(self, project_name, asset_name): + """Try to parse asset name from a file name containing version too + + Eg. 'chair_v001.mov' >> 'chair', 1 + """ + self.log.debug(( + "Asset doc by \"{}\" was not found, trying version regex." + ).format(asset_name)) + + matching_asset_doc = version_number = None + + regex_result = self.version_regex.findall(asset_name) + if regex_result: + _asset_name, _version_number = regex_result[0] + matching_asset_doc = self._get_asset_by_name_case_not_sensitive( + project_name, _asset_name) + if matching_asset_doc: + version_number = int(_version_number) + + return matching_asset_doc, version_number + + def _parse_containing(self, project_name, asset_name): + """Look if file name contains any existing asset name""" + for asset_doc in get_assets(project_name, fields=["name"]): + if asset_doc["name"].lower() in asset_name.lower(): + return get_assets(project_name, + asset_names=[asset_doc["name"]]) + + def _get_subset_and_task(self, asset_doc, variant, project_name): + """Create subset name according to standard template process""" + task_name = self._get_task_name(asset_doc) + + subset_name = get_subset_name_with_asset_doc( + self.family, + variant, + task_name, + asset_doc, + project_name + ) + + return subset_name, task_name + + def _get_task_name(self, asset_doc): + """Get applicable task from 'asset_doc' """ + available_task_names = {} + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + for task_name in asset_tasks.keys(): + available_task_names[task_name.lower()] = task_name + + task_name = None + for _task_name in self.default_tasks: + _task_name_low = _task_name.lower() + if _task_name_low in available_task_names: + task_name = available_task_names[_task_name_low] + break + + return task_name + + def get_default_variants(self): + return self._default_variants + + def get_instance_attr_defs(self): + return [] + + def get_pre_create_attr_defs(self): + # Use same attributes as for instance attributes + return [ + FileDef( + "filepath", + folders=False, + single_item=False, + extensions=self.extensions, + label="Filepath" + ) + ] + + def get_detail_description(self): + return """# Publish batch of .mov to multiple assets. + + File names must then contain only asset name, or asset name + version. + (eg. 'chair.mov', 'chair_v001.mov', not really safe `my_chair_v001.mov` + """ + + def _get_asset_by_name_case_not_sensitive(self, project_name, asset_name): + """Handle more cases in file names""" + asset_name = re.compile(asset_name, re.IGNORECASE) + + assets = list(get_assets(project_name, asset_names=[asset_name])) + if assets: + if len(assets) > 1: + self.log.warning("Too many records found for {}".format( + asset_name)) + return + + return assets.pop() diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_mov_batch.py b/openpype/hosts/traypublisher/plugins/publish/collect_mov_batch.py new file mode 100644 index 0000000000..2a5e356684 --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/collect_mov_batch.py @@ -0,0 +1,34 @@ +import os + +import pyblish.api +from openpype.pipeline import OpenPypePyblishPluginMixin + + +class CollectMovBatch( + pyblish.api.InstancePlugin, OpenPypePyblishPluginMixin +): + """Collect file url for batch mov and create representation.""" + + label = "Collect Mov Batch Files" + order = pyblish.api.CollectorOrder + + hosts = ["traypublisher"] + + def process(self, instance): + if not instance.data.get("creator_identifier") == "render_mov_batch": + return + + file_url = instance.data["creator_attributes"]["filepath"] + file_name = os.path.basename(file_url) + _, ext = os.path.splitext(file_name) + + repre = { + "name": ext[1:], + "ext": ext[1:], + "files": file_name, + "stagingDir": os.path.dirname(file_url) + } + + instance.data["representations"].append(repre) + + self.log.debug("instance.data {}".format(instance.data)) diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 0b54cfd39e..6d2d32a037 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -31,5 +31,18 @@ ".aep" ] } - ] + ], + "BatchMovCreator": { + "family": "render_mov_batch", + "identifier": "", + "label": "Batch Mov", + "icon": "fa.file", + "default_variants": [], + "description": "", + "detailed_description": "", + "default_tasks": "Compositing", + "extensions": [ + ".mov" + ] + } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 55c1b7b7d7..7cb74d86a7 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -78,6 +78,77 @@ } ] } + }, + { + "type": "dict", + "collapsible": true, + "key": "BatchMovCreator", + "label": "Batch Mov Creator", + "use_label_wrap": true, + "collapsible_key": true, + "children": [ + { + "type": "text", + "key": "family", + "label": "Family" + }, + { + "type": "text", + "key": "identifier", + "label": "Identifier", + "placeholder": "< Use 'Family' >", + "tooltip": "All creators must have unique identifier.\nBy default is used 'family' but if you need to have more creators with same families\nyou have to set identifier too." + }, + { + "type": "text", + "key": "label", + "label": "Label" + }, + { + "type": "text", + "key": "icon", + "label": "Icon" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default variants", + "object_type": { + "type": "text" + } + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "description", + "label": "Description" + }, + { + "type": "text", + "key": "detailed_description", + "label": "Detailed Description", + "multiline": true + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "default_tasks", + "label": "Default task" + }, + { + "type": "list", + "key": "extensions", + "label": "Extensions", + "use_label_wrap": true, + "collapsible_key": true, + "collapsed": false, + "object_type": "text" + } + ] } ] }