From 148ce21a9ac25a446a3ce2233501d288b396d34b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Nov 2024 09:09:12 +0100 Subject: [PATCH 001/115] Allow ExtractOIIOTranscode to pass with a warning if it can't find the RGBA channels in source media instead of raising error --- client/ayon_core/lib/transcoding.py | 7 ++++- .../publish/extract_color_transcode.py | 30 ++++++++++++------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index e9750864ac..7fe2c84789 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -67,6 +67,11 @@ VIDEO_EXTENSIONS = { } +class UnknownRGBAChannelsError(ValueError): + """Raised when we can't find RGB channels for conversion in input media.""" + pass + + def get_transcode_temp_directory(): """Creates temporary folder for transcoding. @@ -1427,7 +1432,7 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None): review_channels = get_convert_rgb_channels(channel_names) if review_channels is None: - raise ValueError( + raise UnknownRGBAChannelsError( "Couldn't find channels that can be used for conversion." ) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 3e54d324e3..b259a3230a 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -10,6 +10,7 @@ from ayon_core.lib import ( ) from ayon_core.lib.transcoding import ( + UnknownRGBAChannelsError, convert_colorspace, get_transcode_temp_directory, ) @@ -160,17 +161,24 @@ class ExtractOIIOTranscode(publish.Extractor): output_path = self._get_output_file_path(input_path, new_staging_dir, output_extension) - convert_colorspace( - input_path, - output_path, - config_path, - source_colorspace, - target_colorspace, - view, - display, - additional_command_args, - self.log - ) + try: + convert_colorspace( + input_path, + output_path, + config_path, + source_colorspace, + target_colorspace, + view, + display, + additional_command_args, + self.log + ) + except UnknownRGBAChannelsError: + self.log.error( + "Skipping OIIO Transcode. Unknown RGBA channels" + f" for colorspace conversion in file: {input_path}" + ) + continue # cleanup temporary transcoded files for file_name in new_repre["files"]: From d072da86d1abada39af3e024c6010efc5554cd52 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Nov 2024 09:11:22 +0100 Subject: [PATCH 002/115] Stop processing directly on unknown RGBA channel for a representation --- .../plugins/publish/extract_color_transcode.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index b259a3230a..0287eca300 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -153,8 +153,7 @@ class ExtractOIIOTranscode(publish.Extractor): additional_command_args = (output_def["oiiotool_args"] ["additional_command_args"]) - files_to_convert = self._translate_to_sequence( - files_to_convert) + unknown_rgba_channels = False for file_name in files_to_convert: input_path = os.path.join(original_staging_dir, file_name) @@ -174,11 +173,16 @@ class ExtractOIIOTranscode(publish.Extractor): self.log ) except UnknownRGBAChannelsError: + unknown_rgba_channels = True self.log.error( "Skipping OIIO Transcode. Unknown RGBA channels" f" for colorspace conversion in file: {input_path}" ) - continue + break + + if unknown_rgba_channels: + # Stop processing this representation + break # cleanup temporary transcoded files for file_name in new_repre["files"]: From 363824d58946796d148b0216a5464928a91b3b1d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Nov 2024 09:12:19 +0100 Subject: [PATCH 003/115] Move logic to make it clearer that we always process the same input files of the source representation but used for multiple output profiles --- .../plugins/publish/extract_color_transcode.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 0287eca300..0d88f3073f 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -100,7 +100,19 @@ class ExtractOIIOTranscode(publish.Extractor): self.log.warning("Config file doesn't exist, skipping") continue + # Get representation files to convert + if isinstance(repre["files"], list): + repre_files_to_convert = copy.deepcopy(repre["files"]) + else: + repre_files_to_convert = [repre["files"]] + repre_files_to_convert = self._translate_to_sequence( + repre_files_to_convert) + + # Process each output definition for output_def in profile_output_defs: + # Local copy to avoid accidental mutable changes + files_to_convert = list(repre_files_to_convert) + output_name = output_def["name"] new_repre = copy.deepcopy(repre) @@ -108,11 +120,6 @@ class ExtractOIIOTranscode(publish.Extractor): new_staging_dir = get_transcode_temp_directory() new_repre["stagingDir"] = new_staging_dir - if isinstance(new_repre["files"], list): - files_to_convert = copy.deepcopy(new_repre["files"]) - else: - files_to_convert = [new_repre["files"]] - output_extension = output_def["extension"] output_extension = output_extension.replace('.', '') self._rename_in_representation(new_repre, @@ -121,7 +128,6 @@ class ExtractOIIOTranscode(publish.Extractor): output_extension) transcoding_type = output_def["transcoding_type"] - target_colorspace = view = display = None # NOTE: we use colorspace_data as the fallback values for # the target colorspace. From 445dd4ec5bd79732736eaae89d4c6d337ef9c339 Mon Sep 17 00:00:00 2001 From: jm22dogs Date: Fri, 4 Apr 2025 12:32:15 +0100 Subject: [PATCH 004/115] add card sublabel with folder and task name --- .../tools/publisher/widgets/card_view_widgets.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 2f633b3149..65c5d1d4ef 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -492,10 +492,22 @@ class InstanceCardWidget(CardWidget): self._icon_widget.setVisible(valid) self._context_warning.setVisible(not valid) + @staticmethod + def get_card_widget_sub_label(folder_name, task_name=None): + sublabel = "
" + sublabel += "{}".format(folder_name) + if task_name: + sublabel += " - {}".format(task_name) + sublabel += "" + return sublabel + def _update_product_name(self): variant = self.instance.variant product_name = self.instance.product_name label = self.instance.label + folder_name = self.instance.get_folder_path().split("/")[-1] + task_name = self.instance.get_task_name() + if ( variant == self._last_variant and product_name == self._last_product_name @@ -513,6 +525,7 @@ class InstanceCardWidget(CardWidget): for part in found_parts: replacement = "{}".format(part) label = label.replace(part, replacement) + label += self.get_card_widget_sub_label(folder_name, task_name) self._label_widget.setText(label) # HTML text will cause that label start catch mouse clicks From 1e3aaa887db0a9e7d6e823404911782530c308e2 Mon Sep 17 00:00:00 2001 From: Juan M <166030421+jm22dogs@users.noreply.github.com> Date: Fri, 4 Apr 2025 14:41:39 +0100 Subject: [PATCH 005/115] Update client/ayon_core/tools/publisher/widgets/card_view_widgets.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../publisher/widgets/card_view_widgets.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 65c5d1d4ef..8a8b81d615 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -493,13 +493,17 @@ class InstanceCardWidget(CardWidget): self._context_warning.setVisible(not valid) @staticmethod - def get_card_widget_sub_label(folder_name, task_name=None): - sublabel = "
" - sublabel += "{}".format(folder_name) - if task_name: - sublabel += " - {}".format(task_name) - sublabel += "" - return sublabel + def _get_card_widget_sub_label(folder_path, task_name): + parts = [] + if folder_path: + folder_name = folder_path.split("/")[-1] + parts.append(f"{folder_name}") + if task_name: + parts.append(folder_name) + if not parts: + return None + sublabel = " - ".join(parts) + return f"{sublabel}" def _update_product_name(self): variant = self.instance.variant From 66ecc40a80f084bfccfc28ffe424ef0f3fa8cb1a Mon Sep 17 00:00:00 2001 From: Juan M <166030421+jm22dogs@users.noreply.github.com> Date: Fri, 4 Apr 2025 14:42:45 +0100 Subject: [PATCH 006/115] Update client/ayon_core/tools/publisher/widgets/card_view_widgets.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/publisher/widgets/card_view_widgets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 8a8b81d615..e847b2e970 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -529,7 +529,9 @@ class InstanceCardWidget(CardWidget): for part in found_parts: replacement = "{}".format(part) label = label.replace(part, replacement) - label += self.get_card_widget_sub_label(folder_name, task_name) + sublabel = self._get_card_widget_sub_label(folder_path, task_name) + if sublabel: + label += f"
{sublabel}" self._label_widget.setText(label) # HTML text will cause that label start catch mouse clicks From b6296423f5c7103da44e8e103f106d26b9e34030 Mon Sep 17 00:00:00 2001 From: Juan M <166030421+jm22dogs@users.noreply.github.com> Date: Fri, 4 Apr 2025 14:42:55 +0100 Subject: [PATCH 007/115] Update client/ayon_core/tools/publisher/widgets/card_view_widgets.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/publisher/widgets/card_view_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index e847b2e970..cbafa6fe81 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -509,7 +509,7 @@ class InstanceCardWidget(CardWidget): variant = self.instance.variant product_name = self.instance.product_name label = self.instance.label - folder_name = self.instance.get_folder_path().split("/")[-1] + folder_path = self.instance.get_folder_path() task_name = self.instance.get_task_name() if ( From 08aee24a484986a4e8d3943ff762fa926d3cbc55 Mon Sep 17 00:00:00 2001 From: Juan M <166030421+jm22dogs@users.noreply.github.com> Date: Fri, 4 Apr 2025 14:47:51 +0100 Subject: [PATCH 008/115] Update client/ayon_core/tools/publisher/widgets/card_view_widgets.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../ayon_core/tools/publisher/widgets/card_view_widgets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index cbafa6fe81..958542d264 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -529,9 +529,9 @@ class InstanceCardWidget(CardWidget): for part in found_parts: replacement = "{}".format(part) label = label.replace(part, replacement) - sublabel = self._get_card_widget_sub_label(folder_path, task_name) - if sublabel: - label += f"
{sublabel}" + sublabel = self._get_card_widget_sub_label(folder_path, task_name) + if sublabel: + label += f"
{sublabel}" self._label_widget.setText(label) # HTML text will cause that label start catch mouse clicks From 2d7bd487bac24b28c78e5bf67a19c4f69c6e3db7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 13 May 2025 15:40:40 +0200 Subject: [PATCH 009/115] Allow review/transcoding of more channels, like "Y", "XYZ", "AR", "AG", "AB" --- client/ayon_core/lib/transcoding.py | 73 ++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 1fda014bd8..b236cd101b 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -345,6 +345,9 @@ def get_review_info_by_layer_name(channel_names): ... ] + This tries to find suitable outputs good for review purposes, by + searching for channel names like 'red', 'green', 'blue' or 'R', 'G', 'B', + Args: channel_names (list[str]): List of channel names. @@ -353,7 +356,6 @@ def get_review_info_by_layer_name(channel_names): """ layer_names_order = [] - rgba_by_layer_name = collections.defaultdict(dict) channels_by_layer_name = collections.defaultdict(dict) for channel_name in channel_names: @@ -362,42 +364,76 @@ def get_review_info_by_layer_name(channel_names): if "." in channel_name: layer_name, last_part = channel_name.rsplit(".", 1) - channels_by_layer_name[layer_name][channel_name] = last_part if last_part.lower() not in { - "r", "red", - "g", "green", - "b", "blue", - "a", "alpha" + # Detect RGBA channels + "r", "g", "b", "a", + # Allow detecting of x, y and z channels, and normal channels + "x", "y", "z", "n", + # red, green and blue alpha/opacity, for colored mattes + "ar", "ag", "ab" }: continue if layer_name not in layer_names_order: layer_names_order.append(layer_name) - # R, G, B or A + + # R, G, B, A or X, Y, Z, N, AR, AG, AB channel = last_part[0].upper() - rgba_by_layer_name[layer_name][channel] = channel_name + channels_by_layer_name[layer_name][channel] = channel_name # Put empty layer to the beginning of the list # - if input has R, G, B, A channels they should be used for review - if "" in layer_names_order: - layer_names_order.remove("") - layer_names_order.insert(0, "") + def _sort(_layer_name: str) -> int: + # Prioritize "" layer name + # Prioritize layers with RGB channels + order = 0 + if _layer_name == "": + order -= 10 + + channels = channels_by_layer_name[_layer_name] + if all(channel in channels for channel in "RGB"): + order -= 1 + return order + layer_names_order.sort(key=_sort) output = [] for layer_name in layer_names_order: - rgba_layer_info = rgba_by_layer_name[layer_name] - red = rgba_layer_info.get("R") - green = rgba_layer_info.get("G") - blue = rgba_layer_info.get("B") - if not red or not green or not blue: + channel_info = channels_by_layer_name[layer_name] + + # RGB channels + if all(channel in channel_info for channel in "RGB"): + rgb = "R", "G", "B" + + # XYZ channels (position pass) + elif all(channel in channel_info for channel in "XYZ"): + rgb = "X", "Y", "Z" + + # Colored mattes (as defined in OpenEXR Channel Name standards) + elif all(channel in channel_info for channel in ("AR", "AG", "AB")): + rgb = "AR", "AG", "AB" + + # Luminance channel (as defined in OpenEXR Channel Name standards) + elif "Y" in channel_info: + rgb = "Y", "Y", "Y" + + # Has only Z channel (Z-depth layer) + elif "Z" in channel_info: + rgb = "Z", "Z", "Z" + + else: + # No reviewable channels found continue + + red = channel_info[rgb[0]] + green = channel_info[rgb[1]] + blue = channel_info[rgb[2]] output.append({ "name": layer_name, "review_channels": { "R": red, "G": green, "B": blue, - "A": rgba_layer_info.get("A"), + "A": channel_info.get("A"), } }) return output @@ -1428,7 +1464,8 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None): if review_channels is None: raise ValueError( - "Couldn't find channels that can be used for conversion." + "Couldn't find channels that can be used for conversion " + f"among channels: {channel_names}." ) red, green, blue, alpha = review_channels From afbf2c8848b6045dd7d712323237a7d823922d5f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 17 May 2025 15:40:54 +0200 Subject: [PATCH 010/115] Refactor `UnknownRGBAChannelsError` -> `MissingRGBAChannelsError` --- client/ayon_core/lib/transcoding.py | 4 ++-- client/ayon_core/plugins/publish/extract_color_transcode.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 8c96bde07e..07b6d8a039 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -67,7 +67,7 @@ VIDEO_EXTENSIONS = { } -class UnknownRGBAChannelsError(ValueError): +class MissingRGBAChannelsError(ValueError): """Raised when we can't find RGB channels for conversion in input media.""" pass @@ -1337,7 +1337,7 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None): review_channels = get_convert_rgb_channels(channel_names) if review_channels is None: - raise UnknownRGBAChannelsError( + raise MissingRGBAChannelsError( "Couldn't find channels that can be used for conversion " f"among channels: {channel_names}." ) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 0475b63f82..a4b6fe7fc2 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -11,7 +11,7 @@ from ayon_core.lib import ( is_oiio_supported, ) from ayon_core.lib.transcoding import ( - UnknownRGBAChannelsError, + MissingRGBAChannelsError, convert_colorspace, ) @@ -186,7 +186,7 @@ class ExtractOIIOTranscode(publish.Extractor): additional_command_args, self.log ) - except UnknownRGBAChannelsError: + except MissingRGBAChannelsError: unknown_rgba_channels = True self.log.error( "Skipping OIIO Transcode. Unknown RGBA channels" From 44dc1ea99e46c108bfbc9f4dd63c9eff7a945731 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 17 May 2025 15:43:41 +0200 Subject: [PATCH 011/115] Include message of the original raised error --- client/ayon_core/plugins/publish/extract_color_transcode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index a4b6fe7fc2..09f7e6f201 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -186,8 +186,9 @@ class ExtractOIIOTranscode(publish.Extractor): additional_command_args, self.log ) - except MissingRGBAChannelsError: + except MissingRGBAChannelsError as exc: unknown_rgba_channels = True + self.log.error(exc) self.log.error( "Skipping OIIO Transcode. Unknown RGBA channels" f" for colorspace conversion in file: {input_path}" From 7fa192229c207483c3bf30bf55d479a1d2aae8b3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 17 May 2025 15:47:50 +0200 Subject: [PATCH 012/115] Improve docstring --- client/ayon_core/lib/transcoding.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 07b6d8a039..e665486d7b 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -351,7 +351,8 @@ def get_review_info_by_layer_name(channel_names): ] This tries to find suitable outputs good for review purposes, by - searching for channel names like 'red', 'green', 'blue' or 'R', 'G', 'B', + searching for channel names like RGBA, but also XYZ, Z, N, AR, AG, AB + channels. Args: channel_names (list[str]): List of channel names. From 72895df6ae121b013c63071e080e2609cc22b7a0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 May 2025 11:16:52 +0200 Subject: [PATCH 013/115] Match variable name more with captured exception --- client/ayon_core/plugins/publish/extract_color_transcode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 09f7e6f201..45832bd1e5 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -166,7 +166,7 @@ class ExtractOIIOTranscode(publish.Extractor): files_to_convert = self._translate_to_sequence( files_to_convert) self.log.debug("Files to convert: {}".format(files_to_convert)) - unknown_rgba_channels = False + missing_rgba_review_channels = False for file_name in files_to_convert: self.log.debug("Transcoding file: `{}`".format(file_name)) input_path = os.path.join(original_staging_dir, @@ -187,7 +187,7 @@ class ExtractOIIOTranscode(publish.Extractor): self.log ) except MissingRGBAChannelsError as exc: - unknown_rgba_channels = True + missing_rgba_review_channels = True self.log.error(exc) self.log.error( "Skipping OIIO Transcode. Unknown RGBA channels" @@ -195,7 +195,7 @@ class ExtractOIIOTranscode(publish.Extractor): ) break - if unknown_rgba_channels: + if missing_rgba_review_channels: # Stop processing this representation break From b8ea018b43b7256676362f699d6f66df848cb710 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 May 2025 11:18:37 +0200 Subject: [PATCH 014/115] Clarify exception --- client/ayon_core/lib/transcoding.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index e665486d7b..57ae6cb0bc 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -68,7 +68,12 @@ VIDEO_EXTENSIONS = { class MissingRGBAChannelsError(ValueError): - """Raised when we can't find RGB channels for conversion in input media.""" + """Raised when we can't find channels to use as RGBA for conversion in + input media. + + This may be other channels than solely RGBA, like Z-channel. The error is + raised when no matching 'reviewable' channel was found. + """ pass From 526e5bfabb2af995863ef652fb34635002f37641 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 May 2025 12:01:14 +0200 Subject: [PATCH 015/115] Add unittest --- .../client/ayon_core/lib/test_transcoding.py | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 tests/client/ayon_core/lib/test_transcoding.py diff --git a/tests/client/ayon_core/lib/test_transcoding.py b/tests/client/ayon_core/lib/test_transcoding.py new file mode 100644 index 0000000000..f637e8bd9e --- /dev/null +++ b/tests/client/ayon_core/lib/test_transcoding.py @@ -0,0 +1,135 @@ +import unittest + +from ayon_core.lib.transcoding import ( + get_review_info_by_layer_name +) + + +class GetReviewInfoByLayerName(unittest.TestCase): + """Test responses from `get_review_info_by_layer_name`""" + def test_rgba_channels(self): + + # RGB is supported + info = get_review_info_by_layer_name(["R", "G", "B"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "R", + "G": "G", + "B": "B", + "A": None, + } + }]) + + # rgb is supported + info = get_review_info_by_layer_name(["r", "g", "b"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "r", + "G": "g", + "B": "b", + "A": None, + } + }]) + + # diffuse.[RGB] is supported + info = get_review_info_by_layer_name( + ["diffuse.R", "diffuse.G", "diffuse.B"] + ) + self.assertEqual(info, [{ + "name": "diffuse", + "review_channels": { + "R": "diffuse.R", + "G": "diffuse.G", + "B": "diffuse.B", + "A": None, + } + }]) + + info = get_review_info_by_layer_name(["R", "G", "B", "A"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "R", + "G": "G", + "B": "B", + "A": "A", + } + }]) + + def test_z_channel(self): + + info = get_review_info_by_layer_name(["Z"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "Z", + "G": "Z", + "B": "Z", + "A": None, + } + }]) + + info = get_review_info_by_layer_name(["Z", "A"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "Z", + "G": "Z", + "B": "Z", + "A": "A", + } + }]) + + + def test_unknown_channels(self): + info = get_review_info_by_layer_name(["hello", "world"]) + self.assertEqual(info, []) + + def test_rgba_priority(self): + """Ensure main layer, and RGB channels are prioritized + + If both Z and RGB channels are present for a layer name, then RGB + should be prioritized and the Z channel should be ignored. + + Also, the alpha channel from another "layer name" is not used. Note + how the diffuse response does not take A channel from the main layer. + + """ + + info = get_review_info_by_layer_name([ + "Z", + "diffuse.R", "diffuse.G", "diffuse.B", + "R", "G", "B", "A", + "specular.R", "specular.G", "specular.B", "specular.A", + ]) + self.assertEqual(info, [ + { + "name": "", + "review_channels": { + "R": "R", + "G": "G", + "B": "B", + "A": "A", + }, + }, + { + "name": "diffuse", + "review_channels": { + "R": "diffuse.R", + "G": "diffuse.G", + "B": "diffuse.B", + "A": None, + }, + }, + { + "name": "specular", + "review_channels": { + "R": "specular.R", + "G": "specular.G", + "B": "specular.B", + "A": "specular.A", + }, + }, + ]) From 591767152131c6be76db170f10a745a65e79fd5d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 May 2025 12:04:01 +0200 Subject: [PATCH 016/115] Add AR, AG, AB test case and fix behavior --- client/ayon_core/lib/transcoding.py | 2 +- .../client/ayon_core/lib/test_transcoding.py | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 57ae6cb0bc..895e220729 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -389,7 +389,7 @@ def get_review_info_by_layer_name(channel_names): layer_names_order.append(layer_name) # R, G, B, A or X, Y, Z, N, AR, AG, AB - channel = last_part[0].upper() + channel = last_part.upper() channels_by_layer_name[layer_name][channel] = channel_name # Put empty layer to the beginning of the list diff --git a/tests/client/ayon_core/lib/test_transcoding.py b/tests/client/ayon_core/lib/test_transcoding.py index f637e8bd9e..b9959e2958 100644 --- a/tests/client/ayon_core/lib/test_transcoding.py +++ b/tests/client/ayon_core/lib/test_transcoding.py @@ -82,6 +82,29 @@ class GetReviewInfoByLayerName(unittest.TestCase): } }]) + def test_ar_ag_ab_channels(self): + + info = get_review_info_by_layer_name(["AR", "AG", "AB"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "AR", + "G": "AG", + "B": "AB", + "A": None, + } + }]) + + info = get_review_info_by_layer_name(["AR", "AG", "AB", "A"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "AR", + "G": "AG", + "B": "AB", + "A": "A", + } + }]) def test_unknown_channels(self): info = get_review_info_by_layer_name(["hello", "world"]) From 00921e7806ab345037030fc8bb5ebd426840ce53 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 May 2025 12:05:28 +0200 Subject: [PATCH 017/115] Update client/ayon_core/lib/transcoding.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client/ayon_core/lib/transcoding.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 895e220729..a22b8aaf64 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -74,7 +74,6 @@ class MissingRGBAChannelsError(ValueError): This may be other channels than solely RGBA, like Z-channel. The error is raised when no matching 'reviewable' channel was found. """ - pass def get_transcode_temp_directory(): From 0c23ecc70d1dd2c2ea8e39c7f752b2cc406f379a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 20:52:08 +0200 Subject: [PATCH 018/115] Add support for red, green, blue and alpha --- client/ayon_core/lib/transcoding.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index a22b8aaf64..fc0d6bf8cd 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -377,6 +377,8 @@ def get_review_info_by_layer_name(channel_names): if last_part.lower() not in { # Detect RGBA channels "r", "g", "b", "a", + # Support fully written out rgba channel names + "red", "green", "blue", "alpha", # Allow detecting of x, y and z channels, and normal channels "x", "y", "z", "n", # red, green and blue alpha/opacity, for colored mattes @@ -410,10 +412,20 @@ def get_review_info_by_layer_name(channel_names): for layer_name in layer_names_order: channel_info = channels_by_layer_name[layer_name] + alpha = channel_info.get("A") + # RGB channels if all(channel in channel_info for channel in "RGB"): rgb = "R", "G", "B" + # RGB channels using fully written out channel names + elif all( + channel in channel_info + for channel in ("RED", "GREEN", "BLUE") + ): + rgb = "RED", "GREEN", "BLUE" + alpha = channel_info.get("ALPHA") + # XYZ channels (position pass) elif all(channel in channel_info for channel in "XYZ"): rgb = "X", "Y", "Z" @@ -443,7 +455,7 @@ def get_review_info_by_layer_name(channel_names): "R": red, "G": green, "B": blue, - "A": channel_info.get("A"), + "A": alpha, } }) return output From 55bfd79cf3cdfd4225b5e7ffdb9b660e3dd23592 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 20:55:21 +0200 Subject: [PATCH 019/115] Check against `.upper()` instead of `.lower()` to match strings more with how they are compared later in the code (improve style consistency) --- client/ayon_core/lib/transcoding.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index fc0d6bf8cd..643a056563 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -374,23 +374,23 @@ def get_review_info_by_layer_name(channel_names): if "." in channel_name: layer_name, last_part = channel_name.rsplit(".", 1) - if last_part.lower() not in { + # R, G, B, A or X, Y, Z, N, AR, AG, AB, RED, GREEN, BLUE, ALPHA + channel = last_part.upper() + if channel not in { # Detect RGBA channels - "r", "g", "b", "a", + "R", "G", "B", "A", # Support fully written out rgba channel names - "red", "green", "blue", "alpha", + "RED", "GREEN", "BLUE", "ALPHA", # Allow detecting of x, y and z channels, and normal channels - "x", "y", "z", "n", + "X", "Y", "Z", "N", # red, green and blue alpha/opacity, for colored mattes - "ar", "ag", "ab" + "AR", "AG", "AB" }: continue if layer_name not in layer_names_order: layer_names_order.append(layer_name) - # R, G, B, A or X, Y, Z, N, AR, AG, AB - channel = last_part.upper() channels_by_layer_name[layer_name][channel] = channel_name # Put empty layer to the beginning of the list From 80ba7ea5ed209d3b3eb0f51cb2025ae046c3ec87 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:12:30 +0200 Subject: [PATCH 020/115] implement new 'get_representation_path_v2' function --- client/ayon_core/pipeline/load/__init__.py | 2 + client/ayon_core/pipeline/load/utils.py | 56 ++++++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/load/__init__.py b/client/ayon_core/pipeline/load/__init__.py index 2a33fa119b..9ad2a17d3d 100644 --- a/client/ayon_core/pipeline/load/__init__.py +++ b/client/ayon_core/pipeline/load/__init__.py @@ -25,6 +25,7 @@ from .utils import ( get_loader_identifier, get_loaders_by_name, + get_representation_path_v2, get_representation_path_from_context, get_representation_path, get_representation_path_with_anatomy, @@ -85,6 +86,7 @@ __all__ = ( "get_loader_identifier", "get_loaders_by_name", + "get_representation_path_v2", "get_representation_path_from_context", "get_representation_path", "get_representation_path_with_anatomy", diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index d1731d4cf9..7842741c85 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -14,9 +14,8 @@ from ayon_core.lib import ( StringTemplate, TemplateUnsolved, ) -from ayon_core.pipeline import ( - Anatomy, -) +from ayon_core.lib.path_templates import TemplateResult +from ayon_core.pipeline import Anatomy log = logging.getLogger(__name__) @@ -638,6 +637,57 @@ def _fix_representation_context_compatibility(repre_context): repre_context["udim"] = udim[0] +def get_representation_path_v2( + project_name: str, + repre_entity: dict[str, Any], + *, + anatomy: Optional[Anatomy] = None, + project_entity: Optional[dict[str, Any]] = None, +) -> TemplateResult: + """Get filled representation path. + + Args: + project_name (str): Project name. + repre_entity (dict[str, Any]): Representation entity. + anatomy (Optional[Anatomy]): Project anatomy. + project_entity (Optional[dict[str, Any]): Project entity. Is used to + initialize Anatomy and is not needed if 'anatomy' is passed in. + + Returns: + TemplateResult: Resolved path to representation. + + Raises: + InvalidRepresentationContext: When representation data are probably + invalid or not available. + + """ + if anatomy is None: + anatomy = Anatomy(project_name, project_entity=project_entity) + try: + template = repre_entity["attrib"]["template"] + + except KeyError: + raise InvalidRepresentationContext( + "Representation document does not" + " contain template in data ('data.template')" + ) + + try: + context = copy.deepcopy(repre_entity["context"]) + _fix_representation_context_compatibility(context) + context["root"] = anatomy.roots + + path = StringTemplate.format_strict_template(template, context) + + except TemplateUnsolved as exc: + raise InvalidRepresentationContext( + "Couldn't resolve representation template with available data." + f" Reason: {str(exc)}" + ) + + return path.normalized() + + def get_representation_path_from_context(context): """Preparation wrapper using only context as a argument""" from ayon_core.pipeline import get_current_project_name From 60ff1ddb0c0b095114ba2630c7b72ca237e189f9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:14:21 +0200 Subject: [PATCH 021/115] use the new function --- .../pipeline/farm/pyblish_functions.py | 6 +- client/ayon_core/pipeline/load/utils.py | 104 ++++++++---------- .../extract_usd_layer_contributions.py | 3 + 3 files changed, 52 insertions(+), 61 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 0d8e70f9d2..1c8925d290 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -11,7 +11,7 @@ import clique from ayon_core.lib import Logger from ayon_core.pipeline import ( get_current_project_name, - get_representation_path, + get_representation_path_v2, ) from ayon_core.pipeline.create import get_product_name from ayon_core.pipeline.farm.patterning import match_aov_pattern @@ -1044,7 +1044,9 @@ def get_resources(project_name, version_entity, extension=None): filtered.append(repre_entity) representation = filtered[0] - directory = get_representation_path(representation) + directory = get_representation_path_v2( + project_name, representation + ) print("Source: ", directory) resources = sorted( [ diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 7842741c85..267a8c80c7 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import uuid import platform @@ -5,6 +7,7 @@ import logging import inspect import collections import numbers +import copy from typing import Optional, Union, Any import ayon_api @@ -694,15 +697,15 @@ def get_representation_path_from_context(context): representation = context["representation"] project_entity = context.get("project") - root = None - if ( - project_entity - and project_entity["name"] != get_current_project_name() - ): - anatomy = Anatomy(project_entity["name"]) - root = anatomy.roots - - return get_representation_path(representation, root) + if project_entity: + project_name = project_entity["name"] + else: + project_name = get_current_project_name() + return get_representation_path_v2( + project_name, + representation, + project_entity=project_entity, + ) def get_representation_path_with_anatomy(repre_entity, anatomy): @@ -721,36 +724,18 @@ def get_representation_path_with_anatomy(repre_entity, anatomy): anatomy (Anatomy): Project anatomy object. Returns: - Union[None, TemplateResult]: None if path can't be received + TemplateResult: Resolved representation path. Raises: InvalidRepresentationContext: When representation data are probably invalid or not available. + """ - - try: - template = repre_entity["attrib"]["template"] - - except KeyError: - raise InvalidRepresentationContext(( - "Representation document does not" - " contain template in data ('data.template')" - )) - - try: - context = repre_entity["context"] - _fix_representation_context_compatibility(context) - context["root"] = anatomy.roots - - path = StringTemplate.format_strict_template(template, context) - - except TemplateUnsolved as exc: - raise InvalidRepresentationContext(( - "Couldn't resolve representation template with available data." - " Reason: {}".format(str(exc)) - )) - - return path.normalized() + return get_representation_path_v2( + anatomy.project_name, + repre_entity, + anatomy=anatomy, + ) def get_representation_path(representation, root=None): @@ -771,11 +756,12 @@ def get_representation_path(representation, root=None): """ if root is None: - from ayon_core.pipeline import get_current_project_name, Anatomy + from ayon_core.pipeline import get_current_project_name - anatomy = Anatomy(get_current_project_name()) - return get_representation_path_with_anatomy( - representation, anatomy + project_name = get_current_project_name() + return get_representation_path_v2( + project_name, + representation, ) def path_from_representation(): @@ -848,12 +834,13 @@ def get_representation_path(representation, root=None): def get_representation_path_by_names( - project_name: str, - folder_path: str, - product_name: str, - version_name: str, - representation_name: str, - anatomy: Optional[Anatomy] = None) -> Optional[str]: + project_name: str, + folder_path: str, + product_name: str, + version_name: str, + representation_name: str, + anatomy: Optional[Anatomy] = None +) -> Optional[TemplateResult]: """Get (latest) filepath for representation for folder and product. See `get_representation_by_names` for more details. @@ -870,22 +857,21 @@ def get_representation_path_by_names( representation_name ) if not representation: - return + return None - if not anatomy: - anatomy = Anatomy(project_name) - - if representation: - path = get_representation_path_with_anatomy(representation, anatomy) - return str(path).replace("\\", "/") + return get_representation_path_v2( + project_name, + representation, + anatomy=anatomy, + ) def get_representation_by_names( - project_name: str, - folder_path: str, - product_name: str, - version_name: Union[int, str], - representation_name: str, + project_name: str, + folder_path: str, + product_name: str, + version_name: Union[int, str], + representation_name: str, ) -> Optional[dict]: """Get representation entity for asset and subset. @@ -902,7 +888,7 @@ def get_representation_by_names( folder_entity = ayon_api.get_folder_by_path( project_name, folder_path, fields=["id"]) if not folder_entity: - return + return None if isinstance(product_name, dict) and "name" in product_name: # Allow explicitly passing subset document @@ -914,7 +900,7 @@ def get_representation_by_names( folder_id=folder_entity["id"], fields=["id"]) if not product_entity: - return + return None if version_name == "hero": version_entity = ayon_api.get_hero_version_by_product_id( @@ -926,7 +912,7 @@ def get_representation_by_names( version_entity = ayon_api.get_version_by_name( project_name, version_name, product_id=product_entity["id"]) if not version_entity: - return + return None return ayon_api.get_representation_by_name( project_name, representation_name, version_id=version_entity["id"]) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index 0dc9a5e34d..9db8c49a02 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -1,6 +1,7 @@ from operator import attrgetter import dataclasses import os +import platform from typing import Any, Dict, List import pyblish.api @@ -179,6 +180,8 @@ def get_instance_uri_path( # Ensure `None` for now is also a string path = str(path) + if platform.system().lower() == "windows": + path = path.replace("\\", "/") return path From d55ac4aa547d9f24f96b33a0c66ddf87f4c1186d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:34:47 +0200 Subject: [PATCH 022/115] Use 'get_representation_path' for both signatures. --- .../pipeline/farm/pyblish_functions.py | 4 +- client/ayon_core/pipeline/load/__init__.py | 4 +- client/ayon_core/pipeline/load/utils.py | 240 ++++++++---------- 3 files changed, 115 insertions(+), 133 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 1c8925d290..0dda91914c 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -11,7 +11,7 @@ import clique from ayon_core.lib import Logger from ayon_core.pipeline import ( get_current_project_name, - get_representation_path_v2, + get_representation_path, ) from ayon_core.pipeline.create import get_product_name from ayon_core.pipeline.farm.patterning import match_aov_pattern @@ -1044,7 +1044,7 @@ def get_resources(project_name, version_entity, extension=None): filtered.append(repre_entity) representation = filtered[0] - directory = get_representation_path_v2( + directory = get_representation_path( project_name, representation ) print("Source: ", directory) diff --git a/client/ayon_core/pipeline/load/__init__.py b/client/ayon_core/pipeline/load/__init__.py index 9ad2a17d3d..3bc8d4ba1f 100644 --- a/client/ayon_core/pipeline/load/__init__.py +++ b/client/ayon_core/pipeline/load/__init__.py @@ -25,7 +25,7 @@ from .utils import ( get_loader_identifier, get_loaders_by_name, - get_representation_path_v2, + get_representation_path, get_representation_path_from_context, get_representation_path, get_representation_path_with_anatomy, @@ -86,7 +86,7 @@ __all__ = ( "get_loader_identifier", "get_loaders_by_name", - "get_representation_path_v2", + "get_representation_path", "get_representation_path_from_context", "get_representation_path", "get_representation_path_with_anatomy", diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 267a8c80c7..7cf96a5409 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -2,12 +2,13 @@ from __future__ import annotations import os import uuid -import platform +import warnings import logging import inspect import collections import numbers import copy +from functools import wraps from typing import Optional, Union, Any import ayon_api @@ -640,57 +641,6 @@ def _fix_representation_context_compatibility(repre_context): repre_context["udim"] = udim[0] -def get_representation_path_v2( - project_name: str, - repre_entity: dict[str, Any], - *, - anatomy: Optional[Anatomy] = None, - project_entity: Optional[dict[str, Any]] = None, -) -> TemplateResult: - """Get filled representation path. - - Args: - project_name (str): Project name. - repre_entity (dict[str, Any]): Representation entity. - anatomy (Optional[Anatomy]): Project anatomy. - project_entity (Optional[dict[str, Any]): Project entity. Is used to - initialize Anatomy and is not needed if 'anatomy' is passed in. - - Returns: - TemplateResult: Resolved path to representation. - - Raises: - InvalidRepresentationContext: When representation data are probably - invalid or not available. - - """ - if anatomy is None: - anatomy = Anatomy(project_name, project_entity=project_entity) - try: - template = repre_entity["attrib"]["template"] - - except KeyError: - raise InvalidRepresentationContext( - "Representation document does not" - " contain template in data ('data.template')" - ) - - try: - context = copy.deepcopy(repre_entity["context"]) - _fix_representation_context_compatibility(context) - context["root"] = anatomy.roots - - path = StringTemplate.format_strict_template(template, context) - - except TemplateUnsolved as exc: - raise InvalidRepresentationContext( - "Couldn't resolve representation template with available data." - f" Reason: {str(exc)}" - ) - - return path.normalized() - - def get_representation_path_from_context(context): """Preparation wrapper using only context as a argument""" from ayon_core.pipeline import get_current_project_name @@ -701,7 +651,7 @@ def get_representation_path_from_context(context): project_name = project_entity["name"] else: project_name = get_current_project_name() - return get_representation_path_v2( + return _get_representation_path( project_name, representation, project_entity=project_entity, @@ -731,106 +681,138 @@ def get_representation_path_with_anatomy(repre_entity, anatomy): invalid or not available. """ - return get_representation_path_v2( + return get_representation_path( anatomy.project_name, repre_entity, anatomy=anatomy, ) -def get_representation_path(representation, root=None): - """Get filename from representation document - - There are three ways of getting the path from representation which are - tried in following sequence until successful. - 1. Get template from representation['data']['template'] and data from - representation['context']. Then format template with the data. - 2. Get template from project['config'] and format it with default data set - 3. Get representation['data']['path'] and use it directly +def get_representation_path_with_roots( + representation: dict[str, Any], + roots: dict[str, str], +) -> Optional[TemplateResult]: + """Get filename from representation with custom root. Args: - representation(dict): representation document from the database + representation(dict): Representation entity. + roots (dict[str, str]): Roots to use. + Returns: - str: fullpath of the representation + Optional[TemplateResult]: Resolved representation path. """ - if root is None: - from ayon_core.pipeline import get_current_project_name + try: + template = representation["attrib"]["template"] + except KeyError: + return None - project_name = get_current_project_name() - return get_representation_path_v2( - project_name, - representation, + try: + context = representation["context"] + + _fix_representation_context_compatibility(context) + + context["root"] = roots + path = StringTemplate.format_strict_template( + template, context + ) + except (TemplateUnsolved, KeyError): + # Template references unavailable data + return None + + return path.normalized() + + +def _get_representation_path( + project_name: str, + repre_entity: dict[str, Any], + *, + anatomy: Optional[Anatomy] = None, + project_entity: Optional[dict[str, Any]] = None, +) -> TemplateResult: + """Get filled representation path. + + Args: + project_name (str): Project name. + repre_entity (dict[str, Any]): Representation entity. + anatomy (Optional[Anatomy]): Project anatomy. + project_entity (Optional[dict[str, Any]): Project entity. Is used to + initialize Anatomy and is not needed if 'anatomy' is passed in. + + Returns: + TemplateResult: Resolved path to representation. + + Raises: + InvalidRepresentationContext: When representation data are probably + invalid or not available. + + """ + if anatomy is None: + anatomy = Anatomy(project_name, project_entity=project_entity) + + try: + template = repre_entity["attrib"]["template"] + + except KeyError: + raise InvalidRepresentationContext( + "Representation document does not" + " contain template in data ('data.template')" ) - def path_from_representation(): - try: - template = representation["attrib"]["template"] - except KeyError: - return None + try: + context = copy.deepcopy(repre_entity["context"]) + _fix_representation_context_compatibility(context) + context["root"] = anatomy.roots - try: - context = representation["context"] + path = StringTemplate.format_strict_template(template, context) - _fix_representation_context_compatibility(context) + except TemplateUnsolved as exc: + raise InvalidRepresentationContext( + "Couldn't resolve representation template with available data." + f" Reason: {str(exc)}" + ) - context["root"] = root - path = StringTemplate.format_strict_template( - template, context - ) - # Force replacing backslashes with forward slashed if not on - # windows - if platform.system().lower() != "windows": - path = path.replace("\\", "/") - except (TemplateUnsolved, KeyError): - # Template references unavailable data - return None + return path.normalized() - if not path: - return path - normalized_path = os.path.normpath(path) - if os.path.exists(normalized_path): - return normalized_path - return path +def _get_representation_path_decorator(func): + @wraps(_get_representation_path) + def inner(arg_1, *args, **kwargs): + if isinstance(arg_1, str): + return _get_representation_path(arg_1, *args, **kwargs) + warnings.warn( + ( + "Used deprecated variant of 'get_representation_path'." + " Please change used arguments signature to follow" + " new definiton." + ), + DeprecationWarning, + stacklevel=2, + ) + return get_representation_path_with_roots(arg_1, *args, **kwargs) + return inner - def path_from_data(): - if "path" not in representation["attrib"]: - return None - path = representation["attrib"]["path"] - # Force replacing backslashes with forward slashed if not on - # windows - if platform.system().lower() != "windows": - path = path.replace("\\", "/") +@_get_representation_path_decorator +def get_representation_path(*args, **kwargs) -> TemplateResult: + """Get filled representation path. - if os.path.exists(path): - return os.path.normpath(path) + Args: + project_name (str): Project name. + repre_entity (dict[str, Any]): Representation entity. + anatomy (Optional[Anatomy]): Project anatomy. + project_entity (Optional[dict[str, Any]): Project entity. Is used to + initialize Anatomy and is not needed if 'anatomy' is passed in. - dir_path, file_name = os.path.split(path) - if not os.path.exists(dir_path): - return None + Returns: + TemplateResult: Resolved path to representation. - base_name, ext = os.path.splitext(file_name) - file_name_items = None - if "#" in base_name: - file_name_items = [part for part in base_name.split("#") if part] - elif "%" in base_name: - file_name_items = base_name.split("%") - - if not file_name_items: - return None - - filename_start = file_name_items[0] - - for _file in os.listdir(dir_path): - if _file.startswith(filename_start) and _file.endswith(ext): - return os.path.normpath(path) - - return ( - path_from_representation() or path_from_data() - ) + Raises: + InvalidRepresentationContext: When representation data are probably + invalid or not available. + """ + pass def get_representation_path_by_names( @@ -859,7 +841,7 @@ def get_representation_path_by_names( if not representation: return None - return get_representation_path_v2( + return _get_representation_path( project_name, representation, anatomy=anatomy, From c9bb43059db7891da4b1cf9c30af8d2f4b5bc46c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:36:56 +0200 Subject: [PATCH 023/115] remove doubled import --- client/ayon_core/pipeline/load/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/__init__.py b/client/ayon_core/pipeline/load/__init__.py index 3bc8d4ba1f..b5b09a5dc9 100644 --- a/client/ayon_core/pipeline/load/__init__.py +++ b/client/ayon_core/pipeline/load/__init__.py @@ -27,7 +27,6 @@ from .utils import ( get_representation_path, get_representation_path_from_context, - get_representation_path, get_representation_path_with_anatomy, is_compatible_loader, @@ -88,7 +87,6 @@ __all__ = ( "get_representation_path", "get_representation_path_from_context", - "get_representation_path", "get_representation_path_with_anatomy", "is_compatible_loader", From 8c61e655216fca3597808707c4751d6db59e15d2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:52:59 +0200 Subject: [PATCH 024/115] handle backwards compatibility properly --- client/ayon_core/pipeline/load/utils.py | 42 ++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 7cf96a5409..9dcb2e3b43 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -778,9 +778,19 @@ def _get_representation_path( def _get_representation_path_decorator(func): @wraps(_get_representation_path) - def inner(arg_1, *args, **kwargs): - if isinstance(arg_1, str): - return _get_representation_path(arg_1, *args, **kwargs) + def inner(*args, **kwargs): + from ayon_core.pipeline import get_current_project_name + + # Decide which variant of the function based on passed arguments + # will be used. + if args: + arg_1 = args[0] + if isinstance(arg_1, str): + return _get_representation_path(*args, **kwargs) + + elif "project_name" in kwargs: + return _get_representation_path(*args, **kwargs) + warnings.warn( ( "Used deprecated variant of 'get_representation_path'." @@ -790,7 +800,31 @@ def _get_representation_path_decorator(func): DeprecationWarning, stacklevel=2, ) - return get_representation_path_with_roots(arg_1, *args, **kwargs) + + # Find out which arguments were passed + if args: + representation = args[0] + else: + representation = kwargs.get("representation") + + if len(args) > 1: + roots = args[1] + else: + roots = kwargs.get("root") + + if roots is not None: + return get_representation_path_with_roots( + representation, roots + ) + + project_name = ( + representation["context"].get("project", {}).get("name") + ) + if project_name is None: + project_name = get_current_project_name() + + return _get_representation_path(project_name, representation) + return inner From efcd4425b7acf0f46a949668522530fa007ee231 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:53:45 +0200 Subject: [PATCH 025/115] add signature to the original function --- client/ayon_core/pipeline/load/utils.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 9dcb2e3b43..b3582ed0a7 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -829,7 +829,12 @@ def _get_representation_path_decorator(func): @_get_representation_path_decorator -def get_representation_path(*args, **kwargs) -> TemplateResult: +def get_representation_path(project_name: str, + repre_entity: dict[str, Any], + *, + anatomy: Optional[Anatomy] = None, + project_entity: Optional[dict[str, Any]] = None, +) -> TemplateResult: """Get filled representation path. Args: @@ -846,7 +851,12 @@ def get_representation_path(*args, **kwargs) -> TemplateResult: InvalidRepresentationContext: When representation data are probably invalid or not available. """ - pass + return _get_representation_path( + project_name, + repre_entity, + anatomy=anatomy, + project_entity=project_entity, + ) def get_representation_path_by_names( From 80f84e95fc02e8f4b72e077e412213ae5fae4540 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:54:02 +0200 Subject: [PATCH 026/115] add formatting --- client/ayon_core/pipeline/load/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index b3582ed0a7..900740cadc 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -850,6 +850,7 @@ def get_representation_path(project_name: str, Raises: InvalidRepresentationContext: When representation data are probably invalid or not available. + """ return _get_representation_path( project_name, From dcf5db31d042eb9ad1f7325ca51f75107490151c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Sep 2025 12:00:28 +0200 Subject: [PATCH 027/115] formatting fix --- client/ayon_core/pipeline/load/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 900740cadc..1d0a0e54e4 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -829,7 +829,8 @@ def _get_representation_path_decorator(func): @_get_representation_path_decorator -def get_representation_path(project_name: str, +def get_representation_path( + project_name: str, repre_entity: dict[str, Any], *, anatomy: Optional[Anatomy] = None, From 725e0f5a11298e119600b58321d628f19be4779b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 3 Oct 2025 17:17:47 +0200 Subject: [PATCH 028/115] get rid of private function --- client/ayon_core/pipeline/load/utils.py | 146 +++++++++--------------- 1 file changed, 57 insertions(+), 89 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 1d0a0e54e4..371ce300ba 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -651,7 +651,7 @@ def get_representation_path_from_context(context): project_name = project_entity["name"] else: project_name = get_current_project_name() - return _get_representation_path( + return get_representation_path( project_name, representation, project_entity=project_entity, @@ -724,7 +724,60 @@ def get_representation_path_with_roots( return path.normalized() -def _get_representation_path( +def _get_representation_path_decorator(func): + @wraps(func) + def inner(*args, **kwargs): + from ayon_core.pipeline import get_current_project_name + + # Decide which variant of the function based on passed arguments + # will be used. + if args: + arg_1 = args[0] + if isinstance(arg_1, str): + return func(*args, **kwargs) + + elif "project_name" in kwargs: + return func(*args, **kwargs) + + warnings.warn( + ( + "Used deprecated variant of 'get_representation_path'." + " Please change used arguments signature to follow" + " new definition." + ), + DeprecationWarning, + stacklevel=2, + ) + + # Find out which arguments were passed + if args: + representation = args[0] + else: + representation = kwargs.get("representation") + + if len(args) > 1: + roots = args[1] + else: + roots = kwargs.get("root") + + if roots is not None: + return get_representation_path_with_roots( + representation, roots + ) + + project_name = ( + representation["context"].get("project", {}).get("name") + ) + if project_name is None: + project_name = get_current_project_name() + + return func(project_name, representation) + + return inner + + +@_get_representation_path_decorator +def get_representation_path( project_name: str, repre_entity: dict[str, Any], *, @@ -743,7 +796,7 @@ def _get_representation_path( Returns: TemplateResult: Resolved path to representation. - Raises: + Raises: InvalidRepresentationContext: When representation data are probably invalid or not available. @@ -776,91 +829,6 @@ def _get_representation_path( return path.normalized() -def _get_representation_path_decorator(func): - @wraps(_get_representation_path) - def inner(*args, **kwargs): - from ayon_core.pipeline import get_current_project_name - - # Decide which variant of the function based on passed arguments - # will be used. - if args: - arg_1 = args[0] - if isinstance(arg_1, str): - return _get_representation_path(*args, **kwargs) - - elif "project_name" in kwargs: - return _get_representation_path(*args, **kwargs) - - warnings.warn( - ( - "Used deprecated variant of 'get_representation_path'." - " Please change used arguments signature to follow" - " new definiton." - ), - DeprecationWarning, - stacklevel=2, - ) - - # Find out which arguments were passed - if args: - representation = args[0] - else: - representation = kwargs.get("representation") - - if len(args) > 1: - roots = args[1] - else: - roots = kwargs.get("root") - - if roots is not None: - return get_representation_path_with_roots( - representation, roots - ) - - project_name = ( - representation["context"].get("project", {}).get("name") - ) - if project_name is None: - project_name = get_current_project_name() - - return _get_representation_path(project_name, representation) - - return inner - - -@_get_representation_path_decorator -def get_representation_path( - project_name: str, - repre_entity: dict[str, Any], - *, - anatomy: Optional[Anatomy] = None, - project_entity: Optional[dict[str, Any]] = None, -) -> TemplateResult: - """Get filled representation path. - - Args: - project_name (str): Project name. - repre_entity (dict[str, Any]): Representation entity. - anatomy (Optional[Anatomy]): Project anatomy. - project_entity (Optional[dict[str, Any]): Project entity. Is used to - initialize Anatomy and is not needed if 'anatomy' is passed in. - - Returns: - TemplateResult: Resolved path to representation. - - Raises: - InvalidRepresentationContext: When representation data are probably - invalid or not available. - - """ - return _get_representation_path( - project_name, - repre_entity, - anatomy=anatomy, - project_entity=project_entity, - ) - - def get_representation_path_by_names( project_name: str, folder_path: str, @@ -887,7 +855,7 @@ def get_representation_path_by_names( if not representation: return None - return _get_representation_path( + return get_representation_path( project_name, representation, anatomy=anatomy, From f11800f1e77fc8537fa4cf31bbb4e26bb2777b44 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 3 Oct 2025 17:22:32 +0200 Subject: [PATCH 029/115] fix type hint --- client/ayon_core/pipeline/load/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 371ce300ba..225d4e0d44 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -833,7 +833,7 @@ def get_representation_path_by_names( project_name: str, folder_path: str, product_name: str, - version_name: str, + version_name: Union[int, str], representation_name: str, anatomy: Optional[Anatomy] = None ) -> Optional[TemplateResult]: From b665bf3f79be43ba548d46fac8b970270c7085a8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:10:39 +0200 Subject: [PATCH 030/115] add attribute to the function to be able to detect if new version should be used --- client/ayon_core/pipeline/load/utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 225d4e0d44..e1c9c31db6 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -725,6 +725,11 @@ def get_representation_path_with_roots( def _get_representation_path_decorator(func): + # Add an attribute to the function so addons can check if the new variant + # of the function is available. + # >>> getattr(get_representation_path, "version", None) == 2 + # >>> True + setattr(func, "version", 2) @wraps(func) def inner(*args, **kwargs): from ayon_core.pipeline import get_current_project_name From 4b2d2d5002c5ce3b6d1b8e126d43433778147353 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:33:49 +0200 Subject: [PATCH 031/115] added overload definitions --- client/ayon_core/pipeline/load/utils.py | 55 ++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index e1c9c31db6..13c560bbd4 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -9,7 +9,7 @@ import collections import numbers import copy from functools import wraps -from typing import Optional, Union, Any +from typing import Optional, Union, Any, overload import ayon_api @@ -730,6 +730,7 @@ def _get_representation_path_decorator(func): # >>> getattr(get_representation_path, "version", None) == 2 # >>> True setattr(func, "version", 2) + @wraps(func) def inner(*args, **kwargs): from ayon_core.pipeline import get_current_project_name @@ -781,6 +782,58 @@ def _get_representation_path_decorator(func): return inner +@overload +def get_representation_path( + representation: dict[str, Any], + root: Optional[dict[str, Any]] = None, +) -> TemplateResult: + """DEPRECATED Get filled representation path. + + Use 'get_representation_path' using the new function signature. + + Args: + representation (dict[str, Any]): Representation entity. + root (Optional[dict[str, Any]): Roots to fill the path. + + Returns: + TemplateResult: Resolved path to representation. + + Raises: + InvalidRepresentationContext: When representation data are probably + invalid or not available. + + """ + pass + + +@overload +def get_representation_path( + project_name: str, + repre_entity: dict[str, Any], + *, + anatomy: Optional[Anatomy] = None, + project_entity: Optional[dict[str, Any]] = None, +) -> TemplateResult: + """Get filled representation path. + + Args: + project_name (str): Project name. + repre_entity (dict[str, Any]): Representation entity. + anatomy (Optional[Anatomy]): Project anatomy. + project_entity (Optional[dict[str, Any]): Project entity. Is used to + initialize Anatomy and is not needed if 'anatomy' is passed in. + + Returns: + TemplateResult: Resolved path to representation. + + Raises: + InvalidRepresentationContext: When representation data are probably + invalid or not available. + + """ + pass + + @_get_representation_path_decorator def get_representation_path( project_name: str, From b1cba11f6b796e8a0ed5225c0c52c243648f74a7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 16 Oct 2025 11:32:36 +0200 Subject: [PATCH 032/115] Skips audio collection in editorial contexts. Prevents duplicate audio collection when editorial context already handles audio processing. - Introduces function to get audio instances. - Checks for existing audio instances to avoid duplication. - Skips default audio collection if audio is already provided. --- .../plugins/publish/collect_audio.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/client/ayon_core/plugins/publish/collect_audio.py b/client/ayon_core/plugins/publish/collect_audio.py index 2949ff1196..901c589ddc 100644 --- a/client/ayon_core/plugins/publish/collect_audio.py +++ b/client/ayon_core/plugins/publish/collect_audio.py @@ -46,6 +46,19 @@ class CollectAudio(pyblish.api.ContextPlugin): audio_product_name = "audioMain" def process(self, context): + # Make sure Editorial related products are excluded + # since those are maintained by ExtractOtioAudioTracks + audio_instances = self.get_audio_instances(context) + self.log.debug("Audio instances: {}".format(len(audio_instances))) + + # QUESTION: perhaps there is a better way to do this? + # This is having limitation for cases where no audio instance + # is found but still in editorial context. We should perhaps rather + # check if the instance is in particular editorial context. + if len(audio_instances) >= 1: + self.log.info("Audio provided from related instances") + return + # Fake filtering by family inside context plugin filtered_instances = [] for instance in pyblish.api.instances_by_plugin( @@ -102,6 +115,24 @@ class CollectAudio(pyblish.api.ContextPlugin): }] self.log.debug("Audio Data added to instance ...") + def get_audio_instances(self, context): + """Return only instances which are having audio in families + + Args: + context (pyblish.context): context of publisher + + Returns: + list: list of selected instances + """ + return [ + _i for _i in context + # filter only those with audio product type or family + # and also with reviewAudio data key + if bool("audio" in ( + _i.data.get("families", []) + [_i.data["productType"]]) + ) or _i.data.get("reviewAudio") + ] + def query_representations(self, project_name, folder_paths): """Query representations related to audio products for passed folders. From d8dab916196e6177d1a49c50ead3c320ec7f6f76 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 16 Oct 2025 11:32:55 +0200 Subject: [PATCH 033/115] Adds audio to sibling reviewable instances Ensures audio is added to sibling instances needing audio for reviewable media. - Checks for sibling instances with the same parent ID. - Adds audio information to those instances. --- .../publish/extract_otio_audio_tracks.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py index 3a450a4f33..77e71e587f 100644 --- a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py +++ b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py @@ -125,6 +125,31 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): }) inst.data["audio"] = audio_attr + # Make sure if the audio instance is having siblink instances + # which needs audio for reviewable media so it is also added + # to its instance data + # Retrieve instance data from parent instance shot instance. + parent_instance_id = inst.data["parent_instance_id"] + for sibl_instance in inst.context: + sibl_parent_instance_id = sibl_instance.data.get( + "parent_instance_id") + # make sure the instance is not the same instance + # and the parent instance id is the same + if ( + sibl_instance.id is not inst.id and + sibl_parent_instance_id == parent_instance_id + ): + self.log.info( + "Adding audio to Sibling instance: " + f"{sibl_instance.data['label']}" + ) + audio_attr = sibl_instance.data.get("audio") or [] + audio_attr.append({ + "filename": audio_fpath, + "offset": 0 + }) + sibl_instance.data["audio"] = audio_attr + # add generated audio file to created files for recycling if audio_fpath not in created_files: created_files.append(audio_fpath) From 0b51e17a8a6bfa1d411fab3442f4f7bbabaadc6f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 17 Oct 2025 16:21:32 +0200 Subject: [PATCH 034/115] Fixes audio duplication in sibling instances. Ensures audio is only added to relevant sibling instances, preventing duplication. - Prevents adding audio to the same instance. - Streamlines audio assignment logic. --- .../plugins/publish/extract_otio_audio_tracks.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py index 77e71e587f..925ea03964 100644 --- a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py +++ b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py @@ -65,9 +65,9 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): # remove full mixed audio file os.remove(audio_temp_fpath) - def add_audio_to_instances(self, audio_file, instances): + def add_audio_to_instances(self, audio_file, audio_instances): created_files = [] - for inst in instances: + for inst in audio_instances: name = inst.data["folderPath"] recycling_file = [f for f in created_files if name in f] @@ -134,11 +134,10 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): sibl_parent_instance_id = sibl_instance.data.get( "parent_instance_id") # make sure the instance is not the same instance + if sibl_instance.id == inst.id: + continue # and the parent instance id is the same - if ( - sibl_instance.id is not inst.id and - sibl_parent_instance_id == parent_instance_id - ): + if sibl_parent_instance_id == parent_instance_id: self.log.info( "Adding audio to Sibling instance: " f"{sibl_instance.data['label']}" From 680766418869b7a2f82f096c5c3af4ec85db4b3b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:04:09 +0200 Subject: [PATCH 035/115] rename decorator function --- client/ayon_core/pipeline/load/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 13c560bbd4..70e2936e6f 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -724,7 +724,7 @@ def get_representation_path_with_roots( return path.normalized() -def _get_representation_path_decorator(func): +def _backwards_compatibility_repre_path(func): # Add an attribute to the function so addons can check if the new variant # of the function is available. # >>> getattr(get_representation_path, "version", None) == 2 @@ -834,7 +834,7 @@ def get_representation_path( pass -@_get_representation_path_decorator +@_backwards_compatibility_repre_path def get_representation_path( project_name: str, repre_entity: dict[str, Any], From 50531fa35af7a93a695eca086b3590018fded2e4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 20 Oct 2025 14:23:39 +0200 Subject: [PATCH 036/115] added docstring --- client/ayon_core/pipeline/load/utils.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 70e2936e6f..7fb0c30331 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -725,6 +725,27 @@ def get_representation_path_with_roots( def _backwards_compatibility_repre_path(func): + """Wrapper handling backwards compatibility of 'get_representation_path'. + + Allows 'get_representation_path' to support old and new signatures of the + function. The old signature supported passing in representation entity + and optional roots. The new signature requires the project name + to be passed. In case custom roots should be used, a dedicated function + 'get_representation_path_with_roots' is available. + + The wrapper handles passed arguments, and based on kwargs and types + of the arguments will call the function which relates to + the arguments. + + The function is also marked with an attribute 'version' so other addons + can check if the function is using the new signature or is using + the old signature. That should allow addons to adapt to new signature. + >>> if getattr(get_representation_path, "version", None) == 2: + >>> path = get_representation_path(project_name, repre_entity) + >>> else: + >>> path = get_representation_path(repre_entity) + + """ # Add an attribute to the function so addons can check if the new variant # of the function is available. # >>> getattr(get_representation_path, "version", None) == 2 From fbf370befa36a0ea9f471338d9d05cbea2e1710a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 20 Oct 2025 14:23:53 +0200 Subject: [PATCH 037/115] raise from previous exception --- client/ayon_core/pipeline/load/utils.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 7fb0c30331..c4cf37d69d 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -886,11 +886,10 @@ def get_representation_path( try: template = repre_entity["attrib"]["template"] - except KeyError: + except KeyError as exc: raise InvalidRepresentationContext( - "Representation document does not" - " contain template in data ('data.template')" - ) + "Failed to receive template from representation entity." + ) from exc try: context = copy.deepcopy(repre_entity["context"]) @@ -901,9 +900,8 @@ def get_representation_path( except TemplateUnsolved as exc: raise InvalidRepresentationContext( - "Couldn't resolve representation template with available data." - f" Reason: {str(exc)}" - ) + "Failed to resolve representation template with available data." + ) from exc return path.normalized() From bd0320f56fd4ff2986f7afe4fa99f23a9c7702f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 20 Oct 2025 14:24:04 +0200 Subject: [PATCH 038/115] added planned break of backwards compatibility --- client/ayon_core/pipeline/load/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index c4cf37d69d..8aed7b8b52 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -745,6 +745,8 @@ def _backwards_compatibility_repre_path(func): >>> else: >>> path = get_representation_path(repre_entity) + The plan to remove backwards compatibility is 1.1.2026. + """ # Add an attribute to the function so addons can check if the new variant # of the function is available. @@ -770,7 +772,7 @@ def _backwards_compatibility_repre_path(func): ( "Used deprecated variant of 'get_representation_path'." " Please change used arguments signature to follow" - " new definition." + " new definition. Will be removed 1.1.2026." ), DeprecationWarning, stacklevel=2, From 34b292b06a2a5d0f999ed0093252b10081c9e186 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Oct 2025 15:33:55 +0200 Subject: [PATCH 039/115] revert audio collector changes --- .../plugins/publish/collect_audio.py | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_audio.py b/client/ayon_core/plugins/publish/collect_audio.py index 901c589ddc..2949ff1196 100644 --- a/client/ayon_core/plugins/publish/collect_audio.py +++ b/client/ayon_core/plugins/publish/collect_audio.py @@ -46,19 +46,6 @@ class CollectAudio(pyblish.api.ContextPlugin): audio_product_name = "audioMain" def process(self, context): - # Make sure Editorial related products are excluded - # since those are maintained by ExtractOtioAudioTracks - audio_instances = self.get_audio_instances(context) - self.log.debug("Audio instances: {}".format(len(audio_instances))) - - # QUESTION: perhaps there is a better way to do this? - # This is having limitation for cases where no audio instance - # is found but still in editorial context. We should perhaps rather - # check if the instance is in particular editorial context. - if len(audio_instances) >= 1: - self.log.info("Audio provided from related instances") - return - # Fake filtering by family inside context plugin filtered_instances = [] for instance in pyblish.api.instances_by_plugin( @@ -115,24 +102,6 @@ class CollectAudio(pyblish.api.ContextPlugin): }] self.log.debug("Audio Data added to instance ...") - def get_audio_instances(self, context): - """Return only instances which are having audio in families - - Args: - context (pyblish.context): context of publisher - - Returns: - list: list of selected instances - """ - return [ - _i for _i in context - # filter only those with audio product type or family - # and also with reviewAudio data key - if bool("audio" in ( - _i.data.get("families", []) + [_i.data["productType"]]) - ) or _i.data.get("reviewAudio") - ] - def query_representations(self, project_name, folder_paths): """Query representations related to audio products for passed folders. From 182e457505a15d8872a311b56147dbd600a67dac Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Oct 2025 15:35:12 +0200 Subject: [PATCH 040/115] Improve logic for checking already existing audio key --- client/ayon_core/plugins/publish/collect_audio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_audio.py b/client/ayon_core/plugins/publish/collect_audio.py index 2949ff1196..273e966cfd 100644 --- a/client/ayon_core/plugins/publish/collect_audio.py +++ b/client/ayon_core/plugins/publish/collect_audio.py @@ -52,7 +52,7 @@ class CollectAudio(pyblish.api.ContextPlugin): context, self.__class__ ): # Skip instances that already have audio filled - if instance.data.get("audio"): + if "audio" in instance.data: self.log.debug( "Skipping Audio collection. It is already collected" ) From d2fdae67e75e5a5b99c74793ba0b0210518b785e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Oct 2025 15:47:11 +0200 Subject: [PATCH 041/115] Adds audio instance attribute collection Adds a collector to identify audio instances and link them to sibling instances. This ensures that sibling instances, requiring audio for reviewable media, inherit audio attributes. The collector checks and links audio if: - The sibling instance shares the same parent ID. - The instance is not the audio instance itself. --- .../publish/extract_otio_audio_tracks.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py index 925ea03964..e0bea02082 100644 --- a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py +++ b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py @@ -8,6 +8,56 @@ from ayon_core.lib import ( run_subprocess ) +# pridat collector + +class CollectParentAudioInstanceAttribute(pyblish.api.ContextPlugin): + """Collect audio instance attribute""" + + order = pyblish.api.CollectorOrder + label = "Collect Audio Instance Attribute" + hosts = ["hiero", "resolve", "flame"] + + def process(self, context): + + audio_instances = self.get_audio_instances(context) + + for inst in audio_instances: + # Make sure if the audio instance is having siblink instances + # which needs audio for reviewable media so it is also added + # to its instance data + # Retrieve instance data from parent instance shot instance. + parent_instance_id = inst.data["parent_instance_id"] + for sibl_instance in inst.context: + sibl_parent_instance_id = sibl_instance.data.get( + "parent_instance_id") + # make sure the instance is not the same instance + if sibl_instance.id == inst.id: + continue + # and the parent instance id is the same + if sibl_parent_instance_id == parent_instance_id: + self.log.info( + "Adding audio to Sibling instance: " + f"{sibl_instance.data['label']}" + ) + sibl_instance.data["audio"] = None + + def get_audio_instances(self, context): + """Return only instances which are having audio in families + + Args: + context (pyblish.context): context of publisher + + Returns: + list: list of selected instances + """ + return [ + _i for _i in context + # filter only those with audio product type or family + # and also with reviewAudio data key + if bool("audio" in ( + _i.data.get("families", []) + [_i.data["productType"]]) + ) or _i.data.get("reviewAudio") + ] class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): """Extract Audio tracks from OTIO timeline. From 90852663d1ee89fb6734cd1793090c29eb5ee3d2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Oct 2025 15:49:15 +0200 Subject: [PATCH 042/115] ruff improvements --- client/ayon_core/plugins/publish/extract_otio_audio_tracks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py index e0bea02082..08e786f067 100644 --- a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py +++ b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py @@ -8,7 +8,6 @@ from ayon_core.lib import ( run_subprocess ) -# pridat collector class CollectParentAudioInstanceAttribute(pyblish.api.ContextPlugin): """Collect audio instance attribute""" @@ -59,6 +58,7 @@ class CollectParentAudioInstanceAttribute(pyblish.api.ContextPlugin): ) or _i.data.get("reviewAudio") ] + class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): """Extract Audio tracks from OTIO timeline. From 13e88e70a2f21ceabc31a99ff1e2b0c28af04bb2 Mon Sep 17 00:00:00 2001 From: Aleks Berland Date: Thu, 30 Oct 2025 13:32:26 -0400 Subject: [PATCH 043/115] Fix for missing workfiles details with "Published" filter on - Implemented `get_published_workfile_info` and `get_published_workfile_version_comment` methods in the WorkfilesModel. - Updated AbstractWorkfilesFrontend to define these methods as abstract. - Enhanced BaseWorkfileController to call the new methods. - Modified SidePanelWidget to handle published workfile context and display relevant information. --- client/ayon_core/tools/workfiles/abstract.py | 27 ++++ client/ayon_core/tools/workfiles/control.py | 10 ++ .../tools/workfiles/models/workfiles.py | 64 ++++++++- .../tools/workfiles/widgets/side_panel.py | 126 +++++++++++++----- 4 files changed, 189 insertions(+), 38 deletions(-) diff --git a/client/ayon_core/tools/workfiles/abstract.py b/client/ayon_core/tools/workfiles/abstract.py index 863d6bb9bc..e7a038575a 100644 --- a/client/ayon_core/tools/workfiles/abstract.py +++ b/client/ayon_core/tools/workfiles/abstract.py @@ -787,6 +787,33 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): """ pass + @abstractmethod + def get_published_workfile_info(self, representation_id: str): + """Get published workfile info by representation ID. + + Args: + representation_id (str): Representation id. + + Returns: + Optional[PublishedWorkfileInfo]: Published workfile info or None + if not found. + + """ + pass + + @abstractmethod + def get_published_workfile_version_comment(self, representation_id: str): + """Get version comment for published workfile. + + Args: + representation_id (str): Representation id. + + Returns: + Optional[str]: Version comment or None. + + """ + pass + @abstractmethod def get_workfile_info(self, folder_id, task_id, rootless_path): """Workfile info from database. diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py index f0e0f0e416..13a88e325f 100644 --- a/client/ayon_core/tools/workfiles/control.py +++ b/client/ayon_core/tools/workfiles/control.py @@ -432,6 +432,16 @@ class BaseWorkfileController( folder_id, task_id ) + def get_published_workfile_info(self, representation_id): + return self._workfiles_model.get_published_workfile_info( + representation_id + ) + + def get_published_workfile_version_comment(self, representation_id): + return self._workfiles_model.get_published_workfile_version_comment( + representation_id + ) + def get_workfile_info(self, folder_id, task_id, rootless_path): return self._workfiles_model.get_workfile_info( folder_id, task_id, rootless_path diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index 5b5591fe43..5055c203be 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -79,6 +79,7 @@ class WorkfilesModel: # Published workfiles self._repre_by_id = {} + self._version_by_repre_id = {} self._published_workfile_items_cache = NestedCacheItem( levels=1, default_factory=list ) @@ -95,6 +96,7 @@ class WorkfilesModel: self._workarea_file_items_cache.reset() self._repre_by_id = {} + self._version_by_repre_id = {} self._published_workfile_items_cache.reset() self._workfile_entities_by_task_id = {} @@ -586,7 +588,7 @@ class WorkfilesModel: version_entities = list(ayon_api.get_versions( project_name, product_ids=product_ids, - fields={"id", "author", "taskId"}, + fields={"id", "author", "taskId", "attrib.comment"}, )) repre_entities = [] @@ -600,6 +602,20 @@ class WorkfilesModel: repre_entity["id"]: repre_entity for repre_entity in repre_entities }) + + # Map versions by representation ID for easy lookup + version_by_id = { + version_entity["id"]: version_entity + for version_entity in version_entities + } + for repre_entity in repre_entities: + repre_id = repre_entity["id"] + version_id = repre_entity.get("versionId") + if version_id and version_id in version_by_id: + self._version_by_repre_id[repre_id] = version_by_id[ + version_id + ] + project_entity = self._controller.get_project_entity(project_name) prepared_data = ListPublishedWorkfilesOptionalData( @@ -626,6 +642,52 @@ class WorkfilesModel: ] return items + def get_published_workfile_info( + self, representation_id: str + ) -> Optional[PublishedWorkfileInfo]: + """Get published workfile info by representation ID. + + Args: + representation_id (str): Representation id. + + Returns: + Optional[PublishedWorkfileInfo]: Published workfile info or None + if not found. + + """ + if not representation_id: + return None + + # Search through all cached published workfile items + cache_items = self._published_workfile_items_cache._data_by_key + for folder_cache in cache_items.values(): + if folder_cache.is_valid: + for item in folder_cache.get_data(): + if item.representation_id == representation_id: + return item + return None + + def get_published_workfile_version_comment( + self, representation_id: str + ) -> Optional[str]: + """Get version comment for published workfile. + + Args: + representation_id (str): Representation id. + + Returns: + Optional[str]: Version comment or None. + + """ + if not representation_id: + return None + + version_entity = self._version_by_repre_id.get(representation_id) + if version_entity: + attrib = version_entity.get("attrib") or {} + return attrib.get("comment") + return None + @property def _project_name(self) -> str: return self._controller.get_current_project_name() diff --git a/client/ayon_core/tools/workfiles/widgets/side_panel.py b/client/ayon_core/tools/workfiles/widgets/side_panel.py index b1b91d9721..2834f1dec8 100644 --- a/client/ayon_core/tools/workfiles/widgets/side_panel.py +++ b/client/ayon_core/tools/workfiles/widgets/side_panel.py @@ -1,17 +1,13 @@ import datetime -from qtpy import QtWidgets, QtCore +from qtpy import QtCore, QtWidgets def file_size_to_string(file_size): if not file_size: return "N/A" size = 0 - size_ending_mapping = { - "KB": 1024 ** 1, - "MB": 1024 ** 2, - "GB": 1024 ** 3 - } + size_ending_mapping = {"KB": 1024**1, "MB": 1024**2, "GB": 1024**3} ending = "B" for _ending, _size in size_ending_mapping.items(): if file_size < _size: @@ -70,7 +66,11 @@ class SidePanelWidget(QtWidgets.QWidget): btn_description_save.clicked.connect(self._on_save_click) controller.register_event_callback( - "selection.workarea.changed", self._on_selection_change + "selection.workarea.changed", self._on_workarea_selection_change + ) + controller.register_event_callback( + "selection.representation.changed", + self._on_representation_selection_change, ) self._details_input = details_input @@ -82,10 +82,11 @@ class SidePanelWidget(QtWidgets.QWidget): self._task_id = None self._filepath = None self._rootless_path = None + self._representation_id = None self._orig_description = "" self._controller = controller - self._set_context(None, None, None, None) + self._set_context(None, None, None, None, None) def set_published_mode(self, published_mode): """Change published mode. @@ -95,14 +96,21 @@ class SidePanelWidget(QtWidgets.QWidget): """ self._description_widget.setVisible(not published_mode) + # Clear the context when switching modes to avoid showing stale data + self._set_context(None, None, None, None, None) - def _on_selection_change(self, event): + def _on_workarea_selection_change(self, event): folder_id = event["folder_id"] task_id = event["task_id"] filepath = event["path"] rootless_path = event["rootless_path"] - self._set_context(folder_id, task_id, rootless_path, filepath) + self._set_context(folder_id, task_id, rootless_path, filepath, None) + + def _on_representation_selection_change(self, event): + representation_id = event["representation_id"] + + self._set_context(None, None, None, None, representation_id) def _on_description_change(self): text = self._description_input.toPlainText() @@ -118,23 +126,45 @@ class SidePanelWidget(QtWidgets.QWidget): self._orig_description = description self._btn_description_save.setEnabled(False) - def _set_context(self, folder_id, task_id, rootless_path, filepath): + def _set_context( + self, folder_id, task_id, rootless_path, filepath, representation_id + ): workfile_info = None - # Check if folder, task and file are selected + published_workfile_info = None + + # Check if folder, task and file are selected (workarea mode) if folder_id and task_id and rootless_path: workfile_info = self._controller.get_workfile_info( folder_id, task_id, rootless_path ) - enabled = workfile_info is not None + # Check if representation is selected (published mode) + elif representation_id: + published_workfile_info = ( + self._controller.get_published_workfile_info(representation_id) + ) + + # Get version comment for published workfiles + version_comment = None + if representation_id and published_workfile_info: + version_comment = ( + self._controller.get_published_workfile_version_comment( + representation_id + ) + ) + + enabled = ( + workfile_info is not None or published_workfile_info is not None + ) self._details_input.setEnabled(enabled) - self._description_input.setEnabled(enabled) - self._btn_description_save.setEnabled(enabled) + self._description_input.setEnabled(workfile_info is not None) + self._btn_description_save.setEnabled(workfile_info is not None) self._folder_id = folder_id self._task_id = task_id self._filepath = filepath self._rootless_path = rootless_path + self._representation_id = representation_id # Disable inputs and remove texts if any required arguments are # missing @@ -144,19 +174,28 @@ class SidePanelWidget(QtWidgets.QWidget): self._description_input.setPlainText("") return - description = workfile_info.description - size_value = file_size_to_string(workfile_info.file_size) + # Use published workfile info if available, otherwise use workarea + # info + info = ( + published_workfile_info + if published_workfile_info + else workfile_info + ) + + description = info.description if hasattr(info, "description") else "" + size_value = file_size_to_string(info.file_size) # Append html string datetime_format = "%b %d %Y %H:%M:%S" - file_created = workfile_info.file_created - modification_time = workfile_info.file_modified + file_created = info.file_created + modification_time = info.file_modified if file_created: file_created = datetime.datetime.fromtimestamp(file_created) if modification_time: modification_time = datetime.datetime.fromtimestamp( - modification_time) + modification_time + ) user_items_by_name = self._controller.get_user_items_by_name() @@ -167,34 +206,47 @@ class SidePanelWidget(QtWidgets.QWidget): return username created_lines = [] - if workfile_info.created_by: - created_lines.append( - convert_username(workfile_info.created_by) - ) - if file_created: - created_lines.append(file_created.strftime(datetime_format)) + # For published workfiles, use 'author' field + if published_workfile_info: + if published_workfile_info.author: + created_lines.append( + convert_username(published_workfile_info.author) + ) + if file_created: + created_lines.append(file_created.strftime(datetime_format)) + else: + # For workarea workfiles, use 'created_by' field + if workfile_info.created_by: + created_lines.append( + convert_username(workfile_info.created_by) + ) + if file_created: + created_lines.append(file_created.strftime(datetime_format)) if created_lines: created_lines.insert(0, "Created:") modified_lines = [] - if workfile_info.updated_by: - modified_lines.append( - convert_username(workfile_info.updated_by) - ) + # For workarea workfiles, show 'updated_by' + if workfile_info and workfile_info.updated_by: + modified_lines.append(convert_username(workfile_info.updated_by)) if modification_time: - modified_lines.append( - modification_time.strftime(datetime_format) - ) + modified_lines.append(modification_time.strftime(datetime_format)) if modified_lines: modified_lines.insert(0, "Modified:") - lines = ( + lines = [ "Size:", size_value, - "
".join(created_lines), - "
".join(modified_lines), - ) + ] + # Add version comment for published workfiles + if version_comment: + lines.append(f"Comment:
{version_comment}") + if created_lines: + lines.append("
".join(created_lines)) + if modified_lines: + lines.append("
".join(modified_lines)) + self._orig_description = description self._description_input.setPlainText(description) From cb81a57dddf6024318af16dfbde4a7e63cf7546a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 30 Oct 2025 22:31:49 +0100 Subject: [PATCH 044/115] Allow visualizing Alpha only layers for review as a color matte --- client/ayon_core/lib/transcoding.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 1762881846..38033d084f 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -487,6 +487,11 @@ def get_review_info_by_layer_name(channel_names): elif "Z" in channel_info: rgb = "Z", "Z", "Z" + # Has only A channel (Alpha layer) + elif "A" in channel_info: + rgb = "A", "A", "A" + alpha = None + else: # No reviewable channels found continue From 66c6bdd96025f94ddb39fbcd5b442bd9d9f736c2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 30 Oct 2025 22:57:26 +0100 Subject: [PATCH 045/115] Do not fail on thumbnail creation if it can't resolve a reviewable channel with OIIO --- .../ayon_core/plugins/publish/extract_thumbnail.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index b5885178d0..2a43c12af3 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -17,6 +17,7 @@ from ayon_core.lib import ( run_subprocess, ) from ayon_core.lib.transcoding import ( + MissingRGBAChannelsError, oiio_color_convert, get_oiio_input_and_channel_args, get_oiio_info_for_input, @@ -477,7 +478,16 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): return False input_info = get_oiio_info_for_input(src_path, logger=self.log) - input_arg, channels_arg = get_oiio_input_and_channel_args(input_info) + try: + input_arg, channels_arg = get_oiio_input_and_channel_args( + input_info + ) + except MissingRGBAChannelsError: + self.log.debug( + "Unable to find relevant reviewable channel for thumbnail " + "creation" + ) + return False oiio_cmd = get_oiio_tool_args( "oiiotool", input_arg, src_path, From 5e877f9b05730ac9bc704d2741614e6fbeef9837 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 15:10:03 +0100 Subject: [PATCH 046/115] Copy over attrib to copy product groupping --- client/ayon_core/tools/push_to_project/models/integrate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index cacce44942..761eb175cd 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -952,6 +952,7 @@ class ProjectPushItemProcess: product_name, product_type, folder_id, + attribs=self._src_product_entity["attrib"] ) self._operations.create_entity( project_name, "product", product_entity From 758e232b6c14b872f27b3d5a22bda7fc4db762a0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 15:35:27 +0100 Subject: [PATCH 047/115] Copy over only limited set of attributes for safety --- client/ayon_core/tools/push_to_project/models/integrate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 761eb175cd..a72a74f805 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -948,11 +948,16 @@ class ProjectPushItemProcess: self._product_entity = product_entity return product_entity + src_attrib = self._src_product_entity["attrib"] + copied_attrib = { + "description": src_attrib.get("description"), + "productGroup": src_attrib.get("productGroup") + } product_entity = new_product_entity( product_name, product_type, folder_id, - attribs=self._src_product_entity["attrib"] + attribs=copied_attrib ) self._operations.create_entity( project_name, "product", product_entity From 43f7ace90e298c59a06983cc4857426ab85c1629 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 15:58:36 +0100 Subject: [PATCH 048/115] Make copy of selected attributes closer to existing --- .../tools/push_to_project/models/integrate.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index a72a74f805..b2a987e121 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -949,15 +949,20 @@ class ProjectPushItemProcess: return product_entity src_attrib = self._src_product_entity["attrib"] - copied_attrib = { - "description": src_attrib.get("description"), - "productGroup": src_attrib.get("productGroup") - } + + dst_attrib = {} + for key in { + "description", + "productGroup", + }: + if key in src_attrib: + dst_attrib[key] = src_attrib[key] + product_entity = new_product_entity( product_name, product_type, folder_id, - attribs=copied_attrib + attribs=dst_attrib ) self._operations.create_entity( project_name, "product", product_entity From 23fd59f23ae728b2eadf143ab938c0bec737441f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 16:04:23 +0100 Subject: [PATCH 049/115] Added version_up checkbox If selected it creates new version for existing product, otherwise it overwrites the version. --- .../tools/push_to_project/control.py | 1 + .../tools/push_to_project/ui/window.py | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index b4e0d56dfd..41cd9bf823 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -41,6 +41,7 @@ class PushToContextController: self._process_item_id = None self._use_original_name = False + self._version_up = False self.set_source(project_name, version_ids) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index f382ccce64..c392c6b519 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -153,7 +153,12 @@ class PushToContextSelectWindow(QtWidgets.QWidget): inputs_layout.addRow("New folder name", folder_name_input) inputs_layout.addRow("Variant", variant_input) inputs_layout.addRow( - "Use original product names", original_names_checkbox) + "Use original product names", original_names_checkbox + ) + version_up_checkbox = NiceCheckbox(True, parent=inputs_widget) + inputs_layout.addRow( + "Version up existing Product", version_up_checkbox + ) inputs_layout.addRow("Comment", comment_input) main_splitter.addWidget(context_widget) @@ -209,8 +214,11 @@ class PushToContextSelectWindow(QtWidgets.QWidget): "Show error detail dialog to copy full error." ) original_names_checkbox.setToolTip( - "Required for multi copy, doesn't allow changes " - "variant values." + "Required for multi copy, doesn't allow changes variant values." + ) + version_up_checkbox.setToolTip( + "Version up existing product. If not selected version will be " + "updated." ) overlay_close_btn = QtWidgets.QPushButton( @@ -259,6 +267,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget): library_only_checkbox.stateChanged.connect(self._on_library_only_change) original_names_checkbox.stateChanged.connect( self._on_original_names_change) + version_up_checkbox.stateChanged.connect( + self._on_version_up_checkbox_change) publish_btn.clicked.connect(self._on_select_click) cancel_btn.clicked.connect(self._on_close_click) @@ -328,6 +338,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._new_folder_name_input_text = None self._variant_input_text = None self._comment_input_text = None + self._version_up_checkbox = version_up_checkbox self._first_show = True self._show_timer = show_timer @@ -344,6 +355,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): show_detail_btn.setVisible(False) overlay_close_btn.setVisible(False) overlay_try_btn.setVisible(False) + version_up_checkbox.setChecked(False) # Support of public api function of controller def set_source(self, project_name, version_ids): @@ -424,6 +436,10 @@ class PushToContextSelectWindow(QtWidgets.QWidget): use_original_name = bool(state) self._invalidate_use_original_names(use_original_name) + def _on_version_up_checkbox_change(self, state: int) -> None: + self._controller._version_up = bool(state) + self._version_up_checkbox.setChecked(bool(state)) + def _on_user_input_timer(self): folder_name_enabled = self._new_folder_name_enabled folder_name = self._new_folder_name_input_text From 8bb4b2096a66cfd678c29c8c4b816c0140ad3295 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 16:04:54 +0100 Subject: [PATCH 050/115] Implemented propagation of version up value --- client/ayon_core/tools/push_to_project/control.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 41cd9bf823..56e587a4e2 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -203,6 +203,9 @@ class PushToContextController: return item_ids = [] + dst_version = 1 + if self._version_up: + dst_version = None for src_version_entity in self._src_version_entities: item_id = self._integrate_model.create_process_item( self._src_project_name, @@ -213,7 +216,7 @@ class PushToContextController: self._user_values.variant, comment=self._user_values.comment, new_folder_name=self._user_values.new_folder_name, - dst_version=1, + dst_version=dst_version, use_original_name=self._use_original_name, ) item_ids.append(item_id) From 23a6578d6fc0235b8ba2ec3b75ddef8695373bc9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 16:09:59 +0100 Subject: [PATCH 051/115] Formatting change --- .../tools/push_to_project/models/integrate.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index b2a987e121..8dd8d8145c 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -955,8 +955,9 @@ class ProjectPushItemProcess: "description", "productGroup", }: - if key in src_attrib: - dst_attrib[key] = src_attrib[key] + value = src_attrib.get(key) + if value: + dst_attrib[key] = value product_entity = new_product_entity( product_name, @@ -1001,8 +1002,9 @@ class ProjectPushItemProcess: "description", "intent", }: - if key in src_attrib: - dst_attrib[key] = src_attrib[key] + value = src_attrib.get(key) + if value: + dst_attrib[key] = value if version is None: last_version_entity = ayon_api.get_last_version_by_product_id( From 3bc92d88f04db94c9ffb9f7cbf9b2c1bd866a87c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 16:13:48 +0100 Subject: [PATCH 052/115] Reorganized lines --- client/ayon_core/tools/push_to_project/ui/window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index c392c6b519..7eb90425ab 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -144,6 +144,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget): variant_input.setPlaceholderText("< Variant >") variant_input.setObjectName("ValidatedLineEdit") + version_up_checkbox = NiceCheckbox(True, parent=inputs_widget) + comment_input = PlaceholderLineEdit(inputs_widget) comment_input.setPlaceholderText("< Publish comment >") @@ -155,7 +157,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget): inputs_layout.addRow( "Use original product names", original_names_checkbox ) - version_up_checkbox = NiceCheckbox(True, parent=inputs_widget) inputs_layout.addRow( "Version up existing Product", version_up_checkbox ) From 9713852deb28a5136c54156803723929f4067365 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 16:15:19 +0100 Subject: [PATCH 053/115] Unnecessary and wrong manual set --- client/ayon_core/tools/push_to_project/ui/window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 7eb90425ab..23aa710cd3 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -439,7 +439,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget): def _on_version_up_checkbox_change(self, state: int) -> None: self._controller._version_up = bool(state) - self._version_up_checkbox.setChecked(bool(state)) def _on_user_input_timer(self): folder_name_enabled = self._new_folder_name_enabled From 23b0378a0e0fffc68f206c7800f1552d9f9e764f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 16:17:49 +0100 Subject: [PATCH 054/115] Use public method to set private variable --- client/ayon_core/tools/push_to_project/control.py | 3 +++ client/ayon_core/tools/push_to_project/ui/window.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 56e587a4e2..8fd4b6053e 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -233,6 +233,9 @@ class PushToContextController: thread.start() return item_ids + def set_version_up(self, state): + self._version_up = state + def wait_for_process_thread(self): if self._process_thread is None: return diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 23aa710cd3..6be4d3c237 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -438,7 +438,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._invalidate_use_original_names(use_original_name) def _on_version_up_checkbox_change(self, state: int) -> None: - self._controller._version_up = bool(state) + self._controller.set_version_up(bool(state)) def _on_user_input_timer(self): folder_name_enabled = self._new_folder_name_enabled From 7229f5d794861e71842fd6c1d20707ea174c02a9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 16:57:50 +0100 Subject: [PATCH 055/115] Reworked hardcoded version to version_up variable 1 was used as hardcoded version, this way updated will be always last version if exists. Hardcoding 1 doesnt make sense with `get_versioning_start` which should be source of truth. Incoming value of version would make sense if we would like to start/reset specific version, which is unlikely (and currently impossible without updates to UI). --- .../tools/push_to_project/control.py | 5 +- .../tools/push_to_project/models/integrate.py | 49 ++++++++++--------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 8fd4b6053e..a24cedf455 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -203,9 +203,6 @@ class PushToContextController: return item_ids = [] - dst_version = 1 - if self._version_up: - dst_version = None for src_version_entity in self._src_version_entities: item_id = self._integrate_model.create_process_item( self._src_project_name, @@ -216,7 +213,7 @@ class PushToContextController: self._user_values.variant, comment=self._user_values.comment, new_folder_name=self._user_values.new_folder_name, - dst_version=dst_version, + version_up=self._version_up, use_original_name=self._use_original_name, ) item_ids.append(item_id) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index cacce44942..7130922ba0 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -89,7 +89,7 @@ class ProjectPushItem: variant, comment, new_folder_name, - dst_version, + version_up, item_id=None, use_original_name=False ): @@ -100,7 +100,7 @@ class ProjectPushItem: self.dst_project_name = dst_project_name self.dst_folder_id = dst_folder_id self.dst_task_name = dst_task_name - self.dst_version = dst_version + self.version_up = version_up self.variant = variant self.new_folder_name = new_folder_name self.comment = comment or "" @@ -118,7 +118,7 @@ class ProjectPushItem: str(self.dst_folder_id), str(self.new_folder_name), str(self.dst_task_name), - str(self.dst_version), + str(self.version_up), self.use_original_name ]) return self._repr_value @@ -133,7 +133,7 @@ class ProjectPushItem: "dst_project_name": self.dst_project_name, "dst_folder_id": self.dst_folder_id, "dst_task_name": self.dst_task_name, - "dst_version": self.dst_version, + "version_up": self.version_up, "variant": self.variant, "comment": self.comment, "new_folder_name": self.new_folder_name, @@ -962,7 +962,7 @@ class ProjectPushItemProcess: """Make sure version document exits in database.""" project_name = self._item.dst_project_name - version = self._item.dst_version + version_up = self._item.version_up src_version_entity = self._src_version_entity product_entity = self._product_entity product_id = product_entity["id"] @@ -993,24 +993,25 @@ class ProjectPushItemProcess: if key in src_attrib: dst_attrib[key] = src_attrib[key] - if version is None: - last_version_entity = ayon_api.get_last_version_by_product_id( - project_name, product_id + last_version_entity = ayon_api.get_last_version_by_product_id( + project_name, product_id + ) + if last_version_entity is None: + dst_version = get_versioning_start( + project_name, + self.host_name, + task_name=self._task_info.get("name"), + task_type=self._task_info.get("taskType"), + product_type=product_type, + product_name=product_entity["name"], ) - if last_version_entity: - version = int(last_version_entity["version"]) + 1 - else: - version = get_versioning_start( - project_name, - self.host_name, - task_name=self._task_info.get("name"), - task_type=self._task_info.get("taskType"), - product_type=product_type, - product_name=product_entity["name"], - ) + else: + dst_version = int(last_version_entity["version"]) + if version_up: + dst_version += 1 existing_version_entity = ayon_api.get_version_by_name( - project_name, version, product_id + project_name, dst_version, product_id ) thumbnail_id = self._copy_version_thumbnail() @@ -1032,7 +1033,7 @@ class ProjectPushItemProcess: copied_status = self._get_transferable_status(src_version_entity) version_entity = new_version_entity( - version, + dst_version, product_id, author=src_version_entity["author"], status=copied_status, @@ -1380,7 +1381,7 @@ class IntegrateModel: variant, comment, new_folder_name, - dst_version, + version_up, use_original_name ): """Create new item for integration. @@ -1394,7 +1395,7 @@ class IntegrateModel: variant (str): Variant name. comment (Union[str, None]): Comment. new_folder_name (Union[str, None]): New folder name. - dst_version (int): Destination version number. + version_up (bool): Should destination product be versioned up use_original_name (bool): If original product names should be used Returns: @@ -1411,7 +1412,7 @@ class IntegrateModel: variant, comment=comment, new_folder_name=new_folder_name, - dst_version=dst_version, + version_up=version_up, use_original_name=use_original_name ) process_item = ProjectPushItemProcess(self, item) From 9e6dd82c7477ab10f2c0640b467b863fbf1e705a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 17:23:18 +0100 Subject: [PATCH 056/115] Removed unnecessary call to private method --- client/ayon_core/tools/push_to_project/ui/window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index f382ccce64..c1308cece0 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -376,7 +376,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._invalidate_new_folder_name( new_folder_name, user_values["is_new_folder_name_valid"] ) - self._controller._invalidate() self._projects_combobox.refresh() def _on_first_show(self): From 87ba72eb002635d0cd1c69d8739db97a25a4150e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 4 Nov 2025 09:44:20 +0100 Subject: [PATCH 057/115] Typos --- client/ayon_core/plugins/publish/extract_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 580aa27eef..56863921c0 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -361,14 +361,14 @@ class ExtractReview(pyblish.api.InstancePlugin): if not filtered_output_defs: self.log.debug(( "Repre: {} - All output definitions were filtered" - " out by single frame filter. Skipping" + " out by single frame filter. Skipped." ).format(repre["name"])) continue # Skip if file is not set if first_input_path is None: self.log.warning(( - "Representation \"{}\" have empty files. Skipped." + "Representation \"{}\" has empty files. Skipped." ).format(repre["name"])) continue From 7648d6cc817ad993edd489a8fa8ca66f2d5a01c4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 4 Nov 2025 09:50:50 +0100 Subject: [PATCH 058/115] Fix passing on correct filename to the representation instead of including the `1001-1025%04d` pattern used solely for OIIO sequence conversion --- .../publish/extract_color_transcode.py | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index f80ca7e150..2aefd15fd9 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -117,8 +117,6 @@ class ExtractOIIOTranscode(publish.Extractor): repre_files_to_convert = copy.deepcopy(repre["files"]) else: repre_files_to_convert = [repre["files"]] - repre_files_to_convert = self._translate_to_sequence( - repre_files_to_convert) # Process each output definition for output_def in profile_output_defs: @@ -176,11 +174,17 @@ class ExtractOIIOTranscode(publish.Extractor): additional_command_args = (output_def["oiiotool_args"] ["additional_command_args"]) - files_to_convert = self._translate_to_sequence( - files_to_convert) - self.log.debug("Files to convert: {}".format(files_to_convert)) + sequence_files = self._translate_to_sequence(files_to_convert) + self.log.debug("Files to convert: {}".format(sequence_files)) missing_rgba_review_channels = False - for file_name in files_to_convert: + for file_name in sequence_files: + if isinstance(file_name, clique.Collection): + # Convert to filepath that can be directly converted + # by oiio like `frame.1001-1025%04d.exr` + file_name: str = file_name.format( + "{head}{range}{padding}{tail}" + ) + self.log.debug("Transcoding file: `{}`".format(file_name)) input_path = os.path.join(original_staging_dir, file_name) @@ -238,11 +242,11 @@ class ExtractOIIOTranscode(publish.Extractor): added_review = True # If there is only 1 file outputted then convert list to - # string, cause that'll indicate that its not a sequence. + # string, because that'll indicate that it is not a sequence. if len(new_repre["files"]) == 1: new_repre["files"] = new_repre["files"][0] - # If the source representation has "review" tag, but its not + # If the source representation has "review" tag, but it's not # part of the output definition tags, then both the # representations will be transcoded in ExtractReview and # their outputs will clash in integration. @@ -292,8 +296,7 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre["files"] = renamed_files def _translate_to_sequence(self, files_to_convert): - """Returns original list or list with filename formatted in single - sequence format. + """Returns original list or a clique.Collection of a sequence. Uses clique to find frame sequence, in this case it merges all frames into sequence format (FRAMESTART-FRAMEEND#) and returns it. @@ -302,32 +305,26 @@ class ExtractOIIOTranscode(publish.Extractor): Args: files_to_convert (list): list of file names Returns: - (list) of [file.1001-1010#.exr] or [fileA.exr, fileB.exr] + list[str | clique.Collection]: List of filepaths or a list + of Collections (usually one, unless there are holes) """ pattern = [clique.PATTERNS["frames"]] collections, _ = clique.assemble( files_to_convert, patterns=pattern, assume_padded_when_ambiguous=True) - if collections: if len(collections) > 1: raise ValueError( "Too many collections {}".format(collections)) collection = collections[0] - frames = list(collection.indexes) - if collection.holes().indexes: - return files_to_convert - - # Get the padding from the collection - # This is the number of digits used in the frame numbers - padding = collection.padding - - frame_str = "{}-{}%0{}d".format(frames[0], frames[-1], padding) - file_name = "{}{}{}".format(collection.head, frame_str, - collection.tail) - - files_to_convert = [file_name] + # TODO: Technically oiiotool supports holes in the sequence as well + # using the dedicated --frames argument to specify the frames. + # We may want to use that too so conversions of sequences with + # holes will perform faster as well. + # Separate the collection so that we have no holes/gaps per + # collection. + return collection.separate() return files_to_convert From 76dfbaeb68e23bebc0a263af6172ccea34b2e781 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 4 Nov 2025 09:55:18 +0100 Subject: [PATCH 059/115] Update docstring --- client/ayon_core/plugins/publish/extract_color_transcode.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 2aefd15fd9..1a2c85e597 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -298,9 +298,8 @@ class ExtractOIIOTranscode(publish.Extractor): def _translate_to_sequence(self, files_to_convert): """Returns original list or a clique.Collection of a sequence. - Uses clique to find frame sequence, in this case it merges all frames - into sequence format (FRAMESTART-FRAMEEND#) and returns it. - If sequence not found, it returns original list + Uses clique to find frame sequence Collection. + If sequence not found, it returns original list. Args: files_to_convert (list): list of file names From 6b6001dc42e4011da76344a1c161f3eef45f2001 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 4 Nov 2025 14:07:58 +0100 Subject: [PATCH 060/115] Refactored usage of Qt.CheckState --- .../tools/push_to_project/ui/window.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index c19a756bd7..b77cca0e09 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -319,6 +319,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._folder_name_input = folder_name_input self._comment_input = comment_input self._use_original_names_checkbox = original_names_checkbox + self._library_only_checkbox = library_only_checkbox self._publish_btn = publish_btn @@ -427,17 +428,18 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._comment_input_text = text self._user_input_changed_timer.start() - def _on_library_only_change(self, state: int) -> None: + def _on_library_only_change(self) -> None: """Change toggle state, reset filter, recalculate dropdown""" - state = bool(state) - self._projects_combobox.set_standard_filter_enabled(state) + is_checked = self._library_only_checkbox.isChecked() + self._projects_combobox.set_standard_filter_enabled(is_checked) - def _on_original_names_change(self, state: int) -> None: - use_original_name = bool(state) - self._invalidate_use_original_names(use_original_name) + def _on_original_names_change(self) -> None: + is_checked = self._use_original_names_checkbox.isChecked() + self._invalidate_use_original_names(is_checked) - def _on_version_up_checkbox_change(self, state: int) -> None: - self._controller.set_version_up(bool(state)) + def _on_version_up_checkbox_change(self) -> None: + is_checked = self._version_up_checkbox.isChecked() + self._controller.set_version_up(is_checked) def _on_user_input_timer(self): folder_name_enabled = self._new_folder_name_enabled From 64f511a43b2704d5e967884baa4e59d5de7861fd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:12:17 +0100 Subject: [PATCH 061/115] don't use set for value conversion --- client/ayon_core/lib/attribute_definitions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index cb74fea0f1..36c6429f5e 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -604,7 +604,11 @@ class EnumDef(AbstractAttrDef): if value is None: return copy.deepcopy(self.default) - return list(self._item_values.intersection(value)) + return [ + v + for v in value + if v in self._item_values + ] def is_value_valid(self, value: Any) -> bool: """Check if item is available in possible values.""" From 434098903979a79e3c06ab0399ce7f3c2db156f6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:12:45 +0100 Subject: [PATCH 062/115] avoid 'value' variable conflicts --- client/ayon_core/pipeline/create/structures.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index b2be377b42..1729d91c62 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -245,11 +245,11 @@ class AttributeValues: def _update(self, value): changes = {} - for key, value in dict(value).items(): - if key in self._data and self._data.get(key) == value: + for key, key_value in dict(value).items(): + if key in self._data and self._data.get(key) == key_value: continue - self._data[key] = value - changes[key] = value + self._data[key] = key_value + changes[key] = key_value return changes def _pop(self, key, default): From 026eb67e918b055caa7a68ee98ccfd75c07fd563 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:13:06 +0100 Subject: [PATCH 063/115] deepcopy value --- client/ayon_core/tools/publisher/models/create.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 5098826b8b..3f5352ae8b 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -1,5 +1,6 @@ import logging import re +import copy from typing import ( Union, List, @@ -1098,7 +1099,7 @@ class CreateModel: creator_attributes[key] = attr_def.default elif attr_def.is_value_valid(value): - creator_attributes[key] = value + creator_attributes[key] = copy.deepcopy(value) def _set_instances_publish_attr_values( self, instance_ids, plugin_name, key, value From 2ed1d42f35d4d95f34b4c71fef756c7c062b237a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:18:59 +0100 Subject: [PATCH 064/115] add comment to converted value --- client/ayon_core/pipeline/create/structures.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 1729d91c62..fecb3a5ca4 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -137,6 +137,7 @@ class AttributeValues: if value is None: continue converted_value = attr_def.convert_value(value) + # QUESTION Could we just use converted value all the time? if converted_value == value: self._data[attr_def.key] = value From 5dc462c62acc03c9619de980da5632543e9cf702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 5 Nov 2025 16:53:23 +0100 Subject: [PATCH 065/115] Update client/ayon_core/plugins/publish/extract_otio_audio_tracks.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../plugins/publish/extract_otio_audio_tracks.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py index 08e786f067..fce6eb4e93 100644 --- a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py +++ b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py @@ -49,14 +49,14 @@ class CollectParentAudioInstanceAttribute(pyblish.api.ContextPlugin): Returns: list: list of selected instances """ - return [ - _i for _i in context - # filter only those with audio product type or family - # and also with reviewAudio data key - if bool("audio" in ( - _i.data.get("families", []) + [_i.data["productType"]]) - ) or _i.data.get("reviewAudio") - ] + audio_instances = [] + for instance in context: + if ( + instace.data["productType"] == "audio" + or instace.data.get("reviewAudio") + ): + audio_instances.append(instance) + return audio_instances class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): From e4b3aafc9491ad3fdafdefb18ec4c5740af5e76d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 5 Nov 2025 17:07:53 +0100 Subject: [PATCH 066/115] Refactors audio instance collection for clarity Simplifies audio instance identification. The code now uses dedicated functions to collect and manage audio instances and their associations. --- .../publish/extract_otio_audio_tracks.py | 105 ++++++++---------- 1 file changed, 46 insertions(+), 59 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py index fce6eb4e93..6a955df725 100644 --- a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py +++ b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py @@ -1,5 +1,6 @@ import os import tempfile +import collections import pyblish @@ -9,54 +10,58 @@ from ayon_core.lib import ( ) +def get_audio_instances(context): + """Return only instances which are having audio in families + + Args: + context (pyblish.context): context of publisher + + Returns: + list: list of selected instances + """ + audio_instances = [] + for instance in context: + if not instance.data.get("parent_instance_id"): + continue + if ( + instance.data["productType"] == "audio" + or instance.data.get("reviewAudio") + ): + audio_instances.append(instance) + return audio_instances + + class CollectParentAudioInstanceAttribute(pyblish.api.ContextPlugin): """Collect audio instance attribute""" order = pyblish.api.CollectorOrder label = "Collect Audio Instance Attribute" - hosts = ["hiero", "resolve", "flame"] def process(self, context): - audio_instances = self.get_audio_instances(context) + audio_instances = get_audio_instances(context) - for inst in audio_instances: - # Make sure if the audio instance is having siblink instances - # which needs audio for reviewable media so it is also added - # to its instance data - # Retrieve instance data from parent instance shot instance. - parent_instance_id = inst.data["parent_instance_id"] - for sibl_instance in inst.context: - sibl_parent_instance_id = sibl_instance.data.get( - "parent_instance_id") - # make sure the instance is not the same instance - if sibl_instance.id == inst.id: - continue - # and the parent instance id is the same - if sibl_parent_instance_id == parent_instance_id: - self.log.info( - "Adding audio to Sibling instance: " - f"{sibl_instance.data['label']}" - ) - sibl_instance.data["audio"] = None - - def get_audio_instances(self, context): - """Return only instances which are having audio in families - - Args: - context (pyblish.context): context of publisher - - Returns: - list: list of selected instances - """ - audio_instances = [] + # create mapped instances by parent id + instances_by_parent_id = collections.defaultdict(list) for instance in context: - if ( - instace.data["productType"] == "audio" - or instace.data.get("reviewAudio") - ): - audio_instances.append(instance) - return audio_instances + parent_instance_id = instance.data.get("parent_instance_id") + if not parent_instance_id: + continue + instances_by_parent_id[parent_instance_id].append(instance) + + # distribute audio related attribute + for audio_instance in audio_instances: + parent_instance_id = audio_instance.data["parent_instance_id"] + + for sibl_instance in instances_by_parent_id[parent_instance_id]: + # exclude the same audio instance + if sibl_instance.id == audio_instance.id: + continue + self.log.info( + "Adding audio to Sibling instance: " + f"{sibl_instance.data['label']}" + ) + sibl_instance.data["audio"] = None class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): @@ -69,7 +74,6 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): order = pyblish.api.ExtractorOrder - 0.44 label = "Extract OTIO Audio Tracks" - hosts = ["hiero", "resolve", "flame"] def process(self, context): """Convert otio audio track's content to audio representations @@ -78,13 +82,14 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): context (pyblish.Context): context of publisher """ # split the long audio file to peces devided by isntances - audio_instances = self.get_audio_instances(context) - self.log.debug("Audio instances: {}".format(len(audio_instances))) + audio_instances = get_audio_instances(context) if len(audio_instances) < 1: self.log.info("No audio instances available") return + self.log.debug("Audio instances: {}".format(len(audio_instances))) + # get sequence otio_timeline = context.data["otioTimeline"] @@ -203,24 +208,6 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): if audio_fpath not in created_files: created_files.append(audio_fpath) - def get_audio_instances(self, context): - """Return only instances which are having audio in families - - Args: - context (pyblish.context): context of publisher - - Returns: - list: list of selected instances - """ - return [ - _i for _i in context - # filter only those with audio product type or family - # and also with reviewAudio data key - if bool("audio" in ( - _i.data.get("families", []) + [_i.data["productType"]]) - ) or _i.data.get("reviewAudio") - ] - def get_audio_track_items(self, otio_timeline): """Get all audio clips form OTIO audio tracks From 84db5d396517c74fd9634b3e0d5a8df0938acf52 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 5 Nov 2025 23:00:22 +0100 Subject: [PATCH 067/115] Update client/ayon_core/lib/transcoding.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/lib/transcoding.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 38033d084f..22396a5324 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -441,16 +441,16 @@ def get_review_info_by_layer_name(channel_names): def _sort(_layer_name: str) -> int: # Prioritize "" layer name # Prioritize layers with RGB channels - order = 0 if _layer_name == "rgba": - order -= 11 + return 0 + if _layer_name == "": - order -= 10 + return 1 channels = channels_by_layer_name[_layer_name] if all(channel in channels for channel in "RGB"): - order -= 1 - return order + return 2 + return 10 layer_names_order.sort(key=_sort) output = [] From f22ec30e34cd08e754e3721215e55eb446d81334 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 6 Nov 2025 11:48:50 +0100 Subject: [PATCH 068/115] Refactors audio extraction for correct publishing Updates audio extraction logic to address issues when publishing without an audio product. - Improves audio file handling and ensures correct representation assignments. - Adds helper functions for better code organization and readability. - Improves sibling instance processing. - Fixes an issue where audio wasn't extracted correctly for certain cases. --- .../publish/extract_otio_audio_tracks.py | 156 ++++++++++-------- 1 file changed, 88 insertions(+), 68 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py index 6a955df725..6ad7dd85db 100644 --- a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py +++ b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py @@ -1,13 +1,12 @@ +import collections +import hashlib import os import tempfile -import collections +import uuid +from pathlib import Path import pyblish - -from ayon_core.lib import ( - get_ffmpeg_tool_args, - run_subprocess -) +from ayon_core.lib import get_ffmpeg_tool_args, run_subprocess def get_audio_instances(context): @@ -31,6 +30,24 @@ def get_audio_instances(context): return audio_instances +def map_instances_by_parent_id(context): + """Create a mapping of instances by their parent id + + Args: + context (pyblish.context): context of publisher + + Returns: + dict: mapping of instances by their parent id + """ + instances_by_parent_id = collections.defaultdict(list) + for instance in context: + parent_instance_id = instance.data.get("parent_instance_id") + if not parent_instance_id: + continue + instances_by_parent_id[parent_instance_id].append(instance) + return instances_by_parent_id + + class CollectParentAudioInstanceAttribute(pyblish.api.ContextPlugin): """Collect audio instance attribute""" @@ -42,12 +59,7 @@ class CollectParentAudioInstanceAttribute(pyblish.api.ContextPlugin): audio_instances = get_audio_instances(context) # create mapped instances by parent id - instances_by_parent_id = collections.defaultdict(list) - for instance in context: - parent_instance_id = instance.data.get("parent_instance_id") - if not parent_instance_id: - continue - instances_by_parent_id[parent_instance_id].append(instance) + instances_by_parent_id = map_instances_by_parent_id(context) # distribute audio related attribute for audio_instance in audio_instances: @@ -75,6 +87,8 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): order = pyblish.api.ExtractorOrder - 0.44 label = "Extract OTIO Audio Tracks" + temp_dir = None + def process(self, context): """Convert otio audio track's content to audio representations @@ -99,8 +113,8 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): if not audio_inputs: return - # temp file - audio_temp_fpath = self.create_temp_file("audio") + # Convert all available audio into single file for trimming + audio_temp_fpath = self.create_temp_file("timeline_audio_track") # create empty audio with longest duration empty = self.create_empty(audio_inputs) @@ -114,19 +128,25 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): # remove empty os.remove(empty["mediaPath"]) + # create mapped instances by parent id + instances_by_parent_id = map_instances_by_parent_id(context) + # cut instance framerange and add to representations - self.add_audio_to_instances(audio_temp_fpath, audio_instances) + self.add_audio_to_instances( + audio_temp_fpath, audio_instances, instances_by_parent_id) # remove full mixed audio file os.remove(audio_temp_fpath) - def add_audio_to_instances(self, audio_file, audio_instances): + def add_audio_to_instances( + self, audio_file, audio_instances, instances_by_parent_id): created_files = [] - for inst in audio_instances: - name = inst.data["folderPath"] + for audio_instance in audio_instances: + folder_path = audio_instance.data["folderPath"] + file_suffix = folder_path.replace("/", "-") - recycling_file = [f for f in created_files if name in f] - audio_clip = inst.data["otioClip"] + recycling_file = [f for f in created_files if file_suffix in f] + audio_clip = audio_instance.data["otioClip"] audio_range = audio_clip.range_in_parent() duration = audio_range.duration.to_frames() @@ -139,74 +159,70 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): start_sec = relative_start_time.to_seconds() duration_sec = audio_range.duration.to_seconds() - # temp audio file - audio_fpath = self.create_temp_file(name) + # shot related audio file + shot_audio_fpath = self.create_temp_file(file_suffix) cmd = get_ffmpeg_tool_args( "ffmpeg", "-ss", str(start_sec), "-t", str(duration_sec), "-i", audio_file, - audio_fpath + shot_audio_fpath ) # run subprocess self.log.debug("Executing: {}".format(" ".join(cmd))) run_subprocess(cmd, logger=self.log) - else: - audio_fpath = recycling_file.pop() - if "audio" in ( - inst.data["families"] + [inst.data["productType"]] - ): + # add generated audio file to created files for recycling + if shot_audio_fpath not in created_files: + created_files.append(shot_audio_fpath) + else: + shot_audio_fpath = recycling_file.pop() + + # audio file needs to be published as representation + if audio_instance.data["productType"] == "audio": # create empty representation attr - if "representations" not in inst.data: - inst.data["representations"] = [] + if "representations" not in audio_instance.data: + audio_instance.data["representations"] = [] # add to representations - inst.data["representations"].append({ - "files": os.path.basename(audio_fpath), + audio_instance.data["representations"].append({ + "files": os.path.basename(shot_audio_fpath), "name": "wav", "ext": "wav", - "stagingDir": os.path.dirname(audio_fpath), + "stagingDir": os.path.dirname(shot_audio_fpath), "frameStart": 0, "frameEnd": duration }) - elif "reviewAudio" in inst.data.keys(): - audio_attr = inst.data.get("audio") or [] + # audio file needs to be reviewable too + elif "reviewAudio" in audio_instance.data.keys(): + audio_attr = audio_instance.data.get("audio") or [] audio_attr.append({ - "filename": audio_fpath, + "filename": shot_audio_fpath, "offset": 0 }) - inst.data["audio"] = audio_attr + audio_instance.data["audio"] = audio_attr # Make sure if the audio instance is having siblink instances # which needs audio for reviewable media so it is also added # to its instance data # Retrieve instance data from parent instance shot instance. - parent_instance_id = inst.data["parent_instance_id"] - for sibl_instance in inst.context: - sibl_parent_instance_id = sibl_instance.data.get( - "parent_instance_id") - # make sure the instance is not the same instance - if sibl_instance.id == inst.id: + parent_instance_id = audio_instance.data["parent_instance_id"] + for sibl_instance in instances_by_parent_id[parent_instance_id]: + # exclude the same audio instance + if sibl_instance.id == audio_instance.id: continue - # and the parent instance id is the same - if sibl_parent_instance_id == parent_instance_id: - self.log.info( - "Adding audio to Sibling instance: " - f"{sibl_instance.data['label']}" - ) - audio_attr = sibl_instance.data.get("audio") or [] - audio_attr.append({ - "filename": audio_fpath, - "offset": 0 - }) - sibl_instance.data["audio"] = audio_attr - - # add generated audio file to created files for recycling - if audio_fpath not in created_files: - created_files.append(audio_fpath) + self.log.info( + "Adding audio to Sibling instance: " + f"{sibl_instance.data['label']}" + ) + audio_attr = sibl_instance.data.get("audio") or [] + audio_attr.append({ + "filename": shot_audio_fpath, + "offset": 0 + }) + sibl_instance.data["audio"] = audio_attr def get_audio_track_items(self, otio_timeline): """Get all audio clips form OTIO audio tracks @@ -382,19 +398,23 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): os.remove(filters_tmp_filepath) - def create_temp_file(self, name): + def create_temp_file(self, file_suffix): """Create temp wav file Args: - name (str): name to be used in file name + file_suffix (str): name to be used in file name Returns: str: temp fpath """ - name = name.replace("/", "_") - return os.path.normpath( - tempfile.mktemp( - prefix="pyblish_tmp_{}_".format(name), - suffix=".wav" - ) - ) + extension = ".wav" + # get 8 characters + hash = hashlib.md5(str(uuid.uuid4()).encode()).hexdigest()[:8] + file_name = f"{hash}_{file_suffix}{extension}" + + if not self.temp_dir: + audio_temp_dir = tempfile.mkdtemp(prefix="AYON_audio_") + self.temp_dir = Path(audio_temp_dir) + self.temp_dir.mkdir(parents=True, exist_ok=True) + + return (self.temp_dir / file_name).as_posix() From 6ae58b458477b15325634fa0e4771ca44044464d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 6 Nov 2025 12:34:47 +0100 Subject: [PATCH 069/115] Refactors: Avoids errors when publishing no audio Refactors the audio extraction process to avoid errors when no audio instances are present in the scene. - Prevents processing if no audio instances are found. - Ensures correct handling of missing audio data. - Renames temp directory variable for clarity. --- .../publish/extract_otio_audio_tracks.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py index 6ad7dd85db..1df96b2918 100644 --- a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py +++ b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py @@ -58,6 +58,10 @@ class CollectParentAudioInstanceAttribute(pyblish.api.ContextPlugin): audio_instances = get_audio_instances(context) + # no need to continue if no audio instances found + if not audio_instances: + return + # create mapped instances by parent id instances_by_parent_id = map_instances_by_parent_id(context) @@ -87,7 +91,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): order = pyblish.api.ExtractorOrder - 0.44 label = "Extract OTIO Audio Tracks" - temp_dir = None + temp_dir_path = None def process(self, context): """Convert otio audio track's content to audio representations @@ -98,8 +102,8 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): # split the long audio file to peces devided by isntances audio_instances = get_audio_instances(context) - if len(audio_instances) < 1: - self.log.info("No audio instances available") + # no need to continue if no audio instances found + if not audio_instances: return self.log.debug("Audio instances: {}".format(len(audio_instances))) @@ -412,9 +416,9 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): hash = hashlib.md5(str(uuid.uuid4()).encode()).hexdigest()[:8] file_name = f"{hash}_{file_suffix}{extension}" - if not self.temp_dir: - audio_temp_dir = tempfile.mkdtemp(prefix="AYON_audio_") - self.temp_dir = Path(audio_temp_dir) - self.temp_dir.mkdir(parents=True, exist_ok=True) + if not self.temp_dir_path: + audio_temp_dir_path = tempfile.mkdtemp(prefix="AYON_audio_") + self.temp_dir_path = Path(audio_temp_dir_path) + self.temp_dir_path.mkdir(parents=True, exist_ok=True) - return (self.temp_dir / file_name).as_posix() + return (self.temp_dir_path / file_name).as_posix() From 0f480ee410e99cd72d7cfcd576df4c44af9211e6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Nov 2025 10:20:38 +0100 Subject: [PATCH 070/115] fix updates of the label --- .../publisher/widgets/card_view_widgets.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 8df16f24c2..fdb05e1268 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -288,6 +288,8 @@ class InstanceCardWidget(CardWidget): self._last_product_name = None self._last_variant = None self._last_label = None + self._last_folder_path = None + self._last_task_name = None icon_widget = IconValuePixmapLabel(group_icon, self) icon_widget.setObjectName("ProductTypeIconLabel") @@ -384,15 +386,18 @@ class InstanceCardWidget(CardWidget): self._context_warning.setVisible(not valid) @staticmethod - def _get_card_widget_sub_label(folder_path, task_name): + def _get_card_widget_sub_label( + folder_path: Optional[str], + task_name: Optional[str], + ) -> str: parts = [] if folder_path: folder_name = folder_path.split("/")[-1] parts.append(f"{folder_name}") if task_name: - parts.append(folder_name) + parts.append(task_name) if not parts: - return None + return "" sublabel = " - ".join(parts) return f"{sublabel}" @@ -400,25 +405,30 @@ class InstanceCardWidget(CardWidget): variant = self.instance.variant product_name = self.instance.product_name label = self.instance.label - folder_path = self.instance.get_folder_path() - task_name = self.instance.get_task_name() + folder_path = self.instance.folder_path + task_name = self.instance.task_name if ( variant == self._last_variant and product_name == self._last_product_name and label == self._last_label + and folder_path == self._last_folder_path + and task_name == self._last_task_name ): return self._last_variant = variant self._last_product_name = product_name self._last_label = label + self._last_folder_path = folder_path + self._last_task_name = task_name + # Make `variant` bold label = html_escape(self.instance.label) found_parts = set(re.findall(variant, label, re.IGNORECASE)) if found_parts: for part in found_parts: - replacement = "{}".format(part) + replacement = f"{part}" label = label.replace(part, replacement) sublabel = self._get_card_widget_sub_label(folder_path, task_name) if sublabel: From 606fc39ee35c6a4f7b7b3b8622fa33398dc98659 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Nov 2025 10:32:29 +0100 Subject: [PATCH 071/115] use string concatenation --- .../tools/publisher/widgets/card_view_widgets.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index fdb05e1268..8f2c1ce0f8 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -390,15 +390,14 @@ class InstanceCardWidget(CardWidget): folder_path: Optional[str], task_name: Optional[str], ) -> str: - parts = [] + sublabel = "" if folder_path: folder_name = folder_path.split("/")[-1] - parts.append(f"{folder_name}") + sublabel = f"- {folder_name}" if task_name: - parts.append(task_name) - if not parts: - return "" - sublabel = " - ".join(parts) + sublabel += f" - {task_name}" + if not sublabel: + return sublabel return f"{sublabel}" def _update_product_name(self): From 447c0f45e5d6c3795589f948bee4f86aa09e3350 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Nov 2025 10:34:50 +0100 Subject: [PATCH 072/115] use cursive for task --- client/ayon_core/tools/publisher/widgets/card_view_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 8f2c1ce0f8..6023676913 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -395,7 +395,7 @@ class InstanceCardWidget(CardWidget): folder_name = folder_path.split("/")[-1] sublabel = f"- {folder_name}" if task_name: - sublabel += f" - {task_name}" + sublabel += f" - {task_name}" if not sublabel: return sublabel return f"{sublabel}" From b8714b386461e4b0c8e9a8ae5e1370ed41310572 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Nov 2025 10:54:15 +0100 Subject: [PATCH 073/115] faster split Co-authored-by: Roy Nieterau --- client/ayon_core/tools/publisher/widgets/card_view_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 6023676913..0ee11bfc67 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -392,7 +392,7 @@ class InstanceCardWidget(CardWidget): ) -> str: sublabel = "" if folder_path: - folder_name = folder_path.split("/")[-1] + folder_name = folder_path.rsplit("/", 1)[-1] sublabel = f"- {folder_name}" if task_name: sublabel += f" - {task_name}" From 1cf16961089a2cc884973516f5202be7fbbff0f3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Nov 2025 11:43:06 +0100 Subject: [PATCH 074/115] remove usage of 'desktop' from codebase --- client/ayon_core/tools/publisher/window.py | 9 ++------- .../tools/utils/color_widgets/color_screen_pick.py | 12 ++++-------- client/ayon_core/tools/utils/lib.py | 10 ++-------- 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index dc086a3b48..19994f9f62 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -678,13 +678,8 @@ class PublisherWindow(QtWidgets.QDialog): self._help_dialog.show() window = self.window() - if hasattr(QtWidgets.QApplication, "desktop"): - desktop = QtWidgets.QApplication.desktop() - screen_idx = desktop.screenNumber(window) - screen_geo = desktop.screenGeometry(screen_idx) - else: - screen = window.screen() - screen_geo = screen.geometry() + screen = window.screen() + screen_geo = screen.geometry() window_geo = window.geometry() dialog_x = window_geo.x() + window_geo.width() diff --git a/client/ayon_core/tools/utils/color_widgets/color_screen_pick.py b/client/ayon_core/tools/utils/color_widgets/color_screen_pick.py index 542db2831a..03ed5d3ceb 100644 --- a/client/ayon_core/tools/utils/color_widgets/color_screen_pick.py +++ b/client/ayon_core/tools/utils/color_widgets/color_screen_pick.py @@ -6,7 +6,7 @@ class PickScreenColorWidget(QtWidgets.QWidget): color_selected = QtCore.Signal(QtGui.QColor) def __init__(self, parent=None): - super(PickScreenColorWidget, self).__init__(parent) + super().__init__(parent) self.labels = [] self.magnification = 2 @@ -53,7 +53,7 @@ class PickLabel(QtWidgets.QLabel): close_session = QtCore.Signal() def __init__(self, pick_widget): - super(PickLabel, self).__init__() + super().__init__() self.setMouseTracking(True) self.pick_widget = pick_widget @@ -74,14 +74,10 @@ class PickLabel(QtWidgets.QLabel): self.show() self.windowHandle().setScreen(screen_obj) geo = screen_obj.geometry() - args = ( - QtWidgets.QApplication.desktop().winId(), + pix = screen_obj.grabWindow( + self.winId(), geo.x(), geo.y(), geo.width(), geo.height() ) - if qtpy.API in ("pyqt4", "pyside"): - pix = QtGui.QPixmap.grabWindow(*args) - else: - pix = screen_obj.grabWindow(*args) if pix.width() > pix.height(): size = pix.height() diff --git a/client/ayon_core/tools/utils/lib.py b/client/ayon_core/tools/utils/lib.py index a99c46199b..e087112a04 100644 --- a/client/ayon_core/tools/utils/lib.py +++ b/client/ayon_core/tools/utils/lib.py @@ -53,14 +53,8 @@ def checkstate_enum_to_int(state): def center_window(window): """Move window to center of it's screen.""" - - if hasattr(QtWidgets.QApplication, "desktop"): - desktop = QtWidgets.QApplication.desktop() - screen_idx = desktop.screenNumber(window) - screen_geo = desktop.screenGeometry(screen_idx) - else: - screen = window.screen() - screen_geo = screen.geometry() + screen = window.screen() + screen_geo = screen.geometry() geo = window.frameGeometry() geo.moveCenter(screen_geo.center()) From ef2600ae5a52125279d7b660d563624a20a80d82 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Nov 2025 11:48:49 +0100 Subject: [PATCH 075/115] remove unused import --- client/ayon_core/tools/utils/color_widgets/color_screen_pick.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/utils/color_widgets/color_screen_pick.py b/client/ayon_core/tools/utils/color_widgets/color_screen_pick.py index 03ed5d3ceb..c900ad1f48 100644 --- a/client/ayon_core/tools/utils/color_widgets/color_screen_pick.py +++ b/client/ayon_core/tools/utils/color_widgets/color_screen_pick.py @@ -1,4 +1,3 @@ -import qtpy from qtpy import QtWidgets, QtCore, QtGui From 3338dbe4733b1d6d93f43a01284e5e9404fd8239 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:51:30 +0100 Subject: [PATCH 076/115] selection keeps track about every value --- .../tools/workfiles/models/selection.py | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/tools/workfiles/models/selection.py b/client/ayon_core/tools/workfiles/models/selection.py index 9a6440b2a1..65caa287d1 100644 --- a/client/ayon_core/tools/workfiles/models/selection.py +++ b/client/ayon_core/tools/workfiles/models/selection.py @@ -17,6 +17,8 @@ class SelectionModel(object): self._task_name = None self._task_id = None self._workfile_path = None + self._rootless_workfile_path = None + self._workfile_entity_id = None self._representation_id = None def get_selected_folder_id(self): @@ -62,39 +64,49 @@ class SelectionModel(object): def get_selected_workfile_path(self): return self._workfile_path + def get_selected_workfile_data(self): + return { + "project_name": self._controller.get_current_project_name(), + "path": self._workfile_path, + "rootless_path": self._rootless_workfile_path, + "folder_id": self._folder_id, + "task_name": self._task_name, + "task_id": self._task_id, + "workfile_entity_id": self._workfile_entity_id, + } + def set_selected_workfile_path( self, rootless_path, path, workfile_entity_id ): if path == self._workfile_path: return + self._rootless_workfile_path = rootless_path self._workfile_path = path + self._workfile_entity_id = workfile_entity_id self._controller.emit_event( "selection.workarea.changed", - { - "project_name": self._controller.get_current_project_name(), - "path": path, - "rootless_path": rootless_path, - "folder_id": self._folder_id, - "task_name": self._task_name, - "task_id": self._task_id, - "workfile_entity_id": workfile_entity_id, - }, + self.get_selected_workfile_data(), self.event_source ) def get_selected_representation_id(self): return self._representation_id + def get_selected_representation_data(self): + return { + "project_name": self._controller.get_current_project_name(), + "folder_id": self._folder_id, + "task_id": self._task_id, + "representation_id": self._representation_id, + } + def set_selected_representation_id(self, representation_id): if representation_id == self._representation_id: return self._representation_id = representation_id self._controller.emit_event( "selection.representation.changed", - { - "project_name": self._controller.get_current_project_name(), - "representation_id": representation_id, - }, + self.get_selected_representation_data(), self.event_source ) From ece086c03f9eede9ce8214afbb8a2261b2fbd8d5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:52:35 +0100 Subject: [PATCH 077/115] merge tho methods into one --- client/ayon_core/tools/workfiles/abstract.py | 45 +++++---- client/ayon_core/tools/workfiles/control.py | 17 ++-- .../tools/workfiles/models/workfiles.py | 91 +++++++++---------- 3 files changed, 82 insertions(+), 71 deletions(-) diff --git a/client/ayon_core/tools/workfiles/abstract.py b/client/ayon_core/tools/workfiles/abstract.py index e7a038575a..b41553acc4 100644 --- a/client/ayon_core/tools/workfiles/abstract.py +++ b/client/ayon_core/tools/workfiles/abstract.py @@ -1,8 +1,18 @@ +from __future__ import annotations + import os from abc import ABC, abstractmethod +import typing +from typing import Optional from ayon_core.style import get_default_entity_icon_color +if typing.TYPE_CHECKING: + from ayon_core.host import ( + WorkfileInfo, + PublishedWorkfileInfo, + ) + class FolderItem: """Item representing folder entity on a server. @@ -159,6 +169,17 @@ class WorkareaFilepathResult: self.filepath = filepath +class PublishedWorkfileWrap: + """Wrapper for workfile info that also contains version comment.""" + def __init__( + self, + info: Optional[PublishedWorkfileInfo] = None, + comment: Optional[str] = None, + ) -> None: + self.info = info + self.comment = comment + + class AbstractWorkfilesCommon(ABC): @abstractmethod def is_host_valid(self): @@ -788,32 +809,24 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): pass @abstractmethod - def get_published_workfile_info(self, representation_id: str): + def get_published_workfile_info( + self, + folder_id: Optional[str], + representation_id: Optional[str], + ) -> PublishedWorkfileWrap: """Get published workfile info by representation ID. Args: - representation_id (str): Representation id. + folder_id (Optional[str]): Folder id. + representation_id (Optional[str]): Representation id. Returns: - Optional[PublishedWorkfileInfo]: Published workfile info or None + PublishedWorkfileWrap: Published workfile info or None if not found. """ pass - @abstractmethod - def get_published_workfile_version_comment(self, representation_id: str): - """Get version comment for published workfile. - - Args: - representation_id (str): Representation id. - - Returns: - Optional[str]: Version comment or None. - - """ - pass - @abstractmethod def get_workfile_info(self, folder_id, task_id, rootless_path): """Workfile info from database. diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py index 13a88e325f..c399a1bf33 100644 --- a/client/ayon_core/tools/workfiles/control.py +++ b/client/ayon_core/tools/workfiles/control.py @@ -1,4 +1,7 @@ +from __future__ import annotations + import os +from typing import Optional import ayon_api @@ -18,6 +21,7 @@ from ayon_core.tools.common_models import ( from .abstract import ( AbstractWorkfilesBackend, AbstractWorkfilesFrontend, + PublishedWorkfileWrap, ) from .models import SelectionModel, WorkfilesModel @@ -432,14 +436,13 @@ class BaseWorkfileController( folder_id, task_id ) - def get_published_workfile_info(self, representation_id): + def get_published_workfile_info( + self, + folder_id: Optional[str], + representation_id: Optional[str], + ) -> PublishedWorkfileWrap: return self._workfiles_model.get_published_workfile_info( - representation_id - ) - - def get_published_workfile_version_comment(self, representation_id): - return self._workfiles_model.get_published_workfile_version_comment( - representation_id + folder_id, representation_id ) def get_workfile_info(self, folder_id, task_id, rootless_path): diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index 5055c203be..c15dda2b4f 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -39,6 +39,7 @@ from ayon_core.pipeline.workfile import ( from ayon_core.pipeline.version_start import get_versioning_start from ayon_core.tools.workfiles.abstract import ( WorkareaFilepathResult, + PublishedWorkfileWrap, AbstractWorkfilesBackend, ) @@ -79,7 +80,7 @@ class WorkfilesModel: # Published workfiles self._repre_by_id = {} - self._version_by_repre_id = {} + self._version_comment_by_id = {} self._published_workfile_items_cache = NestedCacheItem( levels=1, default_factory=list ) @@ -96,7 +97,7 @@ class WorkfilesModel: self._workarea_file_items_cache.reset() self._repre_by_id = {} - self._version_by_repre_id = {} + self._version_comment_by_id = {} self._published_workfile_items_cache.reset() self._workfile_entities_by_task_id = {} @@ -554,13 +555,13 @@ class WorkfilesModel: ) def get_published_file_items( - self, folder_id: str, task_id: str + self, folder_id: Optional[str], task_id: Optional[str] ) -> list[PublishedWorkfileInfo]: """Published workfiles for passed context. Args: - folder_id (str): Folder id. - task_id (str): Task id. + folder_id (Optional[str]): Folder id. + task_id (Optional[str]): Task id. Returns: list[PublishedWorkfileInfo]: List of files for published workfiles. @@ -604,17 +605,10 @@ class WorkfilesModel: }) # Map versions by representation ID for easy lookup - version_by_id = { - version_entity["id"]: version_entity + self._version_comment_by_id.update({ + version_entity["id"]: version_entity["attrib"].get("comment") for version_entity in version_entities - } - for repre_entity in repre_entities: - repre_id = repre_entity["id"] - version_id = repre_entity.get("versionId") - if version_id and version_id in version_by_id: - self._version_by_repre_id[repre_id] = version_by_id[ - version_id - ] + }) project_entity = self._controller.get_project_entity(project_name) @@ -643,50 +637,32 @@ class WorkfilesModel: return items def get_published_workfile_info( - self, representation_id: str - ) -> Optional[PublishedWorkfileInfo]: + self, + folder_id: Optional[str], + representation_id: Optional[str], + ) -> PublishedWorkfileWrap: """Get published workfile info by representation ID. Args: - representation_id (str): Representation id. + folder_id (Optional[str]): Folder id. + representation_id (Optional[str]): Representation id. Returns: - Optional[PublishedWorkfileInfo]: Published workfile info or None + PublishedWorkfileWrap: Published workfile info or None if not found. """ if not representation_id: - return None + return PublishedWorkfileWrap() # Search through all cached published workfile items - cache_items = self._published_workfile_items_cache._data_by_key - for folder_cache in cache_items.values(): - if folder_cache.is_valid: - for item in folder_cache.get_data(): - if item.representation_id == representation_id: - return item - return None - - def get_published_workfile_version_comment( - self, representation_id: str - ) -> Optional[str]: - """Get version comment for published workfile. - - Args: - representation_id (str): Representation id. - - Returns: - Optional[str]: Version comment or None. - - """ - if not representation_id: - return None - - version_entity = self._version_by_repre_id.get(representation_id) - if version_entity: - attrib = version_entity.get("attrib") or {} - return attrib.get("comment") - return None + for item in self.get_published_file_items(folder_id, None): + if item.representation_id == representation_id: + comment = self._get_published_workfile_version_comment( + representation_id + ) + return PublishedWorkfileWrap(item, comment) + return PublishedWorkfileWrap() @property def _project_name(self) -> str: @@ -704,6 +680,25 @@ class WorkfilesModel: self._current_username = get_ayon_username() return self._current_username + def _get_published_workfile_version_comment( + self, representation_id: str + ) -> Optional[str]: + """Get version comment for published workfile. + + Args: + representation_id (str): Representation id. + + Returns: + Optional[str]: Version comment or None. + + """ + if not representation_id: + return None + repre = self._repre_by_id.get(representation_id) + if not repre: + return None + return self._version_comment_by_id.get(repre["versionId"]) + # --- Host --- def _open_workfile(self, folder_id: str, task_id: str, filepath: str): # TODO move to workfiles pipeline From 0cc99003f6a35cb942bd9a486255ee0b2f6a1cda Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:52:47 +0100 Subject: [PATCH 078/115] separate logic for workare and publishe workfiles --- .../tools/workfiles/widgets/side_panel.py | 235 ++++++++++-------- 1 file changed, 131 insertions(+), 104 deletions(-) diff --git a/client/ayon_core/tools/workfiles/widgets/side_panel.py b/client/ayon_core/tools/workfiles/widgets/side_panel.py index 2834f1dec8..f79dbca032 100644 --- a/client/ayon_core/tools/workfiles/widgets/side_panel.py +++ b/client/ayon_core/tools/workfiles/widgets/side_panel.py @@ -1,4 +1,5 @@ import datetime +from typing import Optional from qtpy import QtCore, QtWidgets @@ -7,7 +8,11 @@ def file_size_to_string(file_size): if not file_size: return "N/A" size = 0 - size_ending_mapping = {"KB": 1024**1, "MB": 1024**2, "GB": 1024**3} + size_ending_mapping = { + "KB": 1024**1, + "MB": 1024**2, + "GB": 1024**3, + } ending = "B" for _ending, _size in size_ending_mapping.items(): if file_size < _size: @@ -66,7 +71,8 @@ class SidePanelWidget(QtWidgets.QWidget): btn_description_save.clicked.connect(self._on_save_click) controller.register_event_callback( - "selection.workarea.changed", self._on_workarea_selection_change + "selection.workarea.changed", + self._on_workarea_selection_change ) controller.register_event_callback( "selection.representation.changed", @@ -86,9 +92,9 @@ class SidePanelWidget(QtWidgets.QWidget): self._orig_description = "" self._controller = controller - self._set_context(None, None, None, None, None) + self._set_context(False, None, None) - def set_published_mode(self, published_mode): + def set_published_mode(self, published_mode: bool) -> None: """Change published mode. Args: @@ -97,7 +103,19 @@ class SidePanelWidget(QtWidgets.QWidget): self._description_widget.setVisible(not published_mode) # Clear the context when switching modes to avoid showing stale data - self._set_context(None, None, None, None, None) + if published_mode: + self._set_publish_context( + self._folder_id, + self._task_id, + self._representation_id, + ) + else: + self._set_workarea_context( + self._folder_id, + self._task_id, + self._rootless_path, + self._filepath, + ) def _on_workarea_selection_change(self, event): folder_id = event["folder_id"] @@ -105,12 +123,16 @@ class SidePanelWidget(QtWidgets.QWidget): filepath = event["path"] rootless_path = event["rootless_path"] - self._set_context(folder_id, task_id, rootless_path, filepath, None) + self._set_workarea_context( + folder_id, task_id, rootless_path, filepath + ) def _on_representation_selection_change(self, event): + folder_id = event["folder_id"] + task_id = event["task_id"] representation_id = event["representation_id"] - self._set_context(None, None, None, None, representation_id) + self._set_publish_context(folder_id, task_id, representation_id) def _on_description_change(self): text = self._description_input.toPlainText() @@ -126,130 +148,135 @@ class SidePanelWidget(QtWidgets.QWidget): self._orig_description = description self._btn_description_save.setEnabled(False) - def _set_context( - self, folder_id, task_id, rootless_path, filepath, representation_id - ): - workfile_info = None - published_workfile_info = None + def _set_workarea_context( + self, + folder_id: Optional[str], + task_id: Optional[str], + rootless_path: Optional[str], + filepath: Optional[str], + ) -> None: + self._rootless_path = rootless_path + self._filepath = filepath - # Check if folder, task and file are selected (workarea mode) + workfile_info = None + # Check if folder, task and file are selected if folder_id and task_id and rootless_path: workfile_info = self._controller.get_workfile_info( folder_id, task_id, rootless_path ) - # Check if representation is selected (published mode) - elif representation_id: - published_workfile_info = ( - self._controller.get_published_workfile_info(representation_id) - ) - # Get version comment for published workfiles - version_comment = None - if representation_id and published_workfile_info: - version_comment = ( - self._controller.get_published_workfile_version_comment( - representation_id - ) - ) - - enabled = ( - workfile_info is not None or published_workfile_info is not None - ) - - self._details_input.setEnabled(enabled) - self._description_input.setEnabled(workfile_info is not None) - self._btn_description_save.setEnabled(workfile_info is not None) - - self._folder_id = folder_id - self._task_id = task_id - self._filepath = filepath - self._rootless_path = rootless_path - self._representation_id = representation_id - - # Disable inputs and remove texts if any required arguments are - # missing - if not enabled: + if workfile_info is None: self._orig_description = "" - self._details_input.setPlainText("") self._description_input.setPlainText("") + self._set_context(False, folder_id, task_id) return - # Use published workfile info if available, otherwise use workarea - # info - info = ( - published_workfile_info - if published_workfile_info - else workfile_info + self._set_context( + True, + folder_id, + task_id, + file_created = workfile_info.file_created, + file_modified=workfile_info.file_modified, + size_value=workfile_info.file_size, + created_by=workfile_info.created_by, + updated_by=workfile_info.updated_by, ) - description = info.description if hasattr(info, "description") else "" - size_value = file_size_to_string(info.file_size) + description = workfile_info.description + self._orig_description = description + self._description_input.setPlainText(description) + + def _set_publish_context( + self, + folder_id: Optional[str], + task_id: Optional[str], + representation_id: Optional[str], + ) -> None: + self._representation_id = representation_id + published_workfile_wrap = self._controller.get_published_workfile_info( + folder_id, + representation_id, + ) + info = published_workfile_wrap.info + comment = published_workfile_wrap.comment + if info is None: + self._set_context(False, folder_id, task_id) + return + + self._set_context( + True, + folder_id, + task_id, + file_created=info.file_created, + file_modified=info.file_modified, + size_value=info.file_size, + created_by=info.author, + comment=comment, + ) + + def _set_context( + self, + is_valid: bool, + folder_id: Optional[str], + task_id: Optional[str], + *, + file_created: Optional[int] = None, + file_modified: Optional[int] = None, + size_value: Optional[int] = None, + created_by: Optional[str] = None, + updated_by: Optional[str] = None, + comment: Optional[str] = None, + ) -> None: + self._folder_id = folder_id + self._task_id = task_id + + self._details_input.setEnabled(is_valid) + self._description_input.setEnabled(is_valid) + self._btn_description_save.setEnabled(is_valid) + if not is_valid: + self._details_input.setPlainText("") + return - # Append html string datetime_format = "%b %d %Y %H:%M:%S" - file_created = info.file_created - modification_time = info.file_modified if file_created: file_created = datetime.datetime.fromtimestamp(file_created) - if modification_time: - modification_time = datetime.datetime.fromtimestamp( - modification_time + if file_modified: + file_modified = datetime.datetime.fromtimestamp( + file_modified ) user_items_by_name = self._controller.get_user_items_by_name() - def convert_username(username): - user_item = user_items_by_name.get(username) + def convert_username(username_v): + user_item = user_items_by_name.get(username_v) if user_item is not None and user_item.full_name: return user_item.full_name - return username + return username_v - created_lines = [] - # For published workfiles, use 'author' field - if published_workfile_info: - if published_workfile_info.author: - created_lines.append( - convert_username(published_workfile_info.author) - ) - if file_created: - created_lines.append(file_created.strftime(datetime_format)) - else: - # For workarea workfiles, use 'created_by' field - if workfile_info.created_by: - created_lines.append( - convert_username(workfile_info.created_by) - ) - if file_created: - created_lines.append(file_created.strftime(datetime_format)) + lines = [] + if size_value is not None: + size_value = file_size_to_string(size_value) + lines.append(f"Size:
{size_value}") - if created_lines: - created_lines.insert(0, "Created:") - - modified_lines = [] - # For workarea workfiles, show 'updated_by' - if workfile_info and workfile_info.updated_by: - modified_lines.append(convert_username(workfile_info.updated_by)) - if modification_time: - modified_lines.append(modification_time.strftime(datetime_format)) - if modified_lines: - modified_lines.insert(0, "Modified:") - - lines = [ - "Size:", - size_value, - ] # Add version comment for published workfiles - if version_comment: - lines.append(f"Comment:
{version_comment}") - if created_lines: - lines.append("
".join(created_lines)) - if modified_lines: - lines.append("
".join(modified_lines)) + if comment: + lines.append(f"Comment:
{comment}") - self._orig_description = description - self._description_input.setPlainText(description) + if created_by or file_created: + lines.append("Created:") + if created_by: + lines.append(convert_username(created_by)) + if file_created: + lines.append(file_created.strftime(datetime_format)) + + if updated_by or file_modified: + lines.append("Modified:") + if updated_by: + lines.append(convert_username(updated_by)) + if file_modified: + lines.append(file_modified.strftime(datetime_format)) # Set as empty string self._details_input.setPlainText("") - self._details_input.appendHtml("
".join(lines)) + self._details_input.appendHtml("
".join(lines)) \ No newline at end of file From 48d2151d059b68a34e9c00ea989c6dd897fb9673 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:11:31 +0100 Subject: [PATCH 079/115] ruff fixes --- client/ayon_core/tools/workfiles/widgets/side_panel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/workfiles/widgets/side_panel.py b/client/ayon_core/tools/workfiles/widgets/side_panel.py index f79dbca032..2929ac780d 100644 --- a/client/ayon_core/tools/workfiles/widgets/side_panel.py +++ b/client/ayon_core/tools/workfiles/widgets/side_panel.py @@ -175,7 +175,7 @@ class SidePanelWidget(QtWidgets.QWidget): True, folder_id, task_id, - file_created = workfile_info.file_created, + file_created=workfile_info.file_created, file_modified=workfile_info.file_modified, size_value=workfile_info.file_size, created_by=workfile_info.created_by, @@ -279,4 +279,4 @@ class SidePanelWidget(QtWidgets.QWidget): # Set as empty string self._details_input.setPlainText("") - self._details_input.appendHtml("
".join(lines)) \ No newline at end of file + self._details_input.appendHtml("
".join(lines)) From f8e4b29a6cd8200bdbc12d71e22b6fb94851626b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:16:08 +0100 Subject: [PATCH 080/115] remove unused import --- client/ayon_core/tools/workfiles/abstract.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/ayon_core/tools/workfiles/abstract.py b/client/ayon_core/tools/workfiles/abstract.py index b41553acc4..1b92c0d334 100644 --- a/client/ayon_core/tools/workfiles/abstract.py +++ b/client/ayon_core/tools/workfiles/abstract.py @@ -8,10 +8,7 @@ from typing import Optional from ayon_core.style import get_default_entity_icon_color if typing.TYPE_CHECKING: - from ayon_core.host import ( - WorkfileInfo, - PublishedWorkfileInfo, - ) + from ayon_core.host import PublishedWorkfileInfo class FolderItem: From 8ba1a4068500567ad5caf9b933806079e9bdef27 Mon Sep 17 00:00:00 2001 From: marvill85 <32180676+marvill85@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:23:32 +0100 Subject: [PATCH 081/115] Update workfile_template_builder.py Add optional folder_path_regex filtering to linked folder retrieval --- .../pipeline/workfile/workfile_template_builder.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 52e27baa80..461515987a 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -300,7 +300,7 @@ class AbstractTemplateBuilder(ABC): self._loaders_by_name = get_loaders_by_name() return self._loaders_by_name - def get_linked_folder_entities(self, link_type: Optional[str]): + def get_linked_folder_entities(self, link_type: Optional[str], folder_path_regex: Optional[str]): if not link_type: return [] project_name = self.project_name @@ -317,7 +317,7 @@ class AbstractTemplateBuilder(ABC): if link["entityType"] == "folder" } - return list(get_folders(project_name, folder_ids=linked_folder_ids)) + return list(get_folders(project_name, folder_path_regex=folder_path_regex, folder_ids=linked_folder_ids)) def _collect_creators(self): self._creators_by_name = { @@ -1638,7 +1638,7 @@ class PlaceholderLoadMixin(object): linked_folder_entity["id"] for linked_folder_entity in ( self.builder.get_linked_folder_entities( - link_type=link_type)) + link_type=link_type, folder_path_regex=folder_path_regex)) ] if not folder_ids: From ad3c4c9317fa5bd7c81310106e017bc3460f623a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:52:45 +0100 Subject: [PATCH 082/115] remove first dash Co-authored-by: Roy Nieterau --- client/ayon_core/tools/publisher/widgets/card_view_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 0ee11bfc67..9c8288b415 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -393,7 +393,7 @@ class InstanceCardWidget(CardWidget): sublabel = "" if folder_path: folder_name = folder_path.rsplit("/", 1)[-1] - sublabel = f"- {folder_name}" + sublabel = f"{folder_name}" if task_name: sublabel += f" - {task_name}" if not sublabel: From 527b1f979570b55ac2bebcf3bf0665fdec6c373a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:53:57 +0100 Subject: [PATCH 083/115] deselect entities when previous selection widget is focused --- .../tools/launcher/ui/hierarchy_page.py | 44 +++++++++++++++++-- .../tools/launcher/ui/workfiles_page.py | 8 +++- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py index 47388d9685..ff1ba92976 100644 --- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py +++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py @@ -1,5 +1,5 @@ import qtawesome -from qtpy import QtWidgets, QtCore +from qtpy import QtWidgets, QtCore, QtGui from ayon_core.tools.utils import ( PlaceholderLineEdit, @@ -15,6 +15,36 @@ from ayon_core.tools.utils.lib import checkstate_int_to_enum from .workfiles_page import WorkfilesPage +class LauncherFoldersWidget(FoldersWidget): + focused_in = QtCore.Signal() + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._folders_view.installEventFilter(self) + + def eventFilter(self, obj, event): + if event.type() == QtCore.QEvent.FocusIn: + self.focused_in.emit() + return False + + +class LauncherTasksWidget(TasksWidget): + focused_in = QtCore.Signal() + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._tasks_view.installEventFilter(self) + + def deselect(self): + sel_model = self._tasks_view.selectionModel() + sel_model.clearSelection() + + def eventFilter(self, obj, event): + if event.type() == QtCore.QEvent.FocusIn: + self.focused_in.emit() + return False + + class HierarchyPage(QtWidgets.QWidget): def __init__(self, controller, parent): super().__init__(parent) @@ -68,12 +98,12 @@ class HierarchyPage(QtWidgets.QWidget): filters_layout.addWidget(my_tasks_checkbox, 0) # - Folders widget - folders_widget = FoldersWidget(controller, content_body) + folders_widget = LauncherFoldersWidget(controller, content_body) folders_widget.set_header_visible(True) folders_widget.set_deselectable(True) # - Tasks widget - tasks_widget = TasksWidget(controller, content_body) + tasks_widget = LauncherTasksWidget(controller, content_body) # - Third page - Workfiles workfiles_page = WorkfilesPage(controller, content_body) @@ -97,6 +127,8 @@ class HierarchyPage(QtWidgets.QWidget): my_tasks_checkbox.stateChanged.connect( self._on_my_tasks_checkbox_state_changed ) + folders_widget.focused_in.connect(self._on_folders_focus) + tasks_widget.focused_in.connect(self._on_tasks_focus) self._is_visible = False self._controller = controller @@ -151,3 +183,9 @@ class HierarchyPage(QtWidgets.QWidget): task_ids = entity_ids["task_ids"] self._folders_widget.set_folder_ids_filter(folder_ids) self._tasks_widget.set_task_ids_filter(task_ids) + + def _on_folders_focus(self): + self._tasks_widget.deselect() + + def _on_tasks_focus(self): + self._workfiles_page.deselect() diff --git a/client/ayon_core/tools/launcher/ui/workfiles_page.py b/client/ayon_core/tools/launcher/ui/workfiles_page.py index 1ea223031e..d81221f38d 100644 --- a/client/ayon_core/tools/launcher/ui/workfiles_page.py +++ b/client/ayon_core/tools/launcher/ui/workfiles_page.py @@ -3,7 +3,7 @@ from typing import Optional import ayon_api from qtpy import QtCore, QtWidgets, QtGui -from ayon_core.tools.utils import get_qt_icon +from ayon_core.tools.utils import get_qt_icon, DeselectableTreeView from ayon_core.tools.launcher.abstract import AbstractLauncherFrontEnd VERSION_ROLE = QtCore.Qt.UserRole + 1 @@ -127,7 +127,7 @@ class WorkfilesModel(QtGui.QStandardItemModel): return icon -class WorkfilesView(QtWidgets.QTreeView): +class WorkfilesView(DeselectableTreeView): def drawBranches(self, painter, rect, index): return @@ -165,6 +165,10 @@ class WorkfilesPage(QtWidgets.QWidget): def refresh(self) -> None: self._workfiles_model.refresh() + def deselect(self): + sel_model = self._workfiles_view.selectionModel() + sel_model.clearSelection() + def _on_refresh(self) -> None: self._workfiles_proxy.sort(0, QtCore.Qt.DescendingOrder) From 503e627fb5805d23ab5e505bc25b1a9a63f6817f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:05:26 +0100 Subject: [PATCH 084/115] remove unused import --- client/ayon_core/tools/launcher/ui/hierarchy_page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py index ff1ba92976..faebf14ab6 100644 --- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py +++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py @@ -1,5 +1,5 @@ import qtawesome -from qtpy import QtWidgets, QtCore, QtGui +from qtpy import QtWidgets, QtCore from ayon_core.tools.utils import ( PlaceholderLineEdit, From c1d0510fd34fbab644f03652cecfbb6f76d65214 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:05:07 +0100 Subject: [PATCH 085/115] deselect only workfiles --- client/ayon_core/tools/launcher/ui/hierarchy_page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py index faebf14ab6..dc535db5fc 100644 --- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py +++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py @@ -185,7 +185,7 @@ class HierarchyPage(QtWidgets.QWidget): self._tasks_widget.set_task_ids_filter(task_ids) def _on_folders_focus(self): - self._tasks_widget.deselect() + self._workfiles_page.deselect() def _on_tasks_focus(self): self._workfiles_page.deselect() From feba551e99d6217f6d8b033fdffc2c67873c79dd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:46:08 +0100 Subject: [PATCH 086/115] remove dash between folder and task --- client/ayon_core/tools/publisher/widgets/card_view_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 9c8288b415..80c253da01 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -395,7 +395,7 @@ class InstanceCardWidget(CardWidget): folder_name = folder_path.rsplit("/", 1)[-1] sublabel = f"{folder_name}" if task_name: - sublabel += f" - {task_name}" + sublabel += f" {task_name}" if not sublabel: return sublabel return f"{sublabel}" From 9dbd46d86663a127840d6dfb40589dedd1ca826d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:46:16 +0100 Subject: [PATCH 087/115] indent context a little --- client/ayon_core/tools/publisher/widgets/card_view_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 80c253da01..f40730fdf9 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -393,7 +393,7 @@ class InstanceCardWidget(CardWidget): sublabel = "" if folder_path: folder_name = folder_path.rsplit("/", 1)[-1] - sublabel = f"{folder_name}" + sublabel = f"  {folder_name}" if task_name: sublabel += f" {task_name}" if not sublabel: From a07fc4bfaad5c472facc00ff6e0772951dc99084 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 10 Nov 2025 17:55:14 +0800 Subject: [PATCH 088/115] make sure the confirm message box on top after creating hero version --- client/ayon_core/plugins/load/create_hero_version.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/plugins/load/create_hero_version.py b/client/ayon_core/plugins/load/create_hero_version.py index aef0cf8863..d01a97e2ff 100644 --- a/client/ayon_core/plugins/load/create_hero_version.py +++ b/client/ayon_core/plugins/load/create_hero_version.py @@ -75,6 +75,7 @@ class CreateHeroVersion(load.ProductLoaderPlugin): msgBox.setStyleSheet(style.load_stylesheet()) msgBox.setWindowFlags( msgBox.windowFlags() | QtCore.Qt.WindowType.FramelessWindowHint + | QtCore.Qt.WindowType.WindowStaysOnTopHint ) msgBox.exec_() From 770b94bde52a4541b16e56a254228f22ff83c056 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:08:35 +0100 Subject: [PATCH 089/115] show context only if is not same as current context --- .../publisher/widgets/card_view_widgets.py | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index f40730fdf9..9ae545c213 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -23,6 +23,7 @@ from __future__ import annotations import re import collections +from dataclasses import dataclass from typing import Optional from qtpy import QtWidgets, QtCore @@ -58,6 +59,13 @@ class SelectionTypes: extend_to = "extend_to" +@dataclass +class _SharedInfo: + """Shared information for multiple widgets.""" + current_folder_path: Optional[str] = None + current_task_name: Optional[str] = None + + class BaseGroupWidget(QtWidgets.QWidget): selected = QtCore.Signal(str, str, str) removed_selected = QtCore.Signal() @@ -202,11 +210,12 @@ class ContextCardWidget(CardWidget): Is not visually under group widget and is always at the top of card view. """ - def __init__(self, parent): + def __init__(self, shared_info: _SharedInfo, parent: QtWidgets.QWidget): super().__init__(parent) self._id = CONTEXT_ID self._group_identifier = CONTEXT_GROUP + self._shared_info = shared_info icon_widget = PublishPixmapLabel(None, self) icon_widget.setObjectName("ProductTypeIconLabel") @@ -273,9 +282,12 @@ class InstanceCardWidget(CardWidget): is_parent_active: bool, group_icon, parent: BaseGroupWidget, + shared_info: _SharedInfo, ): super().__init__(parent) + self._shared_info = shared_info + self.instance = instance self._is_active = instance.is_active @@ -389,8 +401,15 @@ class InstanceCardWidget(CardWidget): def _get_card_widget_sub_label( folder_path: Optional[str], task_name: Optional[str], + shared_info: _SharedInfo, ) -> str: sublabel = "" + if ( + shared_info.current_folder_path == folder_path + and shared_info.current_task_name == task_name + ): + return sublabel + if folder_path: folder_name = folder_path.rsplit("/", 1)[-1] sublabel = f"  {folder_name}" @@ -429,7 +448,9 @@ class InstanceCardWidget(CardWidget): for part in found_parts: replacement = f"{part}" label = label.replace(part, replacement) - sublabel = self._get_card_widget_sub_label(folder_path, task_name) + sublabel = self._get_card_widget_sub_label( + folder_path, task_name, self._shared_info + ) if sublabel: label += f"
{sublabel}" @@ -514,6 +535,7 @@ class InstanceCardView(AbstractInstanceView): super().__init__(parent) self._controller: AbstractPublisherFrontend = controller + self._shared_info: _SharedInfo = _SharedInfo() scroll_area = QtWidgets.QScrollArea(self) scroll_area.setWidgetResizable(True) @@ -729,11 +751,16 @@ class InstanceCardView(AbstractInstanceView): def refresh(self): """Refresh instances in view based on CreatedContext.""" + self._shared_info.current_folder_path = ( + self._controller.get_current_folder_path() + ) + self._shared_info.current_task_name = ( + self._controller.get_current_task_name() + ) self._make_sure_context_widget_exists() self._update_convertors_group() - context_info_by_id = self._controller.get_instances_context_info() # Prepare instances by group and identifiers by group @@ -841,6 +868,8 @@ class InstanceCardView(AbstractInstanceView): widget.setVisible(False) widget.deleteLater() + sorted_group_names.insert(0, CONTEXT_GROUP) + self._parent_id_by_id = parent_id_by_id self._instance_ids_by_parent_id = instance_ids_by_parent_id self._group_name_by_instance_id = group_by_instance_id @@ -908,7 +937,8 @@ class InstanceCardView(AbstractInstanceView): context_info, is_parent_active, group_icon, - group_widget + group_widget, + self._shared_info, ) widget.selected.connect(self._on_widget_selection) widget.active_changed.connect(self._on_active_changed) @@ -927,7 +957,7 @@ class InstanceCardView(AbstractInstanceView): if self._context_widget is not None: return - widget = ContextCardWidget(self._content_widget) + widget = ContextCardWidget(self._shared_info, self._content_widget) widget.selected.connect(self._on_widget_selection) widget.double_clicked.connect(self.double_clicked) From 9a70ecdd7e0d6388dd60eaa3bace6c33f2bf7b5c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:50:16 +0100 Subject: [PATCH 090/115] revert the last changes --- .../publisher/widgets/card_view_widgets.py | 47 ++++--------------- 1 file changed, 9 insertions(+), 38 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 9ae545c213..68b206c262 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -59,13 +59,6 @@ class SelectionTypes: extend_to = "extend_to" -@dataclass -class _SharedInfo: - """Shared information for multiple widgets.""" - current_folder_path: Optional[str] = None - current_task_name: Optional[str] = None - - class BaseGroupWidget(QtWidgets.QWidget): selected = QtCore.Signal(str, str, str) removed_selected = QtCore.Signal() @@ -210,12 +203,11 @@ class ContextCardWidget(CardWidget): Is not visually under group widget and is always at the top of card view. """ - def __init__(self, shared_info: _SharedInfo, parent: QtWidgets.QWidget): + def __init__(self, parent: QtWidgets.QWidget): super().__init__(parent) self._id = CONTEXT_ID self._group_identifier = CONTEXT_GROUP - self._shared_info = shared_info icon_widget = PublishPixmapLabel(None, self) icon_widget.setObjectName("ProductTypeIconLabel") @@ -282,12 +274,9 @@ class InstanceCardWidget(CardWidget): is_parent_active: bool, group_icon, parent: BaseGroupWidget, - shared_info: _SharedInfo, ): super().__init__(parent) - self._shared_info = shared_info - self.instance = instance self._is_active = instance.is_active @@ -401,23 +390,14 @@ class InstanceCardWidget(CardWidget): def _get_card_widget_sub_label( folder_path: Optional[str], task_name: Optional[str], - shared_info: _SharedInfo, ) -> str: sublabel = "" - if ( - shared_info.current_folder_path == folder_path - and shared_info.current_task_name == task_name - ): - return sublabel - if folder_path: folder_name = folder_path.rsplit("/", 1)[-1] - sublabel = f"  {folder_name}" + sublabel = f"{folder_name}" if task_name: - sublabel += f" {task_name}" - if not sublabel: - return sublabel - return f"{sublabel}" + sublabel += f" - {task_name}" + return sublabel def _update_product_name(self): variant = self.instance.variant @@ -448,11 +428,11 @@ class InstanceCardWidget(CardWidget): for part in found_parts: replacement = f"{part}" label = label.replace(part, replacement) - sublabel = self._get_card_widget_sub_label( - folder_path, task_name, self._shared_info - ) + + label = f"{label}" + sublabel = self._get_card_widget_sub_label(folder_path, task_name) if sublabel: - label += f"
{sublabel}" + label += f"
{sublabel}" self._label_widget.setText(label) # HTML text will cause that label start catch mouse clicks @@ -535,7 +515,6 @@ class InstanceCardView(AbstractInstanceView): super().__init__(parent) self._controller: AbstractPublisherFrontend = controller - self._shared_info: _SharedInfo = _SharedInfo() scroll_area = QtWidgets.QScrollArea(self) scroll_area.setWidgetResizable(True) @@ -751,13 +730,6 @@ class InstanceCardView(AbstractInstanceView): def refresh(self): """Refresh instances in view based on CreatedContext.""" - self._shared_info.current_folder_path = ( - self._controller.get_current_folder_path() - ) - self._shared_info.current_task_name = ( - self._controller.get_current_task_name() - ) - self._make_sure_context_widget_exists() self._update_convertors_group() @@ -938,7 +910,6 @@ class InstanceCardView(AbstractInstanceView): is_parent_active, group_icon, group_widget, - self._shared_info, ) widget.selected.connect(self._on_widget_selection) widget.active_changed.connect(self._on_active_changed) @@ -957,7 +928,7 @@ class InstanceCardView(AbstractInstanceView): if self._context_widget is not None: return - widget = ContextCardWidget(self._shared_info, self._content_widget) + widget = ContextCardWidget(self._content_widget) widget.selected.connect(self._on_widget_selection) widget.double_clicked.connect(self.double_clicked) From 9be4493a9ea888062f3476bee7e1d883c1062581 Mon Sep 17 00:00:00 2001 From: Mustafa Zaky Jafar Date: Mon, 10 Nov 2025 12:56:51 +0200 Subject: [PATCH 091/115] remove unused import --- client/ayon_core/tools/publisher/widgets/card_view_widgets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 68b206c262..aef3f85e0c 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -23,7 +23,6 @@ from __future__ import annotations import re import collections -from dataclasses import dataclass from typing import Optional from qtpy import QtWidgets, QtCore From 0f8339ac922f204cd5bf7c5f8396c6111e00d915 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 12:45:22 +0100 Subject: [PATCH 092/115] use span for context label too --- client/ayon_core/tools/publisher/widgets/card_view_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index aef3f85e0c..ca95b1ff1a 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -211,7 +211,7 @@ class ContextCardWidget(CardWidget): icon_widget = PublishPixmapLabel(None, self) icon_widget.setObjectName("ProductTypeIconLabel") - label_widget = QtWidgets.QLabel(CONTEXT_LABEL, self) + label_widget = QtWidgets.QLabel(f"{CONTEXT_LABEL}", self) icon_layout = QtWidgets.QHBoxLayout() icon_layout.setContentsMargins(5, 5, 5, 5) From d1ef11defa7f193bede9cbeebfe03771085fee02 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:06:56 +0100 Subject: [PATCH 093/115] define helper widget for folders filtering --- client/ayon_core/tools/utils/__init__.py | 2 + .../ayon_core/tools/utils/folders_widget.py | 40 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/client/ayon_core/tools/utils/__init__.py b/client/ayon_core/tools/utils/__init__.py index 111b7c614b..56989927ee 100644 --- a/client/ayon_core/tools/utils/__init__.py +++ b/client/ayon_core/tools/utils/__init__.py @@ -76,6 +76,7 @@ from .folders_widget import ( FoldersQtModel, FOLDERS_MODEL_SENDER_NAME, SimpleFoldersWidget, + FoldersFiltersWidget, ) from .tasks_widget import ( @@ -160,6 +161,7 @@ __all__ = ( "FoldersQtModel", "FOLDERS_MODEL_SENDER_NAME", "SimpleFoldersWidget", + "FoldersFiltersWidget", "TasksWidget", "TasksQtModel", diff --git a/client/ayon_core/tools/utils/folders_widget.py b/client/ayon_core/tools/utils/folders_widget.py index 7b71dd087c..12f4bebdae 100644 --- a/client/ayon_core/tools/utils/folders_widget.py +++ b/client/ayon_core/tools/utils/folders_widget.py @@ -15,6 +15,8 @@ from ayon_core.tools.common_models import ( from .models import RecursiveSortFilterProxyModel from .views import TreeView from .lib import RefreshThread, get_qt_icon +from .widgets import PlaceholderLineEdit +from .nice_checkbox import NiceCheckbox FOLDERS_MODEL_SENDER_NAME = "qt_folders_model" @@ -794,3 +796,41 @@ class SimpleFoldersWidget(FoldersWidget): event (Event): Triggered event. """ pass + + +class FoldersFiltersWidget(QtWidgets.QWidget): + """Helper widget for most commonly used filters in context selection.""" + text_changed = QtCore.Signal(str) + my_tasks_changed = QtCore.Signal(bool) + + def __init__(self, parent: QtWidgets.QWidget) -> None: + super().__init__(parent) + + folders_filter_input = PlaceholderLineEdit(self) + folders_filter_input.setPlaceholderText("Folder name filter...") + + my_tasks_tooltip = ( + "Filter folders and task to only those you are assigned to." + ) + my_tasks_label = QtWidgets.QLabel("My tasks", self) + my_tasks_label.setToolTip(my_tasks_tooltip) + + my_tasks_checkbox = NiceCheckbox(self) + my_tasks_checkbox.setChecked(False) + my_tasks_checkbox.setToolTip(my_tasks_tooltip) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + layout.addWidget(folders_filter_input, 1) + layout.addWidget(my_tasks_label, 0) + layout.addWidget(my_tasks_checkbox, 0) + + folders_filter_input.textChanged.connect(self.text_changed) + my_tasks_checkbox.stateChanged.connect(self._on_my_tasks_change) + + self._folders_filter_input = folders_filter_input + self._my_tasks_checkbox = my_tasks_checkbox + + def _on_my_tasks_change(self, _state: int) -> None: + self.my_tasks_changed.emit(self._my_tasks_checkbox.isChecked()) From cef3bc229a4508e0f19c2dd5fadcb2b0f96c2233 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:07:12 +0100 Subject: [PATCH 094/115] disable case sensitivity for folders proxy --- client/ayon_core/tools/utils/folders_widget.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/utils/folders_widget.py b/client/ayon_core/tools/utils/folders_widget.py index 12f4bebdae..126363086b 100644 --- a/client/ayon_core/tools/utils/folders_widget.py +++ b/client/ayon_core/tools/utils/folders_widget.py @@ -345,6 +345,8 @@ class FoldersProxyModel(RecursiveSortFilterProxyModel): def __init__(self): super().__init__() + self.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + self._folder_ids_filter = None def set_folder_ids_filter(self, folder_ids: Optional[list[str]]): From 0dd47211c54850066d6440599b6fb540b388949d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:13:24 +0100 Subject: [PATCH 095/115] add 'get_current_username' to UsersModel --- client/ayon_core/tools/common_models/users.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/common_models/users.py b/client/ayon_core/tools/common_models/users.py index f7939e5cd3..42a76d8d7d 100644 --- a/client/ayon_core/tools/common_models/users.py +++ b/client/ayon_core/tools/common_models/users.py @@ -1,10 +1,13 @@ import json import collections +from typing import Optional import ayon_api from ayon_api.graphql import FIELD_VALUE, GraphQlQuery, fields_to_dict -from ayon_core.lib import NestedCacheItem +from ayon_core.lib import NestedCacheItem, get_ayon_username + +NOT_SET = object() # --- Implementation that should be in ayon-python-api --- @@ -105,9 +108,18 @@ class UserItem: class UsersModel: def __init__(self, controller): + self._current_username = NOT_SET self._controller = controller self._users_cache = NestedCacheItem(default_factory=list) + def get_current_username(self) -> Optional[str]: + if self._current_username is NOT_SET: + self._current_username = get_ayon_username() + return self._current_username + + def reset(self) -> None: + self._users_cache.reset() + def get_user_items(self, project_name): """Get user items. From ad83d827e2a1dcb092d191360a1756086813f9cf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:14:32 +0100 Subject: [PATCH 096/115] move private methods below public one --- client/ayon_core/tools/loader/control.py | 28 ++++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 9f159bfb21..9ec3e580d9 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -476,20 +476,6 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def is_standard_projects_filter_enabled(self): return self._host is not None - def _get_project_anatomy(self, project_name): - if not project_name: - return None - cache = self._project_anatomy_cache[project_name] - if not cache.is_valid: - cache.update_data(Anatomy(project_name)) - return cache.get_data() - - def _create_event_system(self): - return QueuedEventSystem() - - def _emit_event(self, topic, data=None): - self._event_system.emit(topic, data or {}, "controller") - def get_product_types_filter(self): output = ProductTypesFilter( is_allow_list=False, @@ -545,3 +531,17 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): product_types=profile["filter_product_types"] ) return output + + def _create_event_system(self): + return QueuedEventSystem() + + def _emit_event(self, topic, data=None): + self._event_system.emit(topic, data or {}, "controller") + + def _get_project_anatomy(self, project_name): + if not project_name: + return None + cache = self._project_anatomy_cache[project_name] + if not cache.is_valid: + cache.update_data(Anatomy(project_name)) + return cache.get_data() From 91d44a833b7915b9bbbccb23ad81fd45df295db2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:22:44 +0100 Subject: [PATCH 097/115] implemented my tasks filter to browser --- client/ayon_core/tools/loader/abstract.py | 15 +++++++++ client/ayon_core/tools/loader/control.py | 20 +++++++++++- .../tools/loader/ui/folders_widget.py | 19 ++++++++--- .../ayon_core/tools/loader/ui/tasks_widget.py | 21 ++++++++++-- client/ayon_core/tools/loader/ui/window.py | 32 ++++++++++++++----- 5 files changed, 91 insertions(+), 16 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 9c7934d2db..089d298b2c 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -666,6 +666,21 @@ class FrontendLoaderController(_BaseLoaderController): """ pass + @abstractmethod + def get_my_tasks_entity_ids( + self, project_name: str + ) -> dict[str, list[str]]: + """Get entity ids for my tasks. + + Args: + project_name (str): Project name. + + Returns: + dict[str, list[str]]: Folder and task ids. + + """ + pass + @abstractmethod def get_available_tags_by_entity_type( self, project_name: str diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 9ec3e580d9..2a86a50b6d 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -8,7 +8,11 @@ import ayon_api from ayon_core.settings import get_project_settings from ayon_core.pipeline import get_current_host_name -from ayon_core.lib import NestedCacheItem, CacheItem, filter_profiles +from ayon_core.lib import ( + NestedCacheItem, + CacheItem, + filter_profiles, +) from ayon_core.lib.events import QueuedEventSystem from ayon_core.pipeline import Anatomy, get_current_context from ayon_core.host import ILoadHost @@ -18,6 +22,7 @@ from ayon_core.tools.common_models import ( ThumbnailsModel, TagItem, ProductTypeIconMapping, + UsersModel, ) from .abstract import ( @@ -32,6 +37,8 @@ from .models import ( SiteSyncModel ) +NOT_SET = object() + class ExpectedSelection: def __init__(self, controller): @@ -124,6 +131,7 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): self._loader_actions_model = LoaderActionsModel(self) self._thumbnails_model = ThumbnailsModel() self._sitesync_model = SiteSyncModel(self) + self._users_model = UsersModel(self) @property def log(self): @@ -160,6 +168,7 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): self._projects_model.reset() self._thumbnails_model.reset() self._sitesync_model.reset() + self._users_model.reset() self._projects_model.refresh() @@ -235,6 +244,15 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): output[folder_id] = label return output + def get_my_tasks_entity_ids(self, project_name: str): + username = self._users_model.get_current_username() + assignees = [] + if username: + assignees.append(username) + return self._hierarchy_model.get_entity_ids_for_assignees( + project_name, assignees + ) + def get_available_tags_by_entity_type( self, project_name: str ) -> dict[str, list[str]]: diff --git a/client/ayon_core/tools/loader/ui/folders_widget.py b/client/ayon_core/tools/loader/ui/folders_widget.py index f238eabcef..6de0b17ea2 100644 --- a/client/ayon_core/tools/loader/ui/folders_widget.py +++ b/client/ayon_core/tools/loader/ui/folders_widget.py @@ -1,11 +1,11 @@ +from typing import Optional + import qtpy from qtpy import QtWidgets, QtCore, QtGui -from ayon_core.tools.utils import ( - RecursiveSortFilterProxyModel, - DeselectableTreeView, -) from ayon_core.style import get_objected_colors +from ayon_core.tools.utils import DeselectableTreeView +from ayon_core.tools.utils.folders_widget import FoldersProxyModel from ayon_core.tools.utils import ( FoldersQtModel, @@ -260,7 +260,7 @@ class LoaderFoldersWidget(QtWidgets.QWidget): QtWidgets.QAbstractItemView.ExtendedSelection) folders_model = LoaderFoldersModel(controller) - folders_proxy_model = RecursiveSortFilterProxyModel() + folders_proxy_model = FoldersProxyModel() folders_proxy_model.setSourceModel(folders_model) folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) @@ -314,6 +314,15 @@ class LoaderFoldersWidget(QtWidgets.QWidget): if name: self._folders_view.expandAll() + def set_folder_ids_filter(self, folder_ids: Optional[list[str]]): + """Set filter of folder ids. + + Args: + folder_ids (list[str]): The list of folder ids. + + """ + self._folders_proxy_model.set_folder_ids_filter(folder_ids) + def set_merged_products_selection(self, items): """ diff --git a/client/ayon_core/tools/loader/ui/tasks_widget.py b/client/ayon_core/tools/loader/ui/tasks_widget.py index cc7e2e9c95..3a38739cf0 100644 --- a/client/ayon_core/tools/loader/ui/tasks_widget.py +++ b/client/ayon_core/tools/loader/ui/tasks_widget.py @@ -1,11 +1,11 @@ import collections import hashlib +from typing import Optional from qtpy import QtWidgets, QtCore, QtGui from ayon_core.style import get_default_entity_icon_color from ayon_core.tools.utils import ( - RecursiveSortFilterProxyModel, DeselectableTreeView, TasksQtModel, TASKS_MODEL_SENDER_NAME, @@ -15,9 +15,11 @@ from ayon_core.tools.utils.tasks_widget import ( ITEM_NAME_ROLE, PARENT_ID_ROLE, TASK_TYPE_ROLE, + TasksProxyModel, ) from ayon_core.tools.utils.lib import RefreshThread, get_qt_icon + # Role that can't clash with default 'tasks_widget' roles FOLDER_LABEL_ROLE = QtCore.Qt.UserRole + 100 NO_TASKS_ID = "--no-task--" @@ -295,7 +297,7 @@ class LoaderTasksQtModel(TasksQtModel): return super().data(index, role) -class LoaderTasksProxyModel(RecursiveSortFilterProxyModel): +class LoaderTasksProxyModel(TasksProxyModel): def lessThan(self, left, right): if left.data(ITEM_ID_ROLE) == NO_TASKS_ID: return False @@ -303,6 +305,12 @@ class LoaderTasksProxyModel(RecursiveSortFilterProxyModel): return True return super().lessThan(left, right) + def filterAcceptsRow(self, row, parent_index): + source_index = self.sourceModel().index(row, 0, parent_index) + if source_index.data(ITEM_ID_ROLE) == NO_TASKS_ID: + return True + return super().filterAcceptsRow(row, parent_index) + class LoaderTasksWidget(QtWidgets.QWidget): refreshed = QtCore.Signal() @@ -363,6 +371,15 @@ class LoaderTasksWidget(QtWidgets.QWidget): if name: self._tasks_view.expandAll() + def set_task_ids_filter(self, task_ids: Optional[list[str]]): + """Set filter of folder ids. + + Args: + task_ids (list[str]): The list of folder ids. + + """ + self._tasks_proxy_model.set_task_ids_filter(task_ids) + def refresh(self): self._tasks_model.refresh() diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index df5beb708f..d1d1222f51 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -5,14 +5,14 @@ from qtpy import QtWidgets, QtCore, QtGui from ayon_core.resources import get_ayon_icon_filepath from ayon_core.style import load_stylesheet from ayon_core.tools.utils import ( - PlaceholderLineEdit, ErrorMessageBox, ThumbnailPainterWidget, RefreshButton, GoToCurrentButton, + FoldersFiltersWidget, ) from ayon_core.tools.utils.lib import center_window -from ayon_core.tools.utils import ProjectsCombobox +from ayon_core.tools.utils import ProjectsCombobox, NiceCheckbox from ayon_core.tools.common_models import StatusItem from ayon_core.tools.loader.abstract import ProductTypeItem from ayon_core.tools.loader.control import LoaderController @@ -170,15 +170,14 @@ class LoaderWindow(QtWidgets.QWidget): context_top_layout.addWidget(go_to_current_btn, 0) context_top_layout.addWidget(refresh_btn, 0) - folders_filter_input = PlaceholderLineEdit(context_widget) - folders_filter_input.setPlaceholderText("Folder name filter...") + filters_widget = FoldersFiltersWidget(context_widget) folders_widget = LoaderFoldersWidget(controller, context_widget) context_layout = QtWidgets.QVBoxLayout(context_widget) context_layout.setContentsMargins(0, 0, 0, 0) context_layout.addWidget(context_top_widget, 0) - context_layout.addWidget(folders_filter_input, 0) + context_layout.addWidget(filters_widget, 0) context_layout.addWidget(folders_widget, 1) tasks_widget = LoaderTasksWidget(controller, context_widget) @@ -247,9 +246,12 @@ class LoaderWindow(QtWidgets.QWidget): projects_combobox.refreshed.connect(self._on_projects_refresh) folders_widget.refreshed.connect(self._on_folders_refresh) products_widget.refreshed.connect(self._on_products_refresh) - folders_filter_input.textChanged.connect( + filters_widget.text_changed.connect( self._on_folder_filter_change ) + filters_widget.my_tasks_changed.connect( + self._on_my_tasks_checkbox_state_changed + ) search_bar.filter_changed.connect(self._on_filter_change) product_group_checkbox.stateChanged.connect( self._on_product_group_change @@ -303,7 +305,7 @@ class LoaderWindow(QtWidgets.QWidget): self._refresh_btn = refresh_btn self._projects_combobox = projects_combobox - self._folders_filter_input = folders_filter_input + self._filters_widget = filters_widget self._folders_widget = folders_widget self._tasks_widget = tasks_widget @@ -421,9 +423,23 @@ class LoaderWindow(QtWidgets.QWidget): self._group_dialog.set_product_ids(project_name, product_ids) self._group_dialog.show() - def _on_folder_filter_change(self, text): + def _on_folder_filter_change(self, text: str) -> None: self._folders_widget.set_name_filter(text) + def _on_my_tasks_checkbox_state_changed(self, enabled: bool) -> None: + # self._folders_widget + folder_ids = None + task_ids = None + if enabled: + entity_ids = self._controller.get_my_tasks_entity_ids( + self._selected_project_name + ) + folder_ids = entity_ids["folder_ids"] + task_ids = entity_ids["task_ids"] + self._folders_widget.set_folder_ids_filter(folder_ids) + self._tasks_widget.set_task_ids_filter(task_ids) + + def _on_product_group_change(self): self._products_widget.set_enable_grouping( self._product_group_checkbox.isChecked() From e6325fa2e81580555cb07847e6e78cfac2c6376e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:27:49 +0100 Subject: [PATCH 098/115] use 'FoldersFiltersWidget' in launcher --- .../tools/launcher/ui/hierarchy_page.py | 37 +++---------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/client/ayon_core/tools/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py index dc535db5fc..8554a5af8c 100644 --- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py +++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py @@ -2,14 +2,13 @@ import qtawesome from qtpy import QtWidgets, QtCore from ayon_core.tools.utils import ( - PlaceholderLineEdit, SquareButton, RefreshButton, ProjectsCombobox, FoldersWidget, TasksWidget, - NiceCheckbox, ) +from ayon_core.tools.utils.folders_widget import FoldersFiltersWidget from ayon_core.tools.utils.lib import checkstate_int_to_enum from .workfiles_page import WorkfilesPage @@ -76,26 +75,7 @@ class HierarchyPage(QtWidgets.QWidget): content_body.setOrientation(QtCore.Qt.Horizontal) # - filters - filters_widget = QtWidgets.QWidget(self) - - folders_filter_text = PlaceholderLineEdit(filters_widget) - folders_filter_text.setPlaceholderText("Filter folders...") - - my_tasks_tooltip = ( - "Filter folders and task to only those you are assigned to." - ) - my_tasks_label = QtWidgets.QLabel("My tasks", filters_widget) - my_tasks_label.setToolTip(my_tasks_tooltip) - - my_tasks_checkbox = NiceCheckbox(filters_widget) - my_tasks_checkbox.setChecked(False) - my_tasks_checkbox.setToolTip(my_tasks_tooltip) - - filters_layout = QtWidgets.QHBoxLayout(filters_widget) - filters_layout.setContentsMargins(0, 0, 0, 0) - filters_layout.addWidget(folders_filter_text, 1) - filters_layout.addWidget(my_tasks_label, 0) - filters_layout.addWidget(my_tasks_checkbox, 0) + filters_widget = FoldersFiltersWidget(self) # - Folders widget folders_widget = LauncherFoldersWidget(controller, content_body) @@ -123,8 +103,8 @@ class HierarchyPage(QtWidgets.QWidget): btn_back.clicked.connect(self._on_back_clicked) refresh_btn.clicked.connect(self._on_refresh_clicked) - folders_filter_text.textChanged.connect(self._on_filter_text_changed) - my_tasks_checkbox.stateChanged.connect( + filters_widget.text_changed.connect(self._on_filter_text_changed) + filters_widget.my_tasks_changed.connect( self._on_my_tasks_checkbox_state_changed ) folders_widget.focused_in.connect(self._on_folders_focus) @@ -135,7 +115,6 @@ class HierarchyPage(QtWidgets.QWidget): self._btn_back = btn_back self._projects_combobox = projects_combobox - self._my_tasks_checkbox = my_tasks_checkbox self._folders_widget = folders_widget self._tasks_widget = tasks_widget self._workfiles_page = workfiles_page @@ -158,9 +137,6 @@ class HierarchyPage(QtWidgets.QWidget): self._folders_widget.refresh() self._tasks_widget.refresh() self._workfiles_page.refresh() - self._on_my_tasks_checkbox_state_changed( - self._my_tasks_checkbox.checkState() - ) def _on_back_clicked(self): self._controller.set_selected_project(None) @@ -171,11 +147,10 @@ class HierarchyPage(QtWidgets.QWidget): def _on_filter_text_changed(self, text): self._folders_widget.set_name_filter(text) - def _on_my_tasks_checkbox_state_changed(self, state): + def _on_my_tasks_checkbox_state_changed(self, enabled: bool) -> None: folder_ids = None task_ids = None - state = checkstate_int_to_enum(state) - if state == QtCore.Qt.Checked: + if enabled: entity_ids = self._controller.get_my_tasks_entity_ids( self._project_name ) From 9c3dec09c9382aa210f59583081e7224b0e8ec22 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:53:36 +0100 Subject: [PATCH 099/115] small cleanup --- client/ayon_core/tools/launcher/control.py | 23 +++++++++++----------- client/ayon_core/tools/loader/control.py | 4 +++- client/ayon_core/tools/loader/ui/window.py | 2 -- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/tools/launcher/control.py b/client/ayon_core/tools/launcher/control.py index 85b362f9d7..f4656de787 100644 --- a/client/ayon_core/tools/launcher/control.py +++ b/client/ayon_core/tools/launcher/control.py @@ -1,10 +1,14 @@ from typing import Optional -from ayon_core.lib import Logger, get_ayon_username +from ayon_core.lib import Logger from ayon_core.lib.events import QueuedEventSystem from ayon_core.addon import AddonsManager from ayon_core.settings import get_project_settings, get_studio_settings -from ayon_core.tools.common_models import ProjectsModel, HierarchyModel +from ayon_core.tools.common_models import ( + ProjectsModel, + HierarchyModel, + UsersModel, +) from .abstract import ( AbstractLauncherFrontEnd, @@ -30,13 +34,12 @@ class BaseLauncherController( self._addons_manager = None - self._username = NOT_SET - self._selection_model = LauncherSelectionModel(self) self._projects_model = ProjectsModel(self) self._hierarchy_model = HierarchyModel(self) self._actions_model = ActionsModel(self) self._workfiles_model = WorkfilesModel(self) + self._users_model = UsersModel(self) @property def log(self): @@ -209,6 +212,7 @@ class BaseLauncherController( self._projects_model.reset() self._hierarchy_model.reset() + self._users_model.reset() self._actions_model.refresh() self._projects_model.refresh() @@ -229,8 +233,10 @@ class BaseLauncherController( self._emit_event("controller.refresh.actions.finished") - def get_my_tasks_entity_ids(self, project_name: str): - username = self._get_my_username() + def get_my_tasks_entity_ids( + self, project_name: str + ) -> dict[str, list[str]]: + username = self._users_model.get_current_username() assignees = [] if username: assignees.append(username) @@ -238,10 +244,5 @@ class BaseLauncherController( project_name, assignees ) - def _get_my_username(self): - if self._username is NOT_SET: - self._username = get_ayon_username() - return self._username - def _emit_event(self, topic, data=None): self.emit_event(topic, data, "controller") diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 2a86a50b6d..d0cc9db2f5 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -244,7 +244,9 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): output[folder_id] = label return output - def get_my_tasks_entity_ids(self, project_name: str): + def get_my_tasks_entity_ids( + self, project_name: str + ) -> dict[str, list[str]]: username = self._users_model.get_current_username() assignees = [] if username: diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index d1d1222f51..48704e7481 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -427,7 +427,6 @@ class LoaderWindow(QtWidgets.QWidget): self._folders_widget.set_name_filter(text) def _on_my_tasks_checkbox_state_changed(self, enabled: bool) -> None: - # self._folders_widget folder_ids = None task_ids = None if enabled: @@ -439,7 +438,6 @@ class LoaderWindow(QtWidgets.QWidget): self._folders_widget.set_folder_ids_filter(folder_ids) self._tasks_widget.set_task_ids_filter(task_ids) - def _on_product_group_change(self): self._products_widget.set_enable_grouping( self._product_group_checkbox.isChecked() From 3a6ee43f22e94d9c3f304787820433b3277e3761 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:22:20 +0100 Subject: [PATCH 100/115] added doption to change filters --- client/ayon_core/tools/utils/folders_widget.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/ayon_core/tools/utils/folders_widget.py b/client/ayon_core/tools/utils/folders_widget.py index 126363086b..f506af5352 100644 --- a/client/ayon_core/tools/utils/folders_widget.py +++ b/client/ayon_core/tools/utils/folders_widget.py @@ -834,5 +834,11 @@ class FoldersFiltersWidget(QtWidgets.QWidget): self._folders_filter_input = folders_filter_input self._my_tasks_checkbox = my_tasks_checkbox + def set_text(self, text: str) -> None: + self._folders_filter_input.setText(text) + + def set_my_tasks_checked(self, checked: bool) -> None: + self._my_tasks_checkbox.setChecked(checked) + def _on_my_tasks_change(self, _state: int) -> None: self.my_tasks_changed.emit(self._my_tasks_checkbox.isChecked()) From f9f55b48b03fefa20cb2c361e676858afba1c78e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:23:19 +0100 Subject: [PATCH 101/115] added my tasks filtering to publisher --- client/ayon_core/tools/publisher/abstract.py | 15 +++ client/ayon_core/tools/publisher/control.py | 19 +++- .../widgets/create_context_widgets.py | 33 ++++-- .../tools/publisher/widgets/create_widget.py | 12 +- .../tools/publisher/widgets/folders_dialog.py | 107 ++++++++++-------- 5 files changed, 126 insertions(+), 60 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 14da15793d..bfd0948519 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -295,6 +295,21 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): """Get folder id from folder path.""" pass + @abstractmethod + def get_my_tasks_entity_ids( + self, project_name: str + ) -> dict[str, list[str]]: + """Get entity ids for my tasks. + + Args: + project_name (str): Project name. + + Returns: + dict[str, list[str]]: Folder and task ids. + + """ + pass + # --- Create --- @abstractmethod def get_creator_items(self) -> Dict[str, "CreatorItem"]: diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 038816c6fc..3d11131dc3 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -11,7 +11,11 @@ from ayon_core.pipeline import ( registered_host, get_process_id, ) -from ayon_core.tools.common_models import ProjectsModel, HierarchyModel +from ayon_core.tools.common_models import ( + ProjectsModel, + HierarchyModel, + UsersModel, +) from .models import ( PublishModel, @@ -101,6 +105,7 @@ class PublisherController( # Cacher of avalon documents self._projects_model = ProjectsModel(self) self._hierarchy_model = HierarchyModel(self) + self._users_model = UsersModel(self) @property def log(self): @@ -317,6 +322,17 @@ class PublisherController( return False return True + def get_my_tasks_entity_ids( + self, project_name: str + ) -> dict[str, list[str]]: + username = self._users_model.get_current_username() + assignees = [] + if username: + assignees.append(username) + return self._hierarchy_model.get_entity_ids_for_assignees( + project_name, assignees + ) + # --- Publish specific callbacks --- def get_context_title(self): """Get context title for artist shown at the top of main window.""" @@ -359,6 +375,7 @@ class PublisherController( self._emit_event("controller.reset.started") self._hierarchy_model.reset() + self._users_model.reset() # Publish part must be reset after plugins self._create_model.reset() diff --git a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py index faf2248181..49d236353f 100644 --- a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py @@ -1,10 +1,14 @@ from qtpy import QtWidgets, QtCore from ayon_core.lib.events import QueuedEventSystem -from ayon_core.tools.utils import PlaceholderLineEdit, GoToCurrentButton from ayon_core.tools.common_models import HierarchyExpectedSelection -from ayon_core.tools.utils import FoldersWidget, TasksWidget +from ayon_core.tools.utils import ( + FoldersWidget, + TasksWidget, + FoldersFiltersWidget, + GoToCurrentButton, +) from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend @@ -180,8 +184,7 @@ class CreateContextWidget(QtWidgets.QWidget): headers_widget = QtWidgets.QWidget(self) - folder_filter_input = PlaceholderLineEdit(headers_widget) - folder_filter_input.setPlaceholderText("Filter folders..") + filters_widget = FoldersFiltersWidget(headers_widget) current_context_btn = GoToCurrentButton(headers_widget) current_context_btn.setToolTip("Go to current context") @@ -189,7 +192,8 @@ class CreateContextWidget(QtWidgets.QWidget): headers_layout = QtWidgets.QHBoxLayout(headers_widget) headers_layout.setContentsMargins(0, 0, 0, 0) - headers_layout.addWidget(folder_filter_input, 1) + headers_layout.setSpacing(5) + headers_layout.addWidget(filters_widget, 1) headers_layout.addWidget(current_context_btn, 0) hierarchy_controller = CreateHierarchyController(controller) @@ -207,15 +211,16 @@ class CreateContextWidget(QtWidgets.QWidget): main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) main_layout.addWidget(headers_widget, 0) + main_layout.addSpacing(5) main_layout.addWidget(folders_widget, 2) main_layout.addWidget(tasks_widget, 1) folders_widget.selection_changed.connect(self._on_folder_change) tasks_widget.selection_changed.connect(self._on_task_change) current_context_btn.clicked.connect(self._on_current_context_click) - folder_filter_input.textChanged.connect(self._on_folder_filter_change) + filters_widget.text_changed.connect(self._on_folder_filter_change) + filters_widget.my_tasks_changed.connect(self._on_my_tasks_change) - self._folder_filter_input = folder_filter_input self._current_context_btn = current_context_btn self._folders_widget = folders_widget self._tasks_widget = tasks_widget @@ -303,5 +308,17 @@ class CreateContextWidget(QtWidgets.QWidget): self._last_project_name, folder_id, task_name ) - def _on_folder_filter_change(self, text): + def _on_folder_filter_change(self, text: str) -> None: self._folders_widget.set_name_filter(text) + + def _on_my_tasks_change(self, enabled: bool) -> None: + folder_ids = None + task_ids = None + if enabled: + entity_ids = self._controller.get_my_tasks_entity_ids( + self._last_project_name + ) + folder_ids = entity_ids["folder_ids"] + task_ids = entity_ids["task_ids"] + self._folders_widget.set_folder_ids_filter(folder_ids) + self._tasks_widget.set_task_ids_filter(task_ids) diff --git a/client/ayon_core/tools/publisher/widgets/create_widget.py b/client/ayon_core/tools/publisher/widgets/create_widget.py index b9b3afd895..d98bc95eb2 100644 --- a/client/ayon_core/tools/publisher/widgets/create_widget.py +++ b/client/ayon_core/tools/publisher/widgets/create_widget.py @@ -710,11 +710,13 @@ class CreateWidget(QtWidgets.QWidget): def _on_first_show(self): width = self.width() - part = int(width / 4) - rem_width = width - part - self._main_splitter_widget.setSizes([part, rem_width]) - rem_width = rem_width - part - self._creators_splitter.setSizes([part, rem_width]) + part = int(width / 9) + context_width = part * 3 + create_sel_width = part * 2 + rem_width = width - context_width + self._main_splitter_widget.setSizes([context_width, rem_width]) + rem_width -= create_sel_width + self._creators_splitter.setSizes([create_sel_width, rem_width]) def showEvent(self, event): super().showEvent(event) diff --git a/client/ayon_core/tools/publisher/widgets/folders_dialog.py b/client/ayon_core/tools/publisher/widgets/folders_dialog.py index d2eb68310e..e0d9c098d8 100644 --- a/client/ayon_core/tools/publisher/widgets/folders_dialog.py +++ b/client/ayon_core/tools/publisher/widgets/folders_dialog.py @@ -1,7 +1,10 @@ from qtpy import QtWidgets from ayon_core.lib.events import QueuedEventSystem -from ayon_core.tools.utils import PlaceholderLineEdit, FoldersWidget +from ayon_core.tools.utils import ( + FoldersWidget, + FoldersFiltersWidget, +) from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend @@ -43,8 +46,7 @@ class FoldersDialog(QtWidgets.QDialog): super().__init__(parent) self.setWindowTitle("Select folder") - filter_input = PlaceholderLineEdit(self) - filter_input.setPlaceholderText("Filter folders..") + filters_widget = FoldersFiltersWidget(self) folders_controller = FoldersDialogController(controller) folders_widget = FoldersWidget(folders_controller, self) @@ -59,7 +61,8 @@ class FoldersDialog(QtWidgets.QDialog): btns_layout.addWidget(cancel_btn) layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(filter_input, 0) + layout.setSpacing(5) + layout.addWidget(filters_widget, 0) layout.addWidget(folders_widget, 1) layout.addLayout(btns_layout, 0) @@ -68,12 +71,13 @@ class FoldersDialog(QtWidgets.QDialog): ) folders_widget.double_clicked.connect(self._on_ok_clicked) - filter_input.textChanged.connect(self._on_filter_change) + filters_widget.text_changed.connect(self._on_filter_change) + filters_widget.my_tasks_changed.connect(self._on_my_tasks_change) ok_btn.clicked.connect(self._on_ok_clicked) cancel_btn.clicked.connect(self._on_cancel_clicked) self._controller = controller - self._filter_input = filter_input + self._filters_widget = filters_widget self._ok_btn = ok_btn self._cancel_btn = cancel_btn @@ -88,6 +92,49 @@ class FoldersDialog(QtWidgets.QDialog): self._first_show = True self._default_height = 500 + self._project_name = None + + def showEvent(self, event): + """Refresh folders widget on show.""" + super().showEvent(event) + if self._first_show: + self._first_show = False + self._on_first_show() + # Refresh on show + self.reset(False) + + def reset(self, force=True): + """Reset widget.""" + if not force and not self._soft_reset_enabled: + return + + self._project_name = self._controller.get_current_project_name() + if self._soft_reset_enabled: + self._soft_reset_enabled = False + + self._folders_widget.set_project_name(self._project_name) + + def get_selected_folder_path(self): + """Get selected folder path.""" + return self._selected_folder_path + + def set_selected_folders(self, folder_paths: list[str]) -> None: + """Change preselected folder before showing the dialog. + + This also resets model and clean filter. + """ + self.reset(False) + self._filters_widget.set_text("") + self._filters_widget.set_my_tasks_checked(False) + + folder_id = None + for folder_path in folder_paths: + folder_id = self._controller.get_folder_id_from_path(folder_path) + if folder_id: + break + if folder_id: + self._folders_widget.set_selected_folder(folder_id) + def _on_first_show(self): center = self.rect().center() size = self.size() @@ -103,27 +150,6 @@ class FoldersDialog(QtWidgets.QDialog): # Change reset enabled so model is reset on show event self._soft_reset_enabled = True - def showEvent(self, event): - """Refresh folders widget on show.""" - super().showEvent(event) - if self._first_show: - self._first_show = False - self._on_first_show() - # Refresh on show - self.reset(False) - - def reset(self, force=True): - """Reset widget.""" - if not force and not self._soft_reset_enabled: - return - - if self._soft_reset_enabled: - self._soft_reset_enabled = False - - self._folders_widget.set_project_name( - self._controller.get_current_project_name() - ) - def _on_filter_change(self, text): """Trigger change of filter of folders.""" self._folders_widget.set_name_filter(text) @@ -137,22 +163,11 @@ class FoldersDialog(QtWidgets.QDialog): ) self.done(1) - def set_selected_folders(self, folder_paths): - """Change preselected folder before showing the dialog. - - This also resets model and clean filter. - """ - self.reset(False) - self._filter_input.setText("") - - folder_id = None - for folder_path in folder_paths: - folder_id = self._controller.get_folder_id_from_path(folder_path) - if folder_id: - break - if folder_id: - self._folders_widget.set_selected_folder(folder_id) - - def get_selected_folder_path(self): - """Get selected folder path.""" - return self._selected_folder_path + def _on_my_tasks_change(self, enabled: bool) -> None: + folder_ids = None + if enabled: + entity_ids = self._controller.get_my_tasks_entity_ids( + self._project_name + ) + folder_ids = entity_ids["folder_ids"] + self._folders_widget.set_folder_ids_filter(folder_ids) From ba4ecc6f80b0894e3c9345924106b0eeae6a109b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 17:04:29 +0100 Subject: [PATCH 102/115] use filters widget in workfiles tool --- .../tools/workfiles/widgets/window.py | 46 ++++++------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/client/ayon_core/tools/workfiles/widgets/window.py b/client/ayon_core/tools/workfiles/widgets/window.py index 00362ea866..811fe602d1 100644 --- a/client/ayon_core/tools/workfiles/widgets/window.py +++ b/client/ayon_core/tools/workfiles/widgets/window.py @@ -6,12 +6,11 @@ from ayon_core.tools.utils import ( FoldersWidget, GoToCurrentButton, MessageOverlayObject, - NiceCheckbox, PlaceholderLineEdit, RefreshButton, TasksWidget, + FoldersFiltersWidget, ) -from ayon_core.tools.utils.lib import checkstate_int_to_enum from ayon_core.tools.workfiles.control import BaseWorkfileController from .files_widget import FilesWidget @@ -69,7 +68,6 @@ class WorkfilesToolWindow(QtWidgets.QWidget): self._default_window_flags = flags self._folders_widget = None - self._folder_filter_input = None self._files_widget = None @@ -178,48 +176,33 @@ class WorkfilesToolWindow(QtWidgets.QWidget): col_widget = QtWidgets.QWidget(parent) header_widget = QtWidgets.QWidget(col_widget) - folder_filter_input = PlaceholderLineEdit(header_widget) - folder_filter_input.setPlaceholderText("Filter folders..") + filters_widget = FoldersFiltersWidget(header_widget) go_to_current_btn = GoToCurrentButton(header_widget) refresh_btn = RefreshButton(header_widget) + header_layout = QtWidgets.QHBoxLayout(header_widget) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.addWidget(filters_widget, 1) + header_layout.addWidget(go_to_current_btn, 0) + header_layout.addWidget(refresh_btn, 0) + folder_widget = FoldersWidget( controller, col_widget, handle_expected_selection=True ) - my_tasks_tooltip = ( - "Filter folders and task to only those you are assigned to." - ) - - my_tasks_label = QtWidgets.QLabel("My tasks") - my_tasks_label.setToolTip(my_tasks_tooltip) - - my_tasks_checkbox = NiceCheckbox(folder_widget) - my_tasks_checkbox.setChecked(False) - my_tasks_checkbox.setToolTip(my_tasks_tooltip) - - header_layout = QtWidgets.QHBoxLayout(header_widget) - header_layout.setContentsMargins(0, 0, 0, 0) - header_layout.addWidget(folder_filter_input, 1) - header_layout.addWidget(go_to_current_btn, 0) - header_layout.addWidget(refresh_btn, 0) - header_layout.addWidget(my_tasks_label, 0) - header_layout.addWidget(my_tasks_checkbox, 0) - col_layout = QtWidgets.QVBoxLayout(col_widget) col_layout.setContentsMargins(0, 0, 0, 0) col_layout.addWidget(header_widget, 0) col_layout.addWidget(folder_widget, 1) - folder_filter_input.textChanged.connect(self._on_folder_filter_change) - go_to_current_btn.clicked.connect(self._on_go_to_current_clicked) - refresh_btn.clicked.connect(self._on_refresh_clicked) - my_tasks_checkbox.stateChanged.connect( + filters_widget.text_changed.connect(self._on_folder_filter_change) + filters_widget.my_tasks_changed.connect( self._on_my_tasks_checkbox_state_changed ) + go_to_current_btn.clicked.connect(self._on_go_to_current_clicked) + refresh_btn.clicked.connect(self._on_refresh_clicked) - self._folder_filter_input = folder_filter_input self._folders_widget = folder_widget return col_widget @@ -403,11 +386,10 @@ class WorkfilesToolWindow(QtWidgets.QWidget): else: self.close() - def _on_my_tasks_checkbox_state_changed(self, state): + def _on_my_tasks_checkbox_state_changed(self, enabled: bool) -> None: folder_ids = None task_ids = None - state = checkstate_int_to_enum(state) - if state == QtCore.Qt.Checked: + if enabled: entity_ids = self._controller.get_my_tasks_entity_ids( self._project_name ) From dc7f1556750fdcece3b6391ae83fd8bb7e95eeac Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 17:07:24 +0100 Subject: [PATCH 103/115] remove unused imports --- client/ayon_core/tools/launcher/ui/hierarchy_page.py | 1 - client/ayon_core/tools/loader/ui/window.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/tools/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py index 8554a5af8c..3c8be4679e 100644 --- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py +++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py @@ -9,7 +9,6 @@ from ayon_core.tools.utils import ( TasksWidget, ) from ayon_core.tools.utils.folders_widget import FoldersFiltersWidget -from ayon_core.tools.utils.lib import checkstate_int_to_enum from .workfiles_page import WorkfilesPage diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index 48704e7481..27e416b495 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -12,7 +12,7 @@ from ayon_core.tools.utils import ( FoldersFiltersWidget, ) from ayon_core.tools.utils.lib import center_window -from ayon_core.tools.utils import ProjectsCombobox, NiceCheckbox +from ayon_core.tools.utils import ProjectsCombobox from ayon_core.tools.common_models import StatusItem from ayon_core.tools.loader.abstract import ProductTypeItem from ayon_core.tools.loader.control import LoaderController From 7c8e7c23e9440c7a7d41de4973b8001cffd82bc0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Nov 2025 14:19:04 +0100 Subject: [PATCH 104/115] Change code formatting --- .../workfile/workfile_template_builder.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 461515987a..9ce9579b58 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -300,7 +300,11 @@ class AbstractTemplateBuilder(ABC): self._loaders_by_name = get_loaders_by_name() return self._loaders_by_name - def get_linked_folder_entities(self, link_type: Optional[str], folder_path_regex: Optional[str]): + def get_linked_folder_entities( + self, + link_type: Optional[str], + folder_path_regex: Optional[str], + ): if not link_type: return [] project_name = self.project_name @@ -317,7 +321,11 @@ class AbstractTemplateBuilder(ABC): if link["entityType"] == "folder" } - return list(get_folders(project_name, folder_path_regex=folder_path_regex, folder_ids=linked_folder_ids)) + return list(get_folders( + project_name, + folder_path_regex=folder_path_regex, + folder_ids=linked_folder_ids, + )) def _collect_creators(self): self._creators_by_name = { @@ -1638,7 +1646,10 @@ class PlaceholderLoadMixin(object): linked_folder_entity["id"] for linked_folder_entity in ( self.builder.get_linked_folder_entities( - link_type=link_type, folder_path_regex=folder_path_regex)) + link_type=link_type, + folder_path_regex=folder_path_regex + ) + ) ] if not folder_ids: From 76cfa3e148e5a5d3064614ed82d83c1d71f111ea Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 11 Nov 2025 14:05:22 +0000 Subject: [PATCH 105/115] [Automated] Add generated package files from main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 6aa30b935a..83a7d0a51d 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.6.7+dev" +__version__ = "1.6.8" diff --git a/package.py b/package.py index ff3fad5b19..b3e41b2e81 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.7+dev" +version = "1.6.8" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 6656f15249..212fe505b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.7+dev" +version = "1.6.8" description = "" authors = ["Ynput Team "] readme = "README.md" From 8fdc943553687c4d43dea9d855e906df1d8dbb4e Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 11 Nov 2025 14:05:58 +0000 Subject: [PATCH 106/115] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 83a7d0a51d..e481e81356 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.6.8" +__version__ = "1.6.8+dev" diff --git a/package.py b/package.py index b3e41b2e81..eb30148176 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.8" +version = "1.6.8+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 212fe505b9..6708432e85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.8" +version = "1.6.8+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From ccd54e16cc8be451c08b8b0da296f8ed10d43730 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 11 Nov 2025 14:07:19 +0000 Subject: [PATCH 107/115] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c79ca69fca..513e088fef 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to AYON Tray options: + - 1.6.8 - 1.6.7 - 1.6.6 - 1.6.5 From f38a6dffba44e264aa06f9b424943378bbf4dd98 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Nov 2025 16:40:59 +0100 Subject: [PATCH 108/115] Avoid repeating input channel names if e.g. R, G and B are reading from Y channel --- client/ayon_core/lib/transcoding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 22396a5324..0255b5a9d4 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1545,7 +1545,7 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None): channels_arg += ",A={}".format(float(alpha_default)) input_channels.append("A") - input_channels_str = ",".join(input_channels) + input_channels_str = ",".join(set(input_channels)) subimages = oiio_input_info.get("subimages") input_arg = "-i" From e2c668769032b7a68753f31164887032a6b0ce2e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Nov 2025 16:46:04 +0100 Subject: [PATCH 109/115] Preserve order when making unique to avoid error on `R,G,B` becoming `B,G,R` but the channels being using in `R,G,B` order in `--ch` argument --- client/ayon_core/lib/transcoding.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 0255b5a9d4..076ee79665 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1545,7 +1545,8 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None): channels_arg += ",A={}".format(float(alpha_default)) input_channels.append("A") - input_channels_str = ",".join(set(input_channels)) + # Make sure channels are unique, but preserve order to avoid oiiotool crash + input_channels_str = ",".join(list(dict.fromkeys(input_channels))) subimages = oiio_input_info.get("subimages") input_arg = "-i" From 42642ebd34f5b35bc42cf6e5ace0c7a6866a2426 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:41:55 +0100 Subject: [PATCH 110/115] use graphql to get projects --- .../ayon_core/tools/common_models/projects.py | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/common_models/projects.py b/client/ayon_core/tools/common_models/projects.py index 250c3b020d..3e090e18b8 100644 --- a/client/ayon_core/tools/common_models/projects.py +++ b/client/ayon_core/tools/common_models/projects.py @@ -1,11 +1,13 @@ from __future__ import annotations +import json import contextlib from abc import ABC, abstractmethod from typing import Any, Optional from dataclasses import dataclass import ayon_api +from ayon_api.graphql_queries import projects_graphql_query from ayon_core.style import get_default_entity_icon_color from ayon_core.lib import CacheItem, NestedCacheItem @@ -290,6 +292,7 @@ def _get_project_items_from_entitiy( return [ ProjectItem.from_entity(project) for project in projects + if project["active"] ] @@ -538,8 +541,32 @@ class ProjectsModel(object): self._projects_cache.update_data(project_items) return self._projects_cache.get_data() + def _fetch_projects_bc(self) -> list[dict[str, Any]]: + """Fetch projects using GraphQl. + + This method was added because ayon_api had a bug in 'get_projects'. + + Returns: + list[dict[str, Any]]: List of projects. + + """ + api = ayon_api.get_server_api_connection() + query = projects_graphql_query({"name", "active", "library", "data"}) + + projects = [] + for parsed_data in query.continuous_query(api): + for project in parsed_data["projects"]: + project_data = project["data"] + if project_data is None: + project["data"] = {} + elif isinstance(project_data, str): + project["data"] = json.loads(project_data) + projects.append(project) + return projects + def _query_projects(self) -> list[ProjectItem]: - projects = ayon_api.get_projects(fields=["name", "active", "library"]) + projects = self._fetch_projects_bc() + user = ayon_api.get_user() pinned_projects = ( user From 1cdde6d7779785deafd4996000e025af6dfa4bce Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:03:23 +0100 Subject: [PATCH 111/115] fix typo Thanks @BigRoy --- client/ayon_core/tools/common_models/projects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/common_models/projects.py b/client/ayon_core/tools/common_models/projects.py index 3e090e18b8..d81b581894 100644 --- a/client/ayon_core/tools/common_models/projects.py +++ b/client/ayon_core/tools/common_models/projects.py @@ -277,7 +277,7 @@ class ProductTypeIconMapping: return self._definitions_by_name -def _get_project_items_from_entitiy( +def _get_project_items_from_entity( projects: list[dict[str, Any]] ) -> list[ProjectItem]: """ @@ -575,7 +575,7 @@ class ProjectsModel(object): .get("pinnedProjects") ) or [] pinned_projects = set(pinned_projects) - project_items = _get_project_items_from_entitiy(list(projects)) + project_items = _get_project_items_from_entity(list(projects)) for project in project_items: project.is_pinned = project.name in pinned_projects return project_items From be9b476151b455408d8d076ac944ebbe7bc1e3a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:03:31 +0100 Subject: [PATCH 112/115] use better method name --- client/ayon_core/tools/common_models/projects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/common_models/projects.py b/client/ayon_core/tools/common_models/projects.py index d81b581894..0c1f912fd1 100644 --- a/client/ayon_core/tools/common_models/projects.py +++ b/client/ayon_core/tools/common_models/projects.py @@ -541,7 +541,7 @@ class ProjectsModel(object): self._projects_cache.update_data(project_items) return self._projects_cache.get_data() - def _fetch_projects_bc(self) -> list[dict[str, Any]]: + def _fetch_graphql_projects(self) -> list[dict[str, Any]]: """Fetch projects using GraphQl. This method was added because ayon_api had a bug in 'get_projects'. @@ -565,7 +565,7 @@ class ProjectsModel(object): return projects def _query_projects(self) -> list[ProjectItem]: - projects = self._fetch_projects_bc() + projects = self._fetch_graphql_projects() user = ayon_api.get_user() pinned_projects = ( From 26839fa5c1b52ac4535eb0fbf19283991d05b411 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 12 Nov 2025 17:07:05 +0000 Subject: [PATCH 113/115] [Automated] Add generated package files from main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index e481e81356..869831b3ab 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.6.8+dev" +__version__ = "1.6.9" diff --git a/package.py b/package.py index eb30148176..cbfae1a4b3 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.8+dev" +version = "1.6.9" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 6708432e85..92c336770d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.8+dev" +version = "1.6.9" description = "" authors = ["Ynput Team "] readme = "README.md" From ea81e643f2d7158a50d4a6feebd6ee8ef3113ec4 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 12 Nov 2025 17:07:38 +0000 Subject: [PATCH 114/115] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 869831b3ab..da0cbff11d 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.6.9" +__version__ = "1.6.9+dev" diff --git a/package.py b/package.py index cbfae1a4b3..99524be8aa 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.9" +version = "1.6.9+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 92c336770d..f69f4f843a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.9" +version = "1.6.9+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 2ce5ba257502e279e9f5474ee88b27623fed75b5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 12 Nov 2025 17:08:36 +0000 Subject: [PATCH 115/115] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 513e088fef..e48e4b3b29 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to AYON Tray options: + - 1.6.9 - 1.6.8 - 1.6.7 - 1.6.6