diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 56b48c37f6..1f4900359f 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -1,7 +1,13 @@ import pyblish.api +from ayon_core.lib import BoolDef +import ayon_api +from ayon_core.pipeline import publish -class CollectHierarchy(pyblish.api.ContextPlugin): +class CollectHierarchy( + pyblish.api.ContextPlugin, + publish.AYONPyblishPluginMixin, +): """Collecting hierarchy from `parents`. present in `clip` family instances coming from the request json data file @@ -13,20 +19,40 @@ class CollectHierarchy(pyblish.api.ContextPlugin): label = "Collect Hierarchy" order = pyblish.api.CollectorOrder - 0.076 - hosts = ["resolve", "hiero", "flame", "traypublisher"] + settings_category = "core" - def process(self, context): - project_name = context.data["projectName"] - final_context = { - project_name: { - "entity_type": "project", - "children": {} - }, - } - temp_context = {} + ignore_shot_attributes_on_update = False + + @classmethod + def get_attr_defs_for_context(cls, create_context): + return [ + BoolDef( + "ignore_shot_attributes_on_update", + label="Ignore shot attributes on update", + default=cls.ignore_shot_attributes_on_update + ) + ] + + @classmethod + def apply_settings(cls, project_settings): + cls.ignore_shot_attributes_on_update = ( + project_settings + ["core"] + ["CollectHierarchy"] + ["ignore_shot_attributes_on_update"] + ) + + def _get_shot_instances(self, context): + """Get shot instances from context. + + Args: + context (pyblish.api.Context): Context is a list of instances. + + Returns: + list[pyblish.api.Instance]: A list of shot instances. + """ + shot_instances = [] for instance in context: - self.log.debug("Processing instance: `{}` ...".format(instance)) - # shot data dict product_type = instance.data["productType"] families = instance.data["families"] @@ -41,6 +67,67 @@ class CollectHierarchy(pyblish.api.ContextPlugin): self.log.debug("Skipping not a shot from hero track") continue + shot_instances.append(instance) + + return shot_instances + + def get_existing_folder_entities(self, project_name, shot_instances): + """Get existing folder entities for given shot instances. + + Args: + project_name (str): The name of the project. + shot_instances (list[pyblish.api.Instance]): A list of shot + instances. + + Returns: + dict[str, dict]: A dictionary mapping folder paths to existing + folder entities. + """ + # first we need to get all folder paths from shot instances + folder_paths = { + instance.data["folderPath"] + for instance in shot_instances + } + # then we get all existing folder entities with one request + existing_entities = { + folder_entity["path"]: folder_entity + for folder_entity in ayon_api.get_folders( + project_name, folder_paths=folder_paths, fields={"path"}) + } + for folder_path in folder_paths: + # add None value to non-existing folder entities + existing_entities.setdefault(folder_path, None) + + return existing_entities + + def process(self, context): + # get only shot instances from context + shot_instances = self._get_shot_instances(context) + + if not shot_instances: + return + + # get user input + values = self.get_attr_values_from_data(context.data) + ignore_shot_attributes_on_update = values.get( + "ignore_shot_attributes_on_update", None) + + project_name = context.data["projectName"] + final_context = { + project_name: { + "entity_type": "project", + "children": {} + }, + } + temp_context = {} + existing_entities = self.get_existing_folder_entities( + project_name, shot_instances) + + for instance in shot_instances: + folder_path = instance.data["folderPath"] + self.log.debug( + f"Processing instance: `{folder_path} {instance}` ...") + shot_data = { "entity_type": "folder", # WARNING unless overwritten, default folder type is hardcoded @@ -51,32 +138,43 @@ class CollectHierarchy(pyblish.api.ContextPlugin): } shot_data["attributes"] = {} - SHOT_ATTRS = ( - "handleStart", - "handleEnd", - "frameStart", - "frameEnd", - "clipIn", - "clipOut", - "fps", - "resolutionWidth", - "resolutionHeight", - "pixelAspect", - ) - for shot_attr in SHOT_ATTRS: - attr_value = instance.data.get(shot_attr) - if attr_value is None: - # Shot attribute might not be defined (e.g. CSV ingest) - self.log.debug( - "%s shot attribute is not defined for instance.", - shot_attr - ) - continue + # we need to check if the shot entity already exists + # and if not the attributes needs to be added in case the option + # is disabled by settings + if ( + existing_entities.get(folder_path) + and not ignore_shot_attributes_on_update + ): + SHOT_ATTRS = ( + "handleStart", + "handleEnd", + "frameStart", + "frameEnd", + "clipIn", + "clipOut", + "fps", + "resolutionWidth", + "resolutionHeight", + "pixelAspect", + ) + for shot_attr in SHOT_ATTRS: + attr_value = instance.data.get(shot_attr) + if attr_value is None: + # Shot attribute might not be defined (e.g. CSV ingest) + self.log.debug( + "%s shot attribute is not defined for instance.", + shot_attr + ) + continue - shot_data["attributes"][shot_attr] = attr_value + shot_data["attributes"][shot_attr] = attr_value + else: + self.log.debug( + "Shot attributes will not be updated." + ) # Split by '/' for AYON where asset is a path - name = instance.data["folderPath"].split("/")[-1] + name = folder_path.split("/")[-1] actual = {name: shot_data} for parent in reversed(instance.data["parents"]): diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index eb41c75699..11f520a26c 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -49,6 +49,14 @@ class CollectAudioModel(BaseSettingsModel): ) +class CollectHierarchyModel(BaseSettingsModel): + _isGroup = True + ignore_shot_attributes_on_update: bool = SettingsField( + False, + title="Ignore shot attributes on update" + ) + + class CollectSceneVersionModel(BaseSettingsModel): _isGroup = True hosts: list[str] = SettingsField( @@ -1269,6 +1277,10 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=CollectExplicitResolutionModel, title="Collect Explicit Resolution" ) + CollectHierarchy: CollectHierarchyModel = SettingsField( + default_factory=CollectHierarchyModel, + title="Collect Hierarchy" + ) ValidateEditorialAssetName: ValidateBaseModel = SettingsField( default_factory=ValidateBaseModel, title="Validate Editorial Asset Name" @@ -1466,6 +1478,9 @@ DEFAULT_PUBLISH_VALUES = { ], "options": [] }, + "CollectHierarchy": { + "ignore_shot_attributes_on_update": False, + }, "ValidateEditorialAssetName": { "enabled": True, "optional": False,