From 424e206f099e2e85e6faf4e078a8fbe3b9468497 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 29 Feb 2024 16:53:17 +0100 Subject: [PATCH 01/66] Refactor create_bin function to include a set_as_current parameter for setting the resulting bin as the current folder. Add iter_all_media_pool_clips function to recursively iterate all media pool clips in the current project. Introduce RemoveUnusedMedia inventory action plugin to remove unused media pool items and LoadMedia loader plugin for loading media into Resolve. adaptation from https://github.com/BigRoy/OpenPype/commit/37da9c6f785a645d11a6274fa6ef738450ce967c#diff-3ec58869aa5ff3f62216d417256778fd5b44ea0132b953e67d7880f5e7b53df5 --- client/ayon_core/hosts/resolve/api/lib.py | 71 ++-- .../ayon_core/hosts/resolve/api/pipeline.py | 24 ++ .../remove_unused_media_pool_items.py | 31 ++ .../hosts/resolve/plugins/load/load_media.py | 365 ++++++++++++++++++ 4 files changed, 469 insertions(+), 22 deletions(-) create mode 100644 client/ayon_core/hosts/resolve/plugins/inventory/remove_unused_media_pool_items.py create mode 100644 client/ayon_core/hosts/resolve/plugins/load/load_media.py diff --git a/client/ayon_core/hosts/resolve/api/lib.py b/client/ayon_core/hosts/resolve/api/lib.py index 6e4e17811f..18d6aa31e5 100644 --- a/client/ayon_core/hosts/resolve/api/lib.py +++ b/client/ayon_core/hosts/resolve/api/lib.py @@ -145,7 +145,9 @@ def get_new_timeline(timeline_name: str = None): return new_timeline -def create_bin(name: str, root: object = None) -> object: +def create_bin(name: str, + root: object = None, + set_as_current: bool = True) -> object: """ Create media pool's folder. @@ -156,6 +158,8 @@ def create_bin(name: str, root: object = None) -> object: Args: name (str): name of folder / bin, or hierarchycal name "parent/name" root (resolve.Folder)[optional]: root folder / bin object + set_as_current (resolve.Folder)[optional]: Whether to set the + resulting bin as current folder or not. Returns: object: resolve.Folder @@ -168,22 +172,24 @@ def create_bin(name: str, root: object = None) -> object: if "/" in name.replace("\\", "/"): child_bin = None for bname in name.split("/"): - child_bin = create_bin(bname, child_bin or root_bin) + child_bin = create_bin(bname, + root=child_bin or root_bin, + set_as_current=set_as_current) if child_bin: return child_bin else: - created_bin = None + # Find existing folder or create it for subfolder in root_bin.GetSubFolderList(): - if subfolder.GetName() in name: + if subfolder.GetName() == name: created_bin = subfolder - - if not created_bin: - new_folder = media_pool.AddSubFolder(root_bin, name) - media_pool.SetCurrentFolder(new_folder) + break else: + created_bin = media_pool.AddSubFolder(root_bin, name) + + if set_as_current: media_pool.SetCurrentFolder(created_bin) - return media_pool.GetCurrentFolder() + return created_bin def remove_media_pool_item(media_pool_item: object) -> bool: @@ -272,8 +278,7 @@ def create_timeline_item( # get all variables project = get_current_project() media_pool = project.GetMediaPool() - _clip_property = media_pool_item.GetClipProperty - clip_name = _clip_property("File Name") + clip_name = media_pool_item.GetClipProperty("File Name") timeline = timeline or get_current_timeline() # timing variables @@ -298,16 +303,22 @@ def create_timeline_item( if source_end: clip_data["endFrame"] = source_end if timecode_in: + # Note: specifying a recordFrame will fail to place the timeline + # item if there's already an existing clip at that time on the + # active track. clip_data["recordFrame"] = timeline_in # add to timeline - media_pool.AppendToTimeline([clip_data]) + output_timeline_item = media_pool.AppendToTimeline([clip_data])[0] - output_timeline_item = get_timeline_item( - media_pool_item, timeline) + # Adding the item may fail whilst Resolve will still return a + # TimelineItem instance - however all `Get*` calls return None + # Hence, we check whether the result is valid + if output_timeline_item.GetDuration() is None: + output_timeline_item = None assert output_timeline_item, AssertionError(( - "Clip name '{}' was't created on the timeline: '{}' \n\n" + "Clip name '{}' wasn't created on the timeline: '{}' \n\n" "Please check if correct track position is activated, \n" "or if a clip is not already at the timeline in \n" "position: '{}' out: '{}'. \n\n" @@ -330,19 +341,25 @@ def get_timeline_item(media_pool_item: object, Returns: object: resolve.TimelineItem """ - _clip_property = media_pool_item.GetClipProperty - clip_name = _clip_property("File Name") + clip_name = media_pool_item.GetClipProperty("File Name") output_timeline_item = None timeline = timeline or get_current_timeline() with maintain_current_timeline(timeline): # search the timeline for the added clip - for _ti_data in get_current_timeline_items(): - _ti_clip = _ti_data["clip"]["item"] - _ti_clip_property = _ti_clip.GetMediaPoolItem().GetClipProperty - if clip_name in _ti_clip_property("File Name"): - output_timeline_item = _ti_clip + for ti_data in get_current_timeline_items(): + ti_clip_item = ti_data["clip"]["item"] + ti_media_pool_item = ti_clip_item.GetMediaPoolItem() + + # Skip items that do not have a media pool item, like for example + # an "Adjustment Clip" or a "Fusion Composition" from the effects + # toolbox + if not ti_media_pool_item: + continue + + if clip_name in ti_media_pool_item.GetClipProperty("File Name"): + output_timeline_item = ti_clip_item return output_timeline_item @@ -936,3 +953,13 @@ def get_reformated_path(path, padded=False, first=False): else: path = re.sub(num_pattern, "%d", path) return path + + +def iter_all_media_pool_clips(): + """Recursively iterate all media pool clips in current project""" + root = get_current_project().GetMediaPool().GetRootFolder() + queue = [root] + for folder in queue: + for clip in folder.GetClipList(): + yield clip + queue.extend(folder.GetSubFolderList()) diff --git a/client/ayon_core/hosts/resolve/api/pipeline.py b/client/ayon_core/hosts/resolve/api/pipeline.py index 19d33971dc..78ded41ed9 100644 --- a/client/ayon_core/hosts/resolve/api/pipeline.py +++ b/client/ayon_core/hosts/resolve/api/pipeline.py @@ -2,6 +2,7 @@ Basic avalon integration """ import os +import json import contextlib from collections import OrderedDict @@ -12,6 +13,7 @@ from ayon_core.pipeline import ( schema, register_loader_plugin_path, register_creator_plugin_path, + register_inventory_action_path, AVALON_CONTAINER_ID, ) from ayon_core.host import ( @@ -38,6 +40,7 @@ PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") AVALON_CONTAINERS = ":AVALON_CONTAINERS" @@ -65,6 +68,7 @@ class ResolveHost(HostBase, IWorkfileHost, ILoadHost): register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) + register_inventory_action_path(INVENTORY_PATH) # register callback for switching publishable pyblish.register_callback("instanceToggled", @@ -145,6 +149,26 @@ def ls(): and the Maya equivalent, which is in `avalon.maya.pipeline` """ + # Media Pool instances from Load Media loader + for clip in lib.iter_all_media_pool_clips(): + data = clip.GetMetadata(lib.pype_tag_name) + if not data: + continue + data = json.loads(data) + + # If not all required data, skip it + required = ['schema', 'id', 'loader', 'representation'] + if not all(key in data for key in required): + continue + + container = {key: data[key] for key in required} + container["objectName"] = clip.GetName() # Get path in folders + container["namespace"] = clip.GetName() + container["name"] = clip.GetUniqueId() + container["_item"] = clip + yield container + + # Timeline instances from Load Clip loader # get all track items from current timeline all_timeline_items = lib.get_current_timeline_items(filter=False) diff --git a/client/ayon_core/hosts/resolve/plugins/inventory/remove_unused_media_pool_items.py b/client/ayon_core/hosts/resolve/plugins/inventory/remove_unused_media_pool_items.py new file mode 100644 index 0000000000..6698508d24 --- /dev/null +++ b/client/ayon_core/hosts/resolve/plugins/inventory/remove_unused_media_pool_items.py @@ -0,0 +1,31 @@ +from ayon_core.pipeline import ( + InventoryAction, +) +from ayon_core.pipeline.load.utils import remove_container + + +class RemoveUnusedMedia(InventoryAction): + + label = "Remove Unused Media" + icon = "trash" + + @staticmethod + def is_compatible(container): + return ( + container.get("loader") == "LoadMedia" + ) + + def process(self, containers): + any_removed = False + for container in containers: + media_pool_item = container["_item"] + usage = int(media_pool_item.GetClipProperty("Usage")) + name = media_pool_item.GetName() + if usage == 0: + print(f"Removing {name}") + remove_container(container) + any_removed = True + else: + print(f"Keeping {name} with usage: {usage}") + + return any_removed diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_media.py b/client/ayon_core/hosts/resolve/plugins/load/load_media.py new file mode 100644 index 0000000000..2dda2655db --- /dev/null +++ b/client/ayon_core/hosts/resolve/plugins/load/load_media.py @@ -0,0 +1,365 @@ +import json +import copy +from collections import defaultdict +from typing import Union + +from openpype.client import ( + get_version_by_id, + get_last_version_by_subset_id, +) +from openpype.pipeline import ( + LoaderPlugin, + get_representation_context, + get_representation_path, + registered_host +) +from openpype.pipeline.context_tools import get_current_project_name +from openpype.hosts.resolve.api import lib +from openpype.hosts.resolve.api.pipeline import AVALON_CONTAINER_ID +from openpype.lib.transcoding import ( + VIDEO_EXTENSIONS, + IMAGE_EXTENSIONS +) +from openpype.lib import BoolDef + + +def find_clip_usage(media_pool_item, project=None): + """Return all Timeline Items in the project using the Media Pool Item. + + Each entry in the list is a tuple of Timeline and TimelineItem so that + it's easy to know which Timeline the TimelineItem belongs to. + + Arguments: + media_pool_item (MediaPoolItem): The Media Pool Item to search for. + project (Project): The resolve project the media pool item resides in. + + Returns: + List[Tuple[Timeline, TimelineItem]]: A 2-tuple of a timeline with + the timeline item. + + """ + usage = int(media_pool_item.GetClipProperty("Usage")) + if not usage: + return [] + + if project is None: + project = lib.get_current_project() + + matching_items = [] + unique_id = media_pool_item.GetUniqueId() + for timeline_idx in range(project.GetTimelineCount()): + timeline = project.GetTimelineByIndex(timeline_idx+1) + + # Consider audio and video tracks + for track_type in ["video", "audio"]: + for track_idx in range(timeline.GetTrackCount(track_type)): + timeline_items = timeline.GetItemListInTrack(track_type, + track_idx+1) + for timeline_item in timeline_items: + timeline_item_mpi = timeline_item.GetMediaPoolItem() + if not timeline_item_mpi: + continue + + if timeline_item_mpi.GetUniqueId() == unique_id: + matching_items.append((timeline, timeline_item)) + usage -= 1 + if usage <= 0: + # If there should be no usage left after this found + # entry we return early + return matching_items + + return matching_items + + +class LoadMedia(LoaderPlugin): + """Load a subset as media pool item.""" + + families = ["render2d", "source", "plate", "render", "review"] + + representations = ["*"] + extensions = set( + ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS) + ) + + label = "Load media" + order = -20 + icon = "code-fork" + color = "orange" + + options = [ + BoolDef( + "load_to_timeline", + label="Load to timeline", + default=True, + tooltip="Whether on load to automatically add it to the current " + "timeline" + ), + BoolDef( + "load_once", + label="Re-use existing", + default=True, + tooltip="When enabled - if this particular version is already" + "loaded it will not be loaded again but will be re-used." + ) + ] + + # for loader multiselection + timeline = None + + # presets + clip_color_last = "Olive" + clip_color = "Orange" + + bin_path = "Loader/{representation[context][hierarchy]}/{asset[name]}" + + def load(self, context, name, namespace, options): + + # For loading multiselection, we store timeline before first load + # because the current timeline can change with the imported media. + if self.timeline is None: + self.timeline = lib.get_current_timeline() + + representation = context["representation"] + + project = lib.get_current_project() + media_pool = project.GetMediaPool() + + # Allow to use an existing media pool item and re-use it + item = None + if options.get("load_once", True): + host = registered_host() + repre_id = str(context["representation"]["_id"]) + for container in host.ls(): + if container["representation"] != repre_id: + continue + + if container["loader"] != self.__class__.__name__: + continue + + print(f"Re-using existing container: {container}") + item = container["_item"] + + if item is None: + # Create or set the bin folder, we add it in there + # If bin path is not set we just add into the current active bin + if self.bin_path: + bin_path = self.bin_path.format(**context) + folder = lib.create_bin( + name=bin_path, + root=media_pool.GetRootFolder(), + set_as_current=False + ) + media_pool.SetCurrentFolder(folder) + + # Import media + path = self._get_filepath(representation) + items = media_pool.ImportMedia([path]) + + assert len(items) == 1, "Must import only one media item" + item = items[0] + + self._set_metadata(media_pool_item=item, + context=context) + + data = self._get_container_data(representation) + + # Add containerise data only needed on first load + data.update({ + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "loader": str(self.__class__.__name__), + }) + + item.SetMetadata(lib.pype_tag_name, json.dumps(data)) + + # Always update clip color - even if re-using existing clip + color = self.get_item_color(representation) + item.SetClipColor(color) + + if options.get("load_to_timeline", True): + timeline = options.get("timeline", self.timeline) + if timeline: + # Add media to active timeline + lib.create_timeline_item( + media_pool_item=item, + timeline=timeline + ) + + def switch(self, container, representation): + self.update(container, representation) + + def update(self, container, representation): + # Update MediaPoolItem filepath and metadata + item = container["_item"] + + # Get the existing metadata before we update because the + # metadata gets removed + data = json.loads(item.GetMetadata(lib.pype_tag_name)) + + # Update path + path = self._get_filepath(representation) + item.ReplaceClip(path) + + # Update the metadata + update_data = self._get_container_data(representation) + data.update(update_data) + item.SetMetadata(lib.pype_tag_name, json.dumps(data)) + + context = get_representation_context(representation) + self._set_metadata(media_pool_item=item, context=context) + + # Update the clip color + color = self.get_item_color(representation) + item.SetClipColor(color) + + def remove(self, container): + # Remove MediaPoolItem entry + project = lib.get_current_project() + media_pool = project.GetMediaPool() + item = container["_item"] + + # Delete any usages of the media pool item so there's no trail + # left in existing timelines. Currently only the media pool item + # gets removed which fits the Resolve workflow but is confusing + # artists + usage = find_clip_usage(media_pool_item=item, project=project) + if usage: + # Group all timeline items per timeline, so we can delete the clips + # in the timeline at once. The Resolve objects are not hashable, so + # we need to store them in the dict by id + usage_by_timeline = defaultdict(list) + timeline_by_id = {} + for timeline, timeline_item in usage: + timeline_id = timeline.GetUniqueId() + timeline_by_id[timeline_id] = timeline + usage_by_timeline[timeline.GetUniqueId()].append(timeline_item) + + for timeline_id, timeline_items in usage_by_timeline.items(): + timeline = timeline_by_id[timeline_id] + timeline.DeleteClips(timeline_items) + + # Delete the media pool item + media_pool.DeleteClips([item]) + + def _get_container_data(self, representation): + """Return metadata related to the representation and version.""" + + # load clip to timeline and get main variables + project_name = get_current_project_name() + version = get_version_by_id(project_name, representation["parent"]) + version_data = version.get("data", {}) + version_name = version.get("name", None) + colorspace = version_data.get("colorspace", None) + + # add additional metadata from the version to imprint Avalon knob + add_keys = [ + "frameStart", "frameEnd", "source", "author", + "fps", "handleStart", "handleEnd" + ] + data = { + key: version_data.get(key, str(None)) for key in add_keys + } + + # add variables related to version context + data.update({ + "representation": str(representation["_id"]), + "version": version_name, + "colorspace": colorspace, + }) + + return data + + @classmethod + def get_item_color(cls, representation) -> str: + """Return item color name. + + Coloring depends on whether representation is the latest version. + """ + # Compare version with last version + project_name = get_current_project_name() + version = get_version_by_id( + project_name, + representation["parent"], + fields=["name", "parent"] + ) + last_version = get_last_version_by_subset_id( + project_name, + version["parent"], + fields=["name"] + ) or {} + + # set clip colour + if version.get("name") == last_version.get("name"): + return cls.clip_color_last + else: + return cls.clip_color + + def _set_metadata(self, media_pool_item, context: dict): + """Set Media Pool Item Clip Properties""" + + # Set the timecode for the loaded clip if Resolve doesn't parse it + # correctly from the input. An image sequence will have timecode + # parsed from its frame range, we will want to preserve that. + # TODO: Setting the Start TC breaks existing clips on update + # See: https://forum.blackmagicdesign.com/viewtopic.php?f=21&t=197327 + # Once that's resolved we should enable this + # start_tc = media_pool_item.GetClipProperty("Start TC") + # if start_tc == "00:00:00:00": + # from openpype.pipeline.editorial import frames_to_timecode + # # Assume no timecode was detected from the source media + # + # fps = float(media_pool_item.GetClipProperty("FPS")) + # handle_start = context["version"]["data"].get("handleStart", 0) + # frame_start = context["version"]["data"].get("frameStart", 0) + # frame_start_handle = frame_start - handle_start + # timecode = frames_to_timecode(frame_start_handle, fps) + # + # if timecode != start_tc: + # media_pool_item.SetClipProperty("Start TC", timecode) + + # Set more clip metadata based on the loaded clip's context + metadata = { + "Clip Name": "{asset[name]} {subset[name]} " + "v{version[name]:03d} ({representation[name]})", + "Shot": "{asset[name]}", + "Take": "{subset[name]} v{version[name]:03d}", + "Comments": "{version[data][comment]}" + } + for clip_property, value in metadata.items(): + media_pool_item.SetClipProperty(clip_property, + value.format_map(context)) + + def _get_filepath(self, representation: dict) -> Union[str, dict]: + + is_sequence = bool(representation["context"].get("frame")) + if not is_sequence: + return get_representation_path(representation) + + context = get_representation_context(representation) + version = context["version"] + + # Get the start and end frame of the image sequence, incl. handles + frame_start = version["data"].get("frameStart", 0) + frame_end = version["data"].get("frameEnd", 0) + handle_start = version["data"].get("handleStart", 0) + handle_end = version["data"].get("handleEnd", 0) + frame_start_handle = frame_start - handle_start + frame_end_handle = frame_end + handle_end + padding = len(representation["context"].get("frame")) + + # We format the frame number to the required token. To do so + # we in-place change the representation context data to format the path + # with that replaced data + representation = copy.deepcopy(representation) + representation["context"]["frame"] = f"%0{padding}d" + path = get_representation_path(representation) + + # See Resolve API, to import for example clip "file_[001-100].dpx": + # ImportMedia([{"FilePath":"file_%03d.dpx", + # "StartIndex":1, + # "EndIndex":100}]) + return { + "FilePath": path, + "StartIndex": frame_start_handle, + "EndIndex": frame_end_handle + } From 5ee9fca4026e176f80f9aa18394f9d120daf5346 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 29 Feb 2024 17:05:48 +0100 Subject: [PATCH 02/66] Update client and pipeline imports, refactor transcoding library references, and adjust track index handling in find_clip_usage function. --- .../hosts/resolve/plugins/load/load_media.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_media.py b/client/ayon_core/hosts/resolve/plugins/load/load_media.py index 2dda2655db..60fbea483e 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_media.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_media.py @@ -3,24 +3,24 @@ import copy from collections import defaultdict from typing import Union -from openpype.client import ( +from ayon_core.client import ( get_version_by_id, get_last_version_by_subset_id, ) -from openpype.pipeline import ( +from ayon_core.pipeline import ( LoaderPlugin, get_representation_context, get_representation_path, registered_host ) -from openpype.pipeline.context_tools import get_current_project_name -from openpype.hosts.resolve.api import lib -from openpype.hosts.resolve.api.pipeline import AVALON_CONTAINER_ID -from openpype.lib.transcoding import ( +from ayon_core.pipeline.context_tools import get_current_project_name +from ayon_core.hosts.resolve.api import lib +from ayon_core.hosts.resolve.api.pipeline import AVALON_CONTAINER_ID +from ayon_core.lib.transcoding import ( VIDEO_EXTENSIONS, IMAGE_EXTENSIONS ) -from openpype.lib import BoolDef +from ayon_core.lib import BoolDef def find_clip_usage(media_pool_item, project=None): @@ -48,13 +48,13 @@ def find_clip_usage(media_pool_item, project=None): matching_items = [] unique_id = media_pool_item.GetUniqueId() for timeline_idx in range(project.GetTimelineCount()): - timeline = project.GetTimelineByIndex(timeline_idx+1) + timeline = project.GetTimelineByIndex(timeline_idx + 1) # Consider audio and video tracks for track_type in ["video", "audio"]: for track_idx in range(timeline.GetTrackCount(track_type)): timeline_items = timeline.GetItemListInTrack(track_type, - track_idx+1) + track_idx + 1) for timeline_item in timeline_items: timeline_item_mpi = timeline_item.GetMediaPoolItem() if not timeline_item_mpi: From 7d391231ea3df68a58f0484dc2efc5246e03e1cf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Mar 2024 17:28:41 +0100 Subject: [PATCH 03/66] Update metadata handling in LoadMedia class Added a list of metadata items for clip properties. Refactored setting clip metadata based on context. --- .../hosts/resolve/plugins/load/load_media.py | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_media.py b/client/ayon_core/hosts/resolve/plugins/load/load_media.py index 60fbea483e..dd71e429f1 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_media.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_media.py @@ -112,6 +112,29 @@ class LoadMedia(LoaderPlugin): bin_path = "Loader/{representation[context][hierarchy]}/{asset[name]}" + metadata = [ + { + "name": "Comments", + "value": "{version[data][comment]}" + }, + { + "name": "Shot", + "value": "{asset[name]}" + }, + { + "name": "Take", + "value": "{subset[name]} v{version[name]:03d}" + }, + { + "name": "Clip Name", + "value": ( + "{asset[name]} {subset[name]} " + "v{version[name]:03d} ({representation[name]})" + ) + } + ] + + def load(self, context, name, namespace, options): # For loading multiselection, we store timeline before first load @@ -297,35 +320,10 @@ class LoadMedia(LoaderPlugin): def _set_metadata(self, media_pool_item, context: dict): """Set Media Pool Item Clip Properties""" - # Set the timecode for the loaded clip if Resolve doesn't parse it - # correctly from the input. An image sequence will have timecode - # parsed from its frame range, we will want to preserve that. - # TODO: Setting the Start TC breaks existing clips on update - # See: https://forum.blackmagicdesign.com/viewtopic.php?f=21&t=197327 - # Once that's resolved we should enable this - # start_tc = media_pool_item.GetClipProperty("Start TC") - # if start_tc == "00:00:00:00": - # from openpype.pipeline.editorial import frames_to_timecode - # # Assume no timecode was detected from the source media - # - # fps = float(media_pool_item.GetClipProperty("FPS")) - # handle_start = context["version"]["data"].get("handleStart", 0) - # frame_start = context["version"]["data"].get("frameStart", 0) - # frame_start_handle = frame_start - handle_start - # timecode = frames_to_timecode(frame_start_handle, fps) - # - # if timecode != start_tc: - # media_pool_item.SetClipProperty("Start TC", timecode) - # Set more clip metadata based on the loaded clip's context - metadata = { - "Clip Name": "{asset[name]} {subset[name]} " - "v{version[name]:03d} ({representation[name]})", - "Shot": "{asset[name]}", - "Take": "{subset[name]} v{version[name]:03d}", - "Comments": "{version[data][comment]}" - } - for clip_property, value in metadata.items(): + for meta_item in self.metadata: + clip_property = meta_item["name"] + value = meta_item["value"] media_pool_item.SetClipProperty(clip_property, value.format_map(context)) From 1f9b9c03623d264ace3c17d434fc34e9c9a3e4d8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Mar 2024 22:45:37 +0100 Subject: [PATCH 04/66] Update Resolve settings and LoadMedia plugin configurations: - Refactored clip color variables - Updated media pool bin path template - Modified metadata mapping structure for LoadMedia plugin --- .../hosts/resolve/plugins/load/load_media.py | 43 +++----- server_addon/resolve/server/settings.py | 100 +++++++++++++++++- server_addon/resolve/server/version.py | 2 +- 3 files changed, 109 insertions(+), 36 deletions(-) diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_media.py b/client/ayon_core/hosts/resolve/plugins/load/load_media.py index dd71e429f1..e6270ee2ab 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_media.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_media.py @@ -7,6 +7,7 @@ from ayon_core.client import ( get_version_by_id, get_last_version_by_subset_id, ) +from ayon_core.lib import StringTemplate from ayon_core.pipeline import ( LoaderPlugin, get_representation_context, @@ -108,32 +109,12 @@ class LoadMedia(LoaderPlugin): # presets clip_color_last = "Olive" - clip_color = "Orange" + clip_color_old = "Orange" - bin_path = "Loader/{representation[context][hierarchy]}/{asset[name]}" - - metadata = [ - { - "name": "Comments", - "value": "{version[data][comment]}" - }, - { - "name": "Shot", - "value": "{asset[name]}" - }, - { - "name": "Take", - "value": "{subset[name]} v{version[name]:03d}" - }, - { - "name": "Clip Name", - "value": ( - "{asset[name]} {subset[name]} " - "v{version[name]:03d} ({representation[name]})" - ) - } - ] + media_pool_bin_path = ( + "Loader/{representation[context][hierarchy]}/{asset[name]}") + metadata = [] def load(self, context, name, namespace, options): @@ -165,10 +146,11 @@ class LoadMedia(LoaderPlugin): if item is None: # Create or set the bin folder, we add it in there # If bin path is not set we just add into the current active bin - if self.bin_path: - bin_path = self.bin_path.format(**context) + if self.media_pool_bin_path: + media_pool_bin_path = StringTemplate( + self.media_pool_bin_path).format_strict(context) folder = lib.create_bin( - name=bin_path, + name=media_pool_bin_path, root=media_pool.GetRootFolder(), set_as_current=False ) @@ -315,7 +297,7 @@ class LoadMedia(LoaderPlugin): if version.get("name") == last_version.get("name"): return cls.clip_color_last else: - return cls.clip_color + return cls.clip_color_old def _set_metadata(self, media_pool_item, context: dict): """Set Media Pool Item Clip Properties""" @@ -324,8 +306,9 @@ class LoadMedia(LoaderPlugin): for meta_item in self.metadata: clip_property = meta_item["name"] value = meta_item["value"] - media_pool_item.SetClipProperty(clip_property, - value.format_map(context)) + value_formatted = StringTemplate(value).format_strict(context) + media_pool_item.SetClipProperty( + clip_property, value_formatted) def _get_filepath(self, representation: dict) -> Union[str, dict]: diff --git a/server_addon/resolve/server/settings.py b/server_addon/resolve/server/settings.py index dcdb2f1b27..d7c098d023 100644 --- a/server_addon/resolve/server/settings.py +++ b/server_addon/resolve/server/settings.py @@ -1,4 +1,9 @@ -from ayon_server.settings import BaseSettingsModel, SettingsField +from pydantic import validator +from ayon_server.settings import ( + BaseSettingsModel, + SettingsField, + ensure_unique_names, +) from .imageio import ResolveImageIOModel @@ -56,7 +61,7 @@ class CreateShotClipModels(BaseSettingsModel): workfileFrameStart: int = SettingsField( 1001, - title="Workfiles Start Frame", + title="Workfile Start Frame", section="Shot Attributes" ) handleStart: int = SettingsField( @@ -69,13 +74,64 @@ class CreateShotClipModels(BaseSettingsModel): ) -class CreatorPuginsModel(BaseSettingsModel): +class CreatorPluginsModel(BaseSettingsModel): CreateShotClip: CreateShotClipModels = SettingsField( default_factory=CreateShotClipModels, title="Create Shot Clip" ) +class MetadataMappingModel(BaseSettingsModel): + """Metadata mapping + + Representation document context data are used for formatting of + anatomy tokens. Following are supported: + - version + - task + - asset + + """ + name: str = SettingsField( + "", + title="Metadata property name" + ) + value: str = SettingsField( + "", + title="Metadata value template" + ) + + +class LoadMediaModel(BaseSettingsModel): + clip_color_last: str = SettingsField( + "Olive", + title="Clip color for last version" + ) + clip_color_old: str = SettingsField( + "Orange", + title="Clip color for old version" + ) + media_pool_bin_path: str = SettingsField( + "Loader/{representation[context][hierarchy]}/{asset[name]}", + title="Media Pool bin path template" + ) + metadata: list[MetadataMappingModel] = SettingsField( + default_factory=list, + title="Metadata mapping" + ) + + @validator("metadata") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + + +class LoaderPluginsModel(BaseSettingsModel): + LoadMedia: LoadMediaModel = SettingsField( + default_factory=LoadMediaModel, + title="Load Media" + ) + + class ResolveSettings(BaseSettingsModel): launch_openpype_menu_on_start: bool = SettingsField( False, title="Launch OpenPype menu on start of Resolve" @@ -84,10 +140,14 @@ class ResolveSettings(BaseSettingsModel): default_factory=ResolveImageIOModel, title="Color Management (ImageIO)" ) - create: CreatorPuginsModel = SettingsField( - default_factory=CreatorPuginsModel, + create: CreatorPluginsModel = SettingsField( + default_factory=CreatorPluginsModel, title="Creator plugins", ) + load: LoaderPluginsModel = SettingsField( + default_factory=LoaderPluginsModel, + title="Loader plugins", + ) DEFAULT_VALUES = { @@ -109,5 +169,35 @@ DEFAULT_VALUES = { "handleStart": 10, "handleEnd": 10 } + }, + "load": { + "LoadMedia": { + "clip_color_last": "Olive", + "clip_color_old": "Orange", + "media_pool_bin_path": ( + "Loader/{representation[context][hierarchy]}/{asset[name]}" + ), + "metadata": [ + { + "name": "Comments", + "value": "{version[data][comment]}" + }, + { + "name": "Shot", + "value": "{asset[name]}" + }, + { + "name": "Take", + "value": "{subset[name]} v{version[name]:0>3}" + }, + { + "name": "Clip Name", + "value": ( + "{asset[name]} {subset[name]} " + "v{version[name]:0>3} ({representation[name]})" + ) + } + ] + } } } diff --git a/server_addon/resolve/server/version.py b/server_addon/resolve/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/resolve/server/version.py +++ b/server_addon/resolve/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" From b59c646469090bd3f10c95477aa8baa3023209c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 12 Mar 2024 11:05:54 +0100 Subject: [PATCH 05/66] Reversing changes those are addressed in https://github.com/ynput/ayon-core/pull/92 --- client/ayon_core/hosts/resolve/api/lib.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/hosts/resolve/api/lib.py b/client/ayon_core/hosts/resolve/api/lib.py index 18d6aa31e5..deeb8d4d11 100644 --- a/client/ayon_core/hosts/resolve/api/lib.py +++ b/client/ayon_core/hosts/resolve/api/lib.py @@ -341,25 +341,19 @@ def get_timeline_item(media_pool_item: object, Returns: object: resolve.TimelineItem """ - clip_name = media_pool_item.GetClipProperty("File Name") + _clip_property = media_pool_item.GetClipProperty + clip_name = _clip_property("File Name") output_timeline_item = None timeline = timeline or get_current_timeline() with maintain_current_timeline(timeline): # search the timeline for the added clip - for ti_data in get_current_timeline_items(): - ti_clip_item = ti_data["clip"]["item"] - ti_media_pool_item = ti_clip_item.GetMediaPoolItem() - - # Skip items that do not have a media pool item, like for example - # an "Adjustment Clip" or a "Fusion Composition" from the effects - # toolbox - if not ti_media_pool_item: - continue - - if clip_name in ti_media_pool_item.GetClipProperty("File Name"): - output_timeline_item = ti_clip_item + for _ti_data in get_current_timeline_items(): + _ti_clip = _ti_data["clip"]["item"] + _ti_clip_property = _ti_clip.GetMediaPoolItem().GetClipProperty + if clip_name in _ti_clip_property("File Name"): + output_timeline_item = _ti_clip return output_timeline_item From 084472647d61ee08941c65fcebdda563a7afecfc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Mar 2024 16:35:44 +0100 Subject: [PATCH 06/66] Fixing reported bug on updating mediapoolitem --- client/ayon_core/hosts/resolve/plugins/load/load_media.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_media.py b/client/ayon_core/hosts/resolve/plugins/load/load_media.py index e6270ee2ab..7f56eafe53 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_media.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_media.py @@ -202,8 +202,12 @@ class LoadMedia(LoaderPlugin): data = json.loads(item.GetMetadata(lib.pype_tag_name)) # Update path - path = self._get_filepath(representation) - item.ReplaceClip(path) + path = get_representation_path(representation) + success = item.ReplaceClip(path) + if not success: + raise RuntimeError( + f"Failed to replace media pool item clip to filepath: {path}" + ) # Update the metadata update_data = self._get_container_data(representation) From 15b23b13313faf8080ed9c23c3365b4f5a7026f6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Mar 2024 16:37:16 +0100 Subject: [PATCH 07/66] Adapting Colorbleeds colorspace distribution https://github.com/BigRoy/OpenPype/commit/e22d5dbb9fe5b9006a6b81bb156e875c47ab3638 https://github.com/BigRoy/OpenPype/commit/a403669e8389681f4041438ecf443365c0f972b9 --- .../hosts/resolve/plugins/load/load_media.py | 131 +++++++++++++++++- 1 file changed, 127 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_media.py b/client/ayon_core/hosts/resolve/plugins/load/load_media.py index 7f56eafe53..ccbf76b3d0 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_media.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_media.py @@ -1,13 +1,15 @@ import json import copy +import contextlib from collections import defaultdict -from typing import Union +from typing import Union, Optional from ayon_core.client import ( get_version_by_id, get_last_version_by_subset_id, ) from ayon_core.lib import StringTemplate +from ayon_core.pipeline.colorspace import get_remapped_colorspace_to_native from ayon_core.pipeline import ( LoaderPlugin, get_representation_context, @@ -24,6 +26,57 @@ from ayon_core.lib.transcoding import ( from ayon_core.lib import BoolDef +@contextlib.contextmanager +def project_color_science_mode(project=None, mode="davinciYRGBColorManagedv2"): + """Set project color science mode during context. + + This is especially useful as context for setting the colorspace for media + pool items, because when Resolve is not set to `davinciYRGBColorManagedv2` + it fails to set its "Input Color Space" clip property even though it is + accessible and settable via the Resolve User Interface. + + Args + project (Project): The active Resolve Project. + mode (Optional[str]): The color science mode to apply during the + context. Defaults to 'davinciYRGBColorManagedv2' + + See Also: + https://forum.blackmagicdesign.com/viewtopic.php?f=21&t=197441 + """ + + if project is None: + project = lib.get_current_project() + + original_mode = project.GetSetting("colorScienceMode") + if original_mode != mode: + project.SetSetting("colorScienceMode", mode) + try: + yield + finally: + if project.GetSetting("colorScienceMode") != original_mode: + project.SetSetting("colorScienceMode", original_mode) + + +def set_colorspace(media_pool_item, + colorspace, + mode="davinciYRGBColorManagedv2"): + """Set MediaPoolItem colorspace. + This implements a workaround that you cannot set the input colorspace + unless the Resolve project's color science mode is set to + `davinciYRGBColorManagedv2`. + Args: + media_pool_item (MediaPoolItem): The media pool item. + colorspace (str): The colorspace to apply. + mode (Optional[str]): The Resolve project color science mode to be in + while setting the colorspace. + Defaults to 'davinciYRGBColorManagedv2' + Returns: + bool: Whether applying the colorspace succeeded. + """ + with project_color_science_mode(mode=mode): + return media_pool_item.SetClipProperty("Input Color Space", colorspace) + + def find_clip_usage(media_pool_item, project=None): """Return all Timeline Items in the project using the Media Pool Item. @@ -116,6 +169,13 @@ class LoadMedia(LoaderPlugin): metadata = [] + # cached on apply settings + _host_imageio_settings = None + + @classmethod + def apply_settings(cls, project_settings, system_settings): + cls._host_imageio_settings = project_settings["resolve"]["imageio"] + def load(self, context, name, namespace, options): # For loading multiselection, we store timeline before first load @@ -163,8 +223,8 @@ class LoadMedia(LoaderPlugin): assert len(items) == 1, "Must import only one media item" item = items[0] - self._set_metadata(media_pool_item=item, - context=context) + self._set_metadata(item, context) + self._set_colorspace_from_representation(item, representation) data = self._get_container_data(representation) @@ -201,6 +261,10 @@ class LoadMedia(LoaderPlugin): # metadata gets removed data = json.loads(item.GetMetadata(lib.pype_tag_name)) + # Get metadata to preserve after the clip replacement + # TODO: Maybe preserve more, like LUT, Alpha Mode, Input Sizing Preset + colorspace_before = item.GetClipProperty("Input Color Space") + # Update path path = get_representation_path(representation) success = item.ReplaceClip(path) @@ -215,7 +279,20 @@ class LoadMedia(LoaderPlugin): item.SetMetadata(lib.pype_tag_name, json.dumps(data)) context = get_representation_context(representation) - self._set_metadata(media_pool_item=item, context=context) + self._set_metadata(item, context) + self._set_colorspace_from_representation(item, representation) + + # If no specific colorspace is set then we want to preserve the + # colorspace a user might have set before the clip replacement + if ( + item.GetClipProperty("Input Color Space") == "Project" + and colorspace_before != "Project" + ): + result = set_colorspace(item, colorspace_before) + if not result: + self.log.warning( + f"Failed to re-apply colorspace: {colorspace_before}." + ) # Update the clip color color = self.get_item_color(representation) @@ -348,3 +425,49 @@ class LoadMedia(LoaderPlugin): "StartIndex": frame_start_handle, "EndIndex": frame_end_handle } + + def _get_colorspace(self, representation: dict) -> Optional[str]: + """Return Resolve native colorspace from OCIO colorspace data. + + Returns: + Optional[str]: The Resolve native colorspace name, if any mapped. + """ + + data = representation.get("data", {}).get("colorspaceData", {}) + if not data: + return + + ocio_colorspace = data["colorspace"] + if not ocio_colorspace: + return + + resolve_colorspace = get_remapped_colorspace_to_native( + ocio_colorspace_name=ocio_colorspace, + host_name="resolve", + imageio_host_settings=self._host_imageio_settings + ) + if resolve_colorspace: + return resolve_colorspace + else: + self.log.warning( + f"No mapping from OCIO colorspace '{ocio_colorspace}' " + "found to a Resolve colorspace. " + "Ignoring colorspace." + ) + + def _set_colorspace_from_representation( + self, media_pool_item, representation: dict): + """Set the colorspace for the media pool item. + + Args: + media_pool_item (MediaPoolItem): The media pool item. + representation (dict): The representation data. + """ + # Set the Resolve Input Color Space for the media. + colorspace = self._get_colorspace(representation) + if colorspace: + result = set_colorspace(media_pool_item, colorspace) + if not result: + self.log.warning( + f"Failed to apply colorspace: {colorspace}." + ) From dac1c2ba4c134ad697580ab5d5f9315d0e92c896 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 12:21:54 +0100 Subject: [PATCH 08/66] Fix default setting --- server_addon/resolve/server/settings.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server_addon/resolve/server/settings.py b/server_addon/resolve/server/settings.py index d7c098d023..9a3f78c0ed 100644 --- a/server_addon/resolve/server/settings.py +++ b/server_addon/resolve/server/settings.py @@ -111,7 +111,7 @@ class LoadMediaModel(BaseSettingsModel): title="Clip color for old version" ) media_pool_bin_path: str = SettingsField( - "Loader/{representation[context][hierarchy]}/{asset[name]}", + "Loader/{folder[path]}", title="Media Pool bin path template" ) metadata: list[MetadataMappingModel] = SettingsField( @@ -180,21 +180,21 @@ DEFAULT_VALUES = { "metadata": [ { "name": "Comments", - "value": "{version[data][comment]}" + "value": "{version[attrib][comment]}" }, { "name": "Shot", - "value": "{asset[name]}" + "value": "{folder[path]}" }, { "name": "Take", - "value": "{subset[name]} v{version[name]:0>3}" + "value": "{product[name]} {version[name]}" }, { "name": "Clip Name", "value": ( - "{asset[name]} {subset[name]} " - "v{version[name]:0>3} ({representation[name]})" + "{folder[path]} {product[name]} " + "{version[name]} ({representation[name]})" ) } ] From 78104de8488e75ef0bba5a37db993d59ab1a266a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 12:22:02 +0100 Subject: [PATCH 09/66] Add description --- server_addon/resolve/server/settings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server_addon/resolve/server/settings.py b/server_addon/resolve/server/settings.py index 9a3f78c0ed..7fd790837d 100644 --- a/server_addon/resolve/server/settings.py +++ b/server_addon/resolve/server/settings.py @@ -116,7 +116,11 @@ class LoadMediaModel(BaseSettingsModel): ) metadata: list[MetadataMappingModel] = SettingsField( default_factory=list, - title="Metadata mapping" + title="Metadata mapping", + description=( + "Set these media pool item metadata values on load and update. The" + " keys must match the exact Resolve metadata names like" + " 'Clip Name' or 'Shot'" ) @validator("metadata") From e610b7412dfd3566427c886cc03c86606acebef8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 12:22:18 +0100 Subject: [PATCH 10/66] Fix loader refactor to AYON --- .../hosts/resolve/plugins/load/load_media.py | 109 ++++++++---------- 1 file changed, 48 insertions(+), 61 deletions(-) diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_media.py b/client/ayon_core/hosts/resolve/plugins/load/load_media.py index ccbf76b3d0..c1016ce053 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_media.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_media.py @@ -2,21 +2,16 @@ import json import copy import contextlib from collections import defaultdict -from typing import Union, Optional +from typing import Union, List, Optional, TypedDict -from ayon_core.client import ( - get_version_by_id, - get_last_version_by_subset_id, -) +from ayon_api import version_is_latest from ayon_core.lib import StringTemplate from ayon_core.pipeline.colorspace import get_remapped_colorspace_to_native from ayon_core.pipeline import ( LoaderPlugin, - get_representation_context, get_representation_path, registered_host ) -from ayon_core.pipeline.context_tools import get_current_project_name from ayon_core.hosts.resolve.api import lib from ayon_core.hosts.resolve.api.pipeline import AVALON_CONTAINER_ID from ayon_core.lib.transcoding import ( @@ -26,6 +21,12 @@ from ayon_core.lib.transcoding import ( from ayon_core.lib import BoolDef +class MetadataEntry(TypedDict): + """Metadata entry is dict with {"name": "key", "value: "value"}""" + name: str + value: str + + @contextlib.contextmanager def project_color_science_mode(project=None, mode="davinciYRGBColorManagedv2"): """Set project color science mode during context. @@ -126,9 +127,9 @@ def find_clip_usage(media_pool_item, project=None): class LoadMedia(LoaderPlugin): - """Load a subset as media pool item.""" + """Load product as media pool item.""" - families = ["render2d", "source", "plate", "render", "review"] + product_types = {"render2d", "source", "plate", "render", "review"} representations = ["*"] extensions = set( @@ -164,16 +165,16 @@ class LoadMedia(LoaderPlugin): clip_color_last = "Olive" clip_color_old = "Orange" - media_pool_bin_path = ( - "Loader/{representation[context][hierarchy]}/{asset[name]}") + media_pool_bin_path = "Loader/{folder[path]}" - metadata = [] + metadata: List[MetadataEntry] = [] # cached on apply settings _host_imageio_settings = None @classmethod - def apply_settings(cls, project_settings, system_settings): + def apply_settings(cls, project_settings): + super(LoadMedia, cls).apply_settings(project_settings) cls._host_imageio_settings = project_settings["resolve"]["imageio"] def load(self, context, name, namespace, options): @@ -192,7 +193,7 @@ class LoadMedia(LoaderPlugin): item = None if options.get("load_once", True): host = registered_host() - repre_id = str(context["representation"]["_id"]) + repre_id = context["representation"]["id"] for container in host.ls(): if container["representation"] != repre_id: continue @@ -217,7 +218,7 @@ class LoadMedia(LoaderPlugin): media_pool.SetCurrentFolder(folder) # Import media - path = self._get_filepath(representation) + path = self._get_filepath(context) items = media_pool.ImportMedia([path]) assert len(items) == 1, "Must import only one media item" @@ -226,7 +227,7 @@ class LoadMedia(LoaderPlugin): self._set_metadata(item, context) self._set_colorspace_from_representation(item, representation) - data = self._get_container_data(representation) + data = self._get_container_data(context) # Add containerise data only needed on first load data.update({ @@ -238,7 +239,7 @@ class LoadMedia(LoaderPlugin): item.SetMetadata(lib.pype_tag_name, json.dumps(data)) # Always update clip color - even if re-using existing clip - color = self.get_item_color(representation) + color = self.get_item_color(context) item.SetClipColor(color) if options.get("load_to_timeline", True): @@ -250,10 +251,10 @@ class LoadMedia(LoaderPlugin): timeline=timeline ) - def switch(self, container, representation): - self.update(container, representation) + def switch(self, container, context): + self.update(container, context) - def update(self, container, representation): + def update(self, container, context): # Update MediaPoolItem filepath and metadata item = container["_item"] @@ -266,7 +267,7 @@ class LoadMedia(LoaderPlugin): colorspace_before = item.GetClipProperty("Input Color Space") # Update path - path = get_representation_path(representation) + path = get_representation_path(context["representation"]) success = item.ReplaceClip(path) if not success: raise RuntimeError( @@ -274,12 +275,11 @@ class LoadMedia(LoaderPlugin): ) # Update the metadata - update_data = self._get_container_data(representation) + update_data = self._get_container_data(context) data.update(update_data) item.SetMetadata(lib.pype_tag_name, json.dumps(data)) - context = get_representation_context(representation) - self._set_metadata(item, context) + self._set_metadata(media_pool_item=item, context=context) self._set_colorspace_from_representation(item, representation) # If no specific colorspace is set then we want to preserve the @@ -295,7 +295,7 @@ class LoadMedia(LoaderPlugin): ) # Update the clip color - color = self.get_item_color(representation) + color = self.get_item_color(context) item.SetClipColor(color) def remove(self, container): @@ -327,55 +327,43 @@ class LoadMedia(LoaderPlugin): # Delete the media pool item media_pool.DeleteClips([item]) - def _get_container_data(self, representation): + def _get_container_data(self, context: dict) -> dict: """Return metadata related to the representation and version.""" - # load clip to timeline and get main variables - project_name = get_current_project_name() - version = get_version_by_id(project_name, representation["parent"]) - version_data = version.get("data", {}) - version_name = version.get("name", None) - colorspace = version_data.get("colorspace", None) + # add additional metadata from the version to imprint AYON knob + version = context["version"] + data = {} - # add additional metadata from the version to imprint Avalon knob - add_keys = [ - "frameStart", "frameEnd", "source", "author", - "fps", "handleStart", "handleEnd" - ] - data = { - key: version_data.get(key, str(None)) for key in add_keys - } + # version.attrib + for key in [ + "frameStart", "frameEnd", + "handleStart", "handleEnd", + "source", "fps", "colorSpace" + ]: + data[key] = version["attrib"][key] + + # version.data + for key in ["author"]: + data[key] = version["data"][key] # add variables related to version context data.update({ - "representation": str(representation["_id"]), - "version": version_name, - "colorspace": colorspace, + "representation": context["representation"]["id"], + "version": version["name"], }) return data @classmethod - def get_item_color(cls, representation) -> str: + def get_item_color(cls, context: dict) -> str: """Return item color name. Coloring depends on whether representation is the latest version. """ # Compare version with last version - project_name = get_current_project_name() - version = get_version_by_id( - project_name, - representation["parent"], - fields=["name", "parent"] - ) - last_version = get_last_version_by_subset_id( - project_name, - version["parent"], - fields=["name"] - ) or {} - # set clip colour - if version.get("name") == last_version.get("name"): + if version_is_latest(project_name=context["project"]["name"], + version_id=context["version"]["id"]): return cls.clip_color_last else: return cls.clip_color_old @@ -388,16 +376,15 @@ class LoadMedia(LoaderPlugin): clip_property = meta_item["name"] value = meta_item["value"] value_formatted = StringTemplate(value).format_strict(context) - media_pool_item.SetClipProperty( - clip_property, value_formatted) + media_pool_item.SetClipProperty(clip_property, value_formatted) - def _get_filepath(self, representation: dict) -> Union[str, dict]: + def _get_filepath(self, context: dict) -> Union[str, dict]: + representation = context["representation"] is_sequence = bool(representation["context"].get("frame")) if not is_sequence: return get_representation_path(representation) - context = get_representation_context(representation) version = context["version"] # Get the start and end frame of the image sequence, incl. handles From 9b3e4008ed8ef2956c08534aa8b762d2cd984e74 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 12:25:54 +0100 Subject: [PATCH 11/66] Fix missing bracket --- server_addon/resolve/server/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server_addon/resolve/server/settings.py b/server_addon/resolve/server/settings.py index 7fd790837d..8fd996d8bd 100644 --- a/server_addon/resolve/server/settings.py +++ b/server_addon/resolve/server/settings.py @@ -121,6 +121,7 @@ class LoadMediaModel(BaseSettingsModel): "Set these media pool item metadata values on load and update. The" " keys must match the exact Resolve metadata names like" " 'Clip Name' or 'Shot'" + ) ) @validator("metadata") From dd399586fc7314b3f488bad4420dbec0232be6bb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 12:27:08 +0100 Subject: [PATCH 12/66] Fix default setting --- server_addon/resolve/server/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/resolve/server/settings.py b/server_addon/resolve/server/settings.py index 8fd996d8bd..4d363b1a8f 100644 --- a/server_addon/resolve/server/settings.py +++ b/server_addon/resolve/server/settings.py @@ -180,7 +180,7 @@ DEFAULT_VALUES = { "clip_color_last": "Olive", "clip_color_old": "Orange", "media_pool_bin_path": ( - "Loader/{representation[context][hierarchy]}/{asset[name]}" + "Loader/{folder[path]}" ), "metadata": [ { From 2811eaddb8696ab3816370f9f78699d8dfa9795c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 28 Apr 2024 22:00:21 +0100 Subject: [PATCH 13/66] Working version --- client/ayon_core/hosts/nuke/api/pipeline.py | 6 + .../hosts/nuke/api/push_to_project.py | 116 ++++++++++++++++++ .../tools/push_to_project/control.py | 5 +- .../ayon_core/tools/push_to_project/main.py | 16 ++- .../tools/push_to_project/ui/window.py | 43 +++++-- 5 files changed, 170 insertions(+), 16 deletions(-) create mode 100644 client/ayon_core/hosts/nuke/api/push_to_project.py diff --git a/client/ayon_core/hosts/nuke/api/pipeline.py b/client/ayon_core/hosts/nuke/api/pipeline.py index 0d44aba2f9..23d06c4609 100644 --- a/client/ayon_core/hosts/nuke/api/pipeline.py +++ b/client/ayon_core/hosts/nuke/api/pipeline.py @@ -68,6 +68,7 @@ from .workio import ( current_file ) from .constants import ASSIST +from . import push_to_project log = Logger.get_logger(__name__) @@ -339,6 +340,11 @@ def _install_menu(): lambda: update_placeholder() ) + menu.addCommand( + "Push to Project", + lambda: push_to_project.main() + ) + menu.addSeparator() menu.addCommand( "Experimental tools...", diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py new file mode 100644 index 0000000000..a67a032179 --- /dev/null +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -0,0 +1,116 @@ +from collections import defaultdict +import shutil +import os + +from ayon_api import get_project, get_folder_by_id, get_task_by_id +from ayon_core.settings import get_ayon_settings, get_project_settings +from ayon_core.pipeline import Anatomy, registered_host +from ayon_core.pipeline.template_data import get_template_data +from ayon_core.pipeline.workfile import get_workdir_with_workdir_data +from ayon_core.tools.push_to_project.main import main_show + +from .utils import bake_gizmos_recursively + +import nuke + + +def bake_container(container): + """Bake containers to read nodes.""" + + node = container["node"] + + # Fetch knobs to remove in order. + knobs_to_remove = [] + remove = False + for count in range(0, node.numKnobs()): + knob = node.knob(count) + + # All knobs from "OpenPype" tab knob onwards. + if knob.name() == "AYON": + remove = True + + if remove: + knobs_to_remove.append(knob) + + # Dont remove knobs from "containerId" onwards. + if knob.name() == "containerId": + remove = False + + # Knobs needs to be remove in reverse order, because child knobs needs to + # be remove first. + for knob in reversed(knobs_to_remove): + node.removeKnob(knob) + + node["tile_color"].setValue(0) + + +def main(): + context = main_show("", "", False, True) + + if context is None: + return + + # Get workfile path to save to. + project_name = context["project_name"] + project = get_project(project_name) + folder = get_folder_by_id(project_name, context["folder_id"]) + task = get_task_by_id(project_name, context["task_id"]) + host = registered_host() + ayon_settings = get_ayon_settings() + project_settings = get_project_settings(project_name) + anatomy = Anatomy(project_name) + + workdir_data = get_template_data( + project, folder, task, host.name, ayon_settings + ) + + workdir = get_workdir_with_workdir_data( + workdir_data, + project_name, + anatomy, + project_settings=project_settings + ) + + # Save current workfile. + current_file = host.current_file() + host.save_file(current_file) + + for container in host.ls(): + bake_container(container) + + # Bake gizmos. + bake_gizmos_recursively() + + # Copy all read node files to "resources" folder next to workfile and + # change file path. + first_frame = int(nuke.root()["first_frame"].value()) + last_frame = int(nuke.root()["last_frame"].value()) + files_by_node_name = defaultdict(set) + nodes_by_name = {} + for count in range(first_frame, last_frame + 1): + nuke.frame(count) + for node in nuke.allNodes(filter="Read"): + files_by_node_name[node.name()].add( + nuke.filename(node, nuke.REPLACE) + ) + nodes_by_name[node.name()] = node + + resources_dir = os.path.join(workdir, "resources") + for name, files in files_by_node_name.items(): + dir = os.path.join(resources_dir, name) + if not os.path.exists(dir): + os.makedirs(dir) + + for f in files: + shutil.copy(f, os.path.join(dir, os.path.basename(f))) + + node = nodes_by_name[name] + path = node["file"].value().replace(os.path.dirname(f), dir) + node["file"].setValue(path.replace("\\", "/")) + + # Save current workfile to new context. + basename = os.path.basename(current_file) + host.save_file(os.path.join(workdir, basename)) + + # Open current contex workfile. + host.open_file(current_file) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 58447a8389..96792a2e9b 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -169,13 +169,16 @@ class PushToContextController: return self._integrate_model.get_item_status(item_id) # Processing methods - def submit(self, wait=True): + def submit(self, wait=True, context_only=False): if not self._submission_enabled: return if self._process_thread is not None: return + if context_only: + return + item_id = self._integrate_model.create_process_item( self._src_project_name, self._src_version_id, diff --git a/client/ayon_core/tools/push_to_project/main.py b/client/ayon_core/tools/push_to_project/main.py index a6ff38c16f..d230f6a660 100644 --- a/client/ayon_core/tools/push_to_project/main.py +++ b/client/ayon_core/tools/push_to_project/main.py @@ -4,14 +4,20 @@ from ayon_core.tools.utils import get_ayon_qt_app from ayon_core.tools.push_to_project.ui import PushToContextSelectWindow -def main_show(project_name, version_id): - app = get_ayon_qt_app() - - window = PushToContextSelectWindow() +def main_show(project_name, version_id, library_filter, context_only): + window = PushToContextSelectWindow( + library_filter=library_filter, context_only=context_only + ) window.show() window.set_source(project_name, version_id) - app.exec_() + if __name__ == "__main__": + app = get_ayon_qt_app() + app.exec_() + else: + window.exec_() + + return window.context @click.command() 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 4d64509afd..2a26388221 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -14,12 +14,16 @@ from ayon_core.tools.push_to_project.control import ( ) -class PushToContextSelectWindow(QtWidgets.QWidget): - def __init__(self, controller=None): +class PushToContextSelectWindow(QtWidgets.QDialog): + def __init__( + self, controller=None, library_filter=True, context_only=False + ): super(PushToContextSelectWindow, self).__init__() if controller is None: controller = PushToContextController() self._controller = controller + self.context_only = context_only + self.context = None self.setWindowTitle("Push to project (select context)") self.setWindowIcon(QtGui.QIcon(get_app_icon_path())) @@ -45,7 +49,9 @@ class PushToContextSelectWindow(QtWidgets.QWidget): projects_combobox = ProjectsCombobox(controller, context_widget) projects_combobox.set_select_item_visible(True) - projects_combobox.set_standard_filter_enabled(True) + projects_combobox.set_standard_filter_enabled(False) + if library_filter: + projects_combobox.set_standard_filter_enabled(True) context_splitter = QtWidgets.QSplitter( QtCore.Qt.Vertical, context_widget @@ -89,13 +95,13 @@ class PushToContextSelectWindow(QtWidgets.QWidget): # --- Buttons widget --- btns_widget = QtWidgets.QWidget(self) cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget) - publish_btn = QtWidgets.QPushButton("Publish", btns_widget) + push_btn = QtWidgets.QPushButton("Push", btns_widget) btns_layout = QtWidgets.QHBoxLayout(btns_widget) btns_layout.setContentsMargins(0, 0, 0, 0) btns_layout.addStretch(1) btns_layout.addWidget(cancel_btn, 0) - btns_layout.addWidget(publish_btn, 0) + btns_layout.addWidget(push_btn, 0) sep_1 = SeparatorWidget(parent=main_context_widget) sep_2 = SeparatorWidget(parent=main_context_widget) @@ -160,7 +166,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): variant_input.textChanged.connect(self._on_variant_change) comment_input.textChanged.connect(self._on_comment_change) - publish_btn.clicked.connect(self._on_select_click) + push_btn.clicked.connect(self._on_select_click) cancel_btn.clicked.connect(self._on_close_click) overlay_close_btn.clicked.connect(self._on_close_click) overlay_try_btn.clicked.connect(self._on_try_again_click) @@ -206,7 +212,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._folder_name_input = folder_name_input self._comment_input = comment_input - self._publish_btn = publish_btn + self._push_btn = push_btn self._overlay_widget = overlay_widget self._overlay_close_btn = overlay_close_btn @@ -234,7 +240,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._variant_is_valid = None self._folder_is_valid = None - publish_btn.setEnabled(False) + push_btn.setEnabled(False) overlay_close_btn.setVisible(False) overlay_try_btn.setVisible(False) @@ -372,13 +378,30 @@ class PushToContextSelectWindow(QtWidgets.QWidget): set_style_property(self._variant_input, "state", state) def _on_submission_change(self, event): - self._publish_btn.setEnabled(event["enabled"]) + self._push_btn.setEnabled(event["enabled"]) def _on_close_click(self): self.close() def _on_select_click(self): - self._process_item_id = self._controller.submit(wait=False) + result = self._controller.submit( + wait=True, context_only=self.context_only + ) + + if self.context_only: + user_values = self._controller.get_user_values() + selection_model = self._controller._selection_model + self.context = { + "project_name": selection_model._project_name, + "folder_id": selection_model._folder_id, + "task_id": selection_model._task_id, + "variant": user_values["variant"], + "comment": user_values["comment"], + "folder_name": user_values["new_folder_name"] + } + self.close() + + self._process_item = result def _on_try_again_click(self): self._process_item_id = None From e0ad6af4fb841a5ca923e0fa59295ce4880f48aa Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 30 Apr 2024 16:34:28 +0100 Subject: [PATCH 14/66] Update client/ayon_core/hosts/nuke/api/push_to_project.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/hosts/nuke/api/push_to_project.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py index a67a032179..b58dd99734 100644 --- a/client/ayon_core/hosts/nuke/api/push_to_project.py +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -56,7 +56,6 @@ def main(): folder = get_folder_by_id(project_name, context["folder_id"]) task = get_task_by_id(project_name, context["task_id"]) host = registered_host() - ayon_settings = get_ayon_settings() project_settings = get_project_settings(project_name) anatomy = Anatomy(project_name) From 81a75f7788f3debd781a851a48c23ce539ba1f36 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 30 Apr 2024 16:34:42 +0100 Subject: [PATCH 15/66] Update client/ayon_core/hosts/nuke/api/push_to_project.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/hosts/nuke/api/push_to_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py index b58dd99734..cd7e522d06 100644 --- a/client/ayon_core/hosts/nuke/api/push_to_project.py +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -60,7 +60,7 @@ def main(): anatomy = Anatomy(project_name) workdir_data = get_template_data( - project, folder, task, host.name, ayon_settings + project, folder, task, host.name, project_settings ) workdir = get_workdir_with_workdir_data( From 5f858139a7ffb57f65a93d904fc7c71cfb408094 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 30 Apr 2024 16:34:51 +0100 Subject: [PATCH 16/66] Update client/ayon_core/hosts/nuke/api/push_to_project.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/hosts/nuke/api/push_to_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py index cd7e522d06..75ce08fb71 100644 --- a/client/ayon_core/hosts/nuke/api/push_to_project.py +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -25,7 +25,7 @@ def bake_container(container): for count in range(0, node.numKnobs()): knob = node.knob(count) - # All knobs from "OpenPype" tab knob onwards. + # All knobs from "AYON" tab knob onwards. if knob.name() == "AYON": remove = True From 7411f2181c8ae4ec6e31639eb588e693b3f9554a Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 30 Apr 2024 16:35:53 +0100 Subject: [PATCH 17/66] Update client/ayon_core/hosts/nuke/api/push_to_project.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/hosts/nuke/api/push_to_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py index 75ce08fb71..2f14f4e5f4 100644 --- a/client/ayon_core/hosts/nuke/api/push_to_project.py +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -26,7 +26,7 @@ def bake_container(container): knob = node.knob(count) # All knobs from "AYON" tab knob onwards. - if knob.name() == "AYON": + if knob.name() == MENU_LABEL: remove = True if remove: From 80f340cb6ce2cd3ab1bd51238660c36d6f32db31 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 30 Apr 2024 16:49:13 +0100 Subject: [PATCH 18/66] Separate show method --- .../hosts/nuke/api/push_to_project.py | 7 ++++--- .../ayon_core/tools/push_to_project/main.py | 20 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py index 2f14f4e5f4..b26e7f9aff 100644 --- a/client/ayon_core/hosts/nuke/api/push_to_project.py +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -3,13 +3,14 @@ import shutil import os from ayon_api import get_project, get_folder_by_id, get_task_by_id -from ayon_core.settings import get_ayon_settings, get_project_settings +from ayon_core.settings import get_project_settings from ayon_core.pipeline import Anatomy, registered_host from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.workfile import get_workdir_with_workdir_data -from ayon_core.tools.push_to_project.main import main_show +from ayon_core.tools.push_to_project.main import show from .utils import bake_gizmos_recursively +from .lib import MENU_LABEL import nuke @@ -45,7 +46,7 @@ def bake_container(container): def main(): - context = main_show("", "", False, True) + context = show("", "", False, True) if context is None: return diff --git a/client/ayon_core/tools/push_to_project/main.py b/client/ayon_core/tools/push_to_project/main.py index d230f6a660..bfb921a2b7 100644 --- a/client/ayon_core/tools/push_to_project/main.py +++ b/client/ayon_core/tools/push_to_project/main.py @@ -4,22 +4,26 @@ from ayon_core.tools.utils import get_ayon_qt_app from ayon_core.tools.push_to_project.ui import PushToContextSelectWindow -def main_show(project_name, version_id, library_filter, context_only): +def show(project_name, version_id, library_filter, context_only): window = PushToContextSelectWindow( library_filter=library_filter, context_only=context_only ) window.show() window.set_source(project_name, version_id) - - if __name__ == "__main__": - app = get_ayon_qt_app() - app.exec_() - else: - window.exec_() - + window.exec_() return window.context +def main_show(project_name, version_id): + app = get_ayon_qt_app() + + window = PushToContextSelectWindow() + window.show() + window.set_source(project_name, version_id) + + app.exec_() + + @click.command() @click.option("--project", help="Source project name") @click.option("--version", help="Source version id") From 5564f07a37aba1688b2e7e5b7988ceacfc4314c0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 May 2024 14:09:39 +0100 Subject: [PATCH 19/66] Use context_dialog --- client/ayon_core/hosts/nuke/api/push_to_project.py | 4 ++-- client/ayon_core/tools/context_dialog/__init__.py | 3 ++- client/ayon_core/tools/context_dialog/window.py | 14 +++++++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py index b26e7f9aff..7d6bfa0f32 100644 --- a/client/ayon_core/hosts/nuke/api/push_to_project.py +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -7,7 +7,7 @@ from ayon_core.settings import get_project_settings from ayon_core.pipeline import Anatomy, registered_host from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.workfile import get_workdir_with_workdir_data -from ayon_core.tools.push_to_project.main import show +from ayon_core.tools.context_dialog import show from .utils import bake_gizmos_recursively from .lib import MENU_LABEL @@ -46,7 +46,7 @@ def bake_container(container): def main(): - context = show("", "", False, True) + context = show() if context is None: return diff --git a/client/ayon_core/tools/context_dialog/__init__.py b/client/ayon_core/tools/context_dialog/__init__.py index 4fb912fb62..66920c583c 100644 --- a/client/ayon_core/tools/context_dialog/__init__.py +++ b/client/ayon_core/tools/context_dialog/__init__.py @@ -1,7 +1,8 @@ -from .window import ContextDialog, main +from .window import ContextDialog, main, show __all__ = ( "ContextDialog", "main", + "show" ) diff --git a/client/ayon_core/tools/context_dialog/window.py b/client/ayon_core/tools/context_dialog/window.py index 828d771142..f235aa85d8 100644 --- a/client/ayon_core/tools/context_dialog/window.py +++ b/client/ayon_core/tools/context_dialog/window.py @@ -343,7 +343,7 @@ class ContextDialogController: def store_output(self): if not self._output_path: - return + return self.get_selected_context() dirpath = os.path.dirname(self._output_path) os.makedirs(dirpath, exist_ok=True) @@ -791,3 +791,15 @@ def main( window.show() app.exec_() controller.store_output() + + +def show( + strict=True +): + controller = ContextDialogController() + controller.set_strict(strict) + window = ContextDialog(controller=controller) + window.show() + window.exec_() + + return controller.store_output() From 7dd6f28e8dda2e41b4443577e1456aa415cf160a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 May 2024 14:12:39 +0100 Subject: [PATCH 20/66] Revert changes to push_to_project --- .../tools/push_to_project/control.py | 5 +-- .../ayon_core/tools/push_to_project/main.py | 10 ----- .../tools/push_to_project/ui/window.py | 43 +++++-------------- 3 files changed, 11 insertions(+), 47 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 96792a2e9b..58447a8389 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -169,16 +169,13 @@ class PushToContextController: return self._integrate_model.get_item_status(item_id) # Processing methods - def submit(self, wait=True, context_only=False): + def submit(self, wait=True): if not self._submission_enabled: return if self._process_thread is not None: return - if context_only: - return - item_id = self._integrate_model.create_process_item( self._src_project_name, self._src_version_id, diff --git a/client/ayon_core/tools/push_to_project/main.py b/client/ayon_core/tools/push_to_project/main.py index bfb921a2b7..a6ff38c16f 100644 --- a/client/ayon_core/tools/push_to_project/main.py +++ b/client/ayon_core/tools/push_to_project/main.py @@ -4,16 +4,6 @@ from ayon_core.tools.utils import get_ayon_qt_app from ayon_core.tools.push_to_project.ui import PushToContextSelectWindow -def show(project_name, version_id, library_filter, context_only): - window = PushToContextSelectWindow( - library_filter=library_filter, context_only=context_only - ) - window.show() - window.set_source(project_name, version_id) - window.exec_() - return window.context - - def main_show(project_name, version_id): app = get_ayon_qt_app() 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 2a26388221..4d64509afd 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -14,16 +14,12 @@ from ayon_core.tools.push_to_project.control import ( ) -class PushToContextSelectWindow(QtWidgets.QDialog): - def __init__( - self, controller=None, library_filter=True, context_only=False - ): +class PushToContextSelectWindow(QtWidgets.QWidget): + def __init__(self, controller=None): super(PushToContextSelectWindow, self).__init__() if controller is None: controller = PushToContextController() self._controller = controller - self.context_only = context_only - self.context = None self.setWindowTitle("Push to project (select context)") self.setWindowIcon(QtGui.QIcon(get_app_icon_path())) @@ -49,9 +45,7 @@ class PushToContextSelectWindow(QtWidgets.QDialog): projects_combobox = ProjectsCombobox(controller, context_widget) projects_combobox.set_select_item_visible(True) - projects_combobox.set_standard_filter_enabled(False) - if library_filter: - projects_combobox.set_standard_filter_enabled(True) + projects_combobox.set_standard_filter_enabled(True) context_splitter = QtWidgets.QSplitter( QtCore.Qt.Vertical, context_widget @@ -95,13 +89,13 @@ class PushToContextSelectWindow(QtWidgets.QDialog): # --- Buttons widget --- btns_widget = QtWidgets.QWidget(self) cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget) - push_btn = QtWidgets.QPushButton("Push", btns_widget) + publish_btn = QtWidgets.QPushButton("Publish", btns_widget) btns_layout = QtWidgets.QHBoxLayout(btns_widget) btns_layout.setContentsMargins(0, 0, 0, 0) btns_layout.addStretch(1) btns_layout.addWidget(cancel_btn, 0) - btns_layout.addWidget(push_btn, 0) + btns_layout.addWidget(publish_btn, 0) sep_1 = SeparatorWidget(parent=main_context_widget) sep_2 = SeparatorWidget(parent=main_context_widget) @@ -166,7 +160,7 @@ class PushToContextSelectWindow(QtWidgets.QDialog): variant_input.textChanged.connect(self._on_variant_change) comment_input.textChanged.connect(self._on_comment_change) - push_btn.clicked.connect(self._on_select_click) + publish_btn.clicked.connect(self._on_select_click) cancel_btn.clicked.connect(self._on_close_click) overlay_close_btn.clicked.connect(self._on_close_click) overlay_try_btn.clicked.connect(self._on_try_again_click) @@ -212,7 +206,7 @@ class PushToContextSelectWindow(QtWidgets.QDialog): self._folder_name_input = folder_name_input self._comment_input = comment_input - self._push_btn = push_btn + self._publish_btn = publish_btn self._overlay_widget = overlay_widget self._overlay_close_btn = overlay_close_btn @@ -240,7 +234,7 @@ class PushToContextSelectWindow(QtWidgets.QDialog): self._variant_is_valid = None self._folder_is_valid = None - push_btn.setEnabled(False) + publish_btn.setEnabled(False) overlay_close_btn.setVisible(False) overlay_try_btn.setVisible(False) @@ -378,30 +372,13 @@ class PushToContextSelectWindow(QtWidgets.QDialog): set_style_property(self._variant_input, "state", state) def _on_submission_change(self, event): - self._push_btn.setEnabled(event["enabled"]) + self._publish_btn.setEnabled(event["enabled"]) def _on_close_click(self): self.close() def _on_select_click(self): - result = self._controller.submit( - wait=True, context_only=self.context_only - ) - - if self.context_only: - user_values = self._controller.get_user_values() - selection_model = self._controller._selection_model - self.context = { - "project_name": selection_model._project_name, - "folder_id": selection_model._folder_id, - "task_id": selection_model._task_id, - "variant": user_values["variant"], - "comment": user_values["comment"], - "folder_name": user_values["new_folder_name"] - } - self.close() - - self._process_item = result + self._process_item_id = self._controller.submit(wait=False) def _on_try_again_click(self): self._process_item_id = None From 2082214b356671d9a28854ad7ee2a32e2509d1db Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 May 2024 21:11:29 +0100 Subject: [PATCH 21/66] BigRoy feedback --- client/ayon_core/hosts/nuke/api/push_to_project.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py index 7d6bfa0f32..fde26bfe81 100644 --- a/client/ayon_core/hosts/nuke/api/push_to_project.py +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -7,7 +7,7 @@ from ayon_core.settings import get_project_settings from ayon_core.pipeline import Anatomy, registered_host from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.workfile import get_workdir_with_workdir_data -from ayon_core.tools.context_dialog import show +from ayon_core.tools import context_dialog from .utils import bake_gizmos_recursively from .lib import MENU_LABEL @@ -46,7 +46,7 @@ def bake_container(container): def main(): - context = show() + context = context_dialog.show() if context is None: return From 92f9fda546c09f097479bc3fc9db1fd642ced0e5 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 23 May 2024 23:02:28 +0100 Subject: [PATCH 22/66] Update client/ayon_core/tools/context_dialog/window.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/context_dialog/window.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/context_dialog/window.py b/client/ayon_core/tools/context_dialog/window.py index f235aa85d8..532bfe878e 100644 --- a/client/ayon_core/tools/context_dialog/window.py +++ b/client/ayon_core/tools/context_dialog/window.py @@ -793,13 +793,10 @@ def main( controller.store_output() -def show( - strict=True -): +def ask_for_context(strict=True): controller = ContextDialogController() controller.set_strict(strict) window = ContextDialog(controller=controller) - window.show() window.exec_() - return controller.store_output() + return controller.get_selected_context() From f73d0a544d746ad1dd8f252c11720404912eb271 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 23 May 2024 23:02:34 +0100 Subject: [PATCH 23/66] Update client/ayon_core/tools/context_dialog/window.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/context_dialog/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/context_dialog/window.py b/client/ayon_core/tools/context_dialog/window.py index 532bfe878e..ea5fdfbaec 100644 --- a/client/ayon_core/tools/context_dialog/window.py +++ b/client/ayon_core/tools/context_dialog/window.py @@ -343,7 +343,7 @@ class ContextDialogController: def store_output(self): if not self._output_path: - return self.get_selected_context() + return dirpath = os.path.dirname(self._output_path) os.makedirs(dirpath, exist_ok=True) From 71471cf72592d8a155a2384ff07a84c8a182df54 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 May 2024 23:07:02 +0100 Subject: [PATCH 24/66] Fixes for ask_for_context --- client/ayon_core/hosts/nuke/api/push_to_project.py | 2 +- client/ayon_core/tools/context_dialog/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/nuke/api/push_to_project.py b/client/ayon_core/hosts/nuke/api/push_to_project.py index fde26bfe81..f145ed652b 100644 --- a/client/ayon_core/hosts/nuke/api/push_to_project.py +++ b/client/ayon_core/hosts/nuke/api/push_to_project.py @@ -46,7 +46,7 @@ def bake_container(container): def main(): - context = context_dialog.show() + context = context_dialog.ask_for_context() if context is None: return diff --git a/client/ayon_core/tools/context_dialog/__init__.py b/client/ayon_core/tools/context_dialog/__init__.py index 66920c583c..8a77a46109 100644 --- a/client/ayon_core/tools/context_dialog/__init__.py +++ b/client/ayon_core/tools/context_dialog/__init__.py @@ -1,8 +1,8 @@ -from .window import ContextDialog, main, show +from .window import ContextDialog, main, ask_for_context __all__ = ( "ContextDialog", "main", - "show" + "ask_for_context" ) From 5bffaa730efce006283b66462d52a543713fcce0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Jun 2024 17:27:25 +0800 Subject: [PATCH 25/66] fix the incorrect fps value when loading point cache --- server_addon/max/client/ayon_max/api/lib.py | 17 ++++++++++++----- .../ayon_max/plugins/load/load_pointcache.py | 4 +++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/server_addon/max/client/ayon_max/api/lib.py b/server_addon/max/client/ayon_max/api/lib.py index eb22dbafd2..8fce21311a 100644 --- a/server_addon/max/client/ayon_max/api/lib.py +++ b/server_addon/max/client/ayon_max/api/lib.py @@ -272,10 +272,8 @@ def reset_frame_range(fps: bool = True): scene frame rate in frames-per-second. """ if fps: - task_entity = get_current_task_entity() - task_attributes = task_entity["attrib"] - fps_number = float(task_attributes["fps"]) - rt.frameRate = fps_number + set_fps() + frame_range = get_frame_range() set_timeline( @@ -284,6 +282,15 @@ def reset_frame_range(fps: bool = True): frame_range["frameStartHandle"], frame_range["frameEndHandle"]) +def set_fps(): + """Set fps to current folder + """ + task_entity = get_current_task_entity() + task_attributes = task_entity["attrib"] + fps_number = float(task_attributes["fps"]) + rt.frameRate = fps_number + + def reset_unit_scale(): """Apply the unit scale setting to 3dsMax """ @@ -358,7 +365,7 @@ def is_headless(): def set_timeline(frameStart, frameEnd): """Set frame range for timeline editor in Max """ - rt.animationRange = rt.interval(frameStart, frameEnd) + rt.animationRange = rt.interval(int(frameStart), int(frameEnd)) return rt.animationRange diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index d7def3d0ba..7f2d4c05f8 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -7,7 +7,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. import os from ayon_core.pipeline import load, get_representation_path from ayon_max.api import lib, maintained_selection -from ayon_max.api.lib import unique_namespace +from ayon_max.api.lib import unique_namespace, set_fps from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, @@ -31,6 +31,8 @@ class AbcLoader(load.LoaderPlugin): file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) + set_fps() + abc_before = { c for c in rt.rootNode.Children From f861dfa73d8a17c33e82c263f01341e410401b44 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Jun 2024 12:47:49 +0200 Subject: [PATCH 26/66] Fix refactor of `remove_unused_media_pool_items` location --- .../plugins/inventory/remove_unused_media_pool_items.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {client/ayon_core/hosts/resolve => server_addon/resolve/client/ayon_resolve}/plugins/inventory/remove_unused_media_pool_items.py (100%) diff --git a/client/ayon_core/hosts/resolve/plugins/inventory/remove_unused_media_pool_items.py b/server_addon/resolve/client/ayon_resolve/plugins/inventory/remove_unused_media_pool_items.py similarity index 100% rename from client/ayon_core/hosts/resolve/plugins/inventory/remove_unused_media_pool_items.py rename to server_addon/resolve/client/ayon_resolve/plugins/inventory/remove_unused_media_pool_items.py From d39acab7158387fcd42499c921ab92fd6fff9221 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Jun 2024 12:51:39 +0200 Subject: [PATCH 27/66] Fix usage of undefined variable --- .../resolve/client/ayon_resolve/plugins/load/load_media.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py index c1016ce053..d5dbfa8934 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py @@ -280,7 +280,10 @@ class LoadMedia(LoaderPlugin): item.SetMetadata(lib.pype_tag_name, json.dumps(data)) self._set_metadata(media_pool_item=item, context=context) - self._set_colorspace_from_representation(item, representation) + self._set_colorspace_from_representation( + item, + representation=context["representation"] + ) # If no specific colorspace is set then we want to preserve the # colorspace a user might have set before the clip replacement From 14781e795c317f534b29c384663a446a854c8b10 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Jun 2024 12:51:53 +0200 Subject: [PATCH 28/66] Cosmetics --- .../resolve/client/ayon_resolve/plugins/load/load_media.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py index d5dbfa8934..114e8dca79 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py @@ -62,15 +62,18 @@ def set_colorspace(media_pool_item, colorspace, mode="davinciYRGBColorManagedv2"): """Set MediaPoolItem colorspace. + This implements a workaround that you cannot set the input colorspace unless the Resolve project's color science mode is set to `davinciYRGBColorManagedv2`. + Args: media_pool_item (MediaPoolItem): The media pool item. colorspace (str): The colorspace to apply. mode (Optional[str]): The Resolve project color science mode to be in while setting the colorspace. Defaults to 'davinciYRGBColorManagedv2' + Returns: bool: Whether applying the colorspace succeeded. """ From 7a72ecb7fbbe024dca908bb1cfec7284964133ea Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Jun 2024 12:53:57 +0200 Subject: [PATCH 29/66] Fix refactor of imports --- .../resolve/client/ayon_resolve/plugins/load/load_media.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py index 114e8dca79..6d42061eb0 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py @@ -12,13 +12,13 @@ from ayon_core.pipeline import ( get_representation_path, registered_host ) -from ayon_core.hosts.resolve.api import lib -from ayon_core.hosts.resolve.api.pipeline import AVALON_CONTAINER_ID from ayon_core.lib.transcoding import ( VIDEO_EXTENSIONS, IMAGE_EXTENSIONS ) from ayon_core.lib import BoolDef +from ayon_resolve.api import lib +from ayon_resolve.api.pipeline import AVALON_CONTAINER_ID class MetadataEntry(TypedDict): From a51b2b4e3e17bc80c4f66e8177f24b158841a140 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Jun 2024 12:56:13 +0200 Subject: [PATCH 30/66] Add selected to label --- .../plugins/inventory/remove_unused_media_pool_items.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/resolve/client/ayon_resolve/plugins/inventory/remove_unused_media_pool_items.py b/server_addon/resolve/client/ayon_resolve/plugins/inventory/remove_unused_media_pool_items.py index 6698508d24..7ea55dc1ff 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/inventory/remove_unused_media_pool_items.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/inventory/remove_unused_media_pool_items.py @@ -6,7 +6,7 @@ from ayon_core.pipeline.load.utils import remove_container class RemoveUnusedMedia(InventoryAction): - label = "Remove Unused Media" + label = "Remove Unused Selected Media" icon = "trash" @staticmethod From e3f8ff9de35ad5ec37b6981c2f7fd3710acd6356 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Jun 2024 19:58:08 +0800 Subject: [PATCH 31/66] set scene fps during loading point cache context --- server_addon/max/client/ayon_max/api/lib.py | 55 ++++++++++-- .../ayon_max/plugins/load/load_pointcache.py | 85 ++++++++++--------- 2 files changed, 92 insertions(+), 48 deletions(-) diff --git a/server_addon/max/client/ayon_max/api/lib.py b/server_addon/max/client/ayon_max/api/lib.py index 8fce21311a..eb29af5ec5 100644 --- a/server_addon/max/client/ayon_max/api/lib.py +++ b/server_addon/max/client/ayon_max/api/lib.py @@ -7,8 +7,11 @@ from typing import Any, Dict, Union import six +import ayon_api + from ayon_core.pipeline import ( get_current_project_name, + get_current_folder_path, colorspace ) from ayon_core.settings import get_project_settings @@ -272,7 +275,7 @@ def reset_frame_range(fps: bool = True): scene frame rate in frames-per-second. """ if fps: - set_fps() + rt.frameRate = float(get_fps_for_current_context()) frame_range = get_frame_range() @@ -282,13 +285,37 @@ def reset_frame_range(fps: bool = True): frame_range["frameStartHandle"], frame_range["frameEndHandle"]) -def set_fps(): - """Set fps to current folder +def get_fps_for_current_context(): + """Get fps that should be set for current context. + + Todos: + - Skip project value. + - Merge logic with 'get_frame_range' and 'reset_scene_resolution' -> + all the values in the functions can be collected at one place as + they have same requirements. + + Returns: + Union[int, float]: FPS value. """ - task_entity = get_current_task_entity() - task_attributes = task_entity["attrib"] - fps_number = float(task_attributes["fps"]) - rt.frameRate = fps_number + task_entity = get_current_task_entity(fields={"attrib"}) + fps = task_entity.get("attrib", {}).get("fps") + if not fps: + project_name = get_current_project_name() + folder_path = get_current_folder_path() + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path, fields={"attrib.fps"} + ) or {} + + fps = folder_entity.get("attrib", {}).get("fps") + if not fps: + project_entity = ayon_api.get_project( + project_name, fields=["attrib.fps"] + ) or {} + fps = project_entity.get("attrib", {}).get("fps") + + if not fps: + fps = 25 + return fps def reset_unit_scale(): @@ -580,3 +607,17 @@ def suspended_refresh(): finally: rt.enableSceneRedraw() rt.resumeEditing() + + +@contextlib.contextmanager +def scene_fps(fps): + """Set scene fps during context + + Args: + fps (float): fps value + """ + try: + rt.frameRate = float(get_fps_for_current_context()) + yield + finally: + rt.frameRate = fps diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index 7f2d4c05f8..f862f2ed67 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -7,7 +7,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. import os from ayon_core.pipeline import load, get_representation_path from ayon_max.api import lib, maintained_selection -from ayon_max.api.lib import unique_namespace, set_fps +from ayon_max.api.lib import unique_namespace, scene_fps from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, @@ -31,47 +31,50 @@ class AbcLoader(load.LoaderPlugin): file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) - set_fps() - - abc_before = { - c - for c in rt.rootNode.Children - if rt.classOf(c) == rt.AlembicContainer - } - - rt.AlembicImport.ImportToRoot = False - rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport) - - abc_after = { - c - for c in rt.rootNode.Children - if rt.classOf(c) == rt.AlembicContainer - } - - # This should yield new AlembicContainer node - abc_containers = abc_after.difference(abc_before) - - if len(abc_containers) != 1: - self.log.error("Something failed when loading.") - - abc_container = abc_containers.pop() - selections = rt.GetCurrentSelection() - for abc in selections: - for cam_shape in abc.Children: - cam_shape.playbackType = 0 - - namespace = unique_namespace( - name + "_", - suffix="_", - ) abc_objects = [] - for abc_object in abc_container.Children: - abc_object.name = f"{namespace}:{abc_object.name}" - abc_objects.append(abc_object) - # rename the abc container with namespace - abc_container_name = f"{namespace}:{name}" - abc_container.name = abc_container_name - abc_objects.append(abc_container) + product_fps = float(context["version"]["attrib"].get("fps")) + if product_fps is None: + # Just stick to current scene FPS + product_fps = float(rt.frameRate) + with scene_fps(product_fps): + abc_before = { + c + for c in rt.rootNode.Children + if rt.classOf(c) == rt.AlembicContainer + } + + rt.AlembicImport.ImportToRoot = False + rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport) + + abc_after = { + c + for c in rt.rootNode.Children + if rt.classOf(c) == rt.AlembicContainer + } + + # This should yield new AlembicContainer node + abc_containers = abc_after.difference(abc_before) + + if len(abc_containers) != 1: + self.log.error("Something failed when loading.") + + abc_container = abc_containers.pop() + selections = rt.GetCurrentSelection() + for abc in selections: + for cam_shape in abc.Children: + cam_shape.playbackType = 0 + + namespace = unique_namespace( + name + "_", + suffix="_", + ) + for abc_object in abc_container.Children: + abc_object.name = f"{namespace}:{abc_object.name}" + abc_objects.append(abc_object) + # rename the abc container with namespace + abc_container_name = f"{namespace}:{name}" + abc_container.name = abc_container_name + abc_objects.append(abc_container) return containerise( name, abc_objects, context, From a0ba192306429ce64921869c1161ce8593be536c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 11 Jun 2024 14:05:07 +0200 Subject: [PATCH 32/66] Rename resolve paths and update imports for media loading - Renamed resolve paths for media pool items - Updated imports for media loading functionality --- .../plugins/inventory/remove_unused_media_pool_items.py | 0 .../resolve/client/ayon_resolve/plugins/load/load_media.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename {client/ayon_core/hosts/resolve => server_addon/resolve/client/ayon_resolve}/plugins/inventory/remove_unused_media_pool_items.py (100%) diff --git a/client/ayon_core/hosts/resolve/plugins/inventory/remove_unused_media_pool_items.py b/server_addon/resolve/client/ayon_resolve/plugins/inventory/remove_unused_media_pool_items.py similarity index 100% rename from client/ayon_core/hosts/resolve/plugins/inventory/remove_unused_media_pool_items.py rename to server_addon/resolve/client/ayon_resolve/plugins/inventory/remove_unused_media_pool_items.py diff --git a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py index c1016ce053..dea0431747 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py @@ -12,8 +12,8 @@ from ayon_core.pipeline import ( get_representation_path, registered_host ) -from ayon_core.hosts.resolve.api import lib -from ayon_core.hosts.resolve.api.pipeline import AVALON_CONTAINER_ID +from ayon_resolve.api import lib +from ayon_resolve.api.pipeline import AVALON_CONTAINER_ID from ayon_core.lib.transcoding import ( VIDEO_EXTENSIONS, IMAGE_EXTENSIONS From d7aa02c3e681474d16a1f245a0116c7a83706042 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Jun 2024 20:11:06 +0800 Subject: [PATCH 33/66] code tweaks - big roy's comment --- .../ayon_max/plugins/load/load_pointcache.py | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index f862f2ed67..498c5c6ce7 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -31,50 +31,52 @@ class AbcLoader(load.LoaderPlugin): file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) - abc_objects = [] + abc_before = { + c + for c in rt.rootNode.Children + if rt.classOf(c) == rt.AlembicContainer + } + + rt.AlembicImport.ImportToRoot = False product_fps = float(context["version"]["attrib"].get("fps")) if product_fps is None: # Just stick to current scene FPS product_fps = float(rt.frameRate) with scene_fps(product_fps): - abc_before = { - c - for c in rt.rootNode.Children - if rt.classOf(c) == rt.AlembicContainer - } + rt.importFile(file_path, + rt.name("noPrompt"), + using=rt.AlembicImport) - rt.AlembicImport.ImportToRoot = False - rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport) + abc_after = { + c + for c in rt.rootNode.Children + if rt.classOf(c) == rt.AlembicContainer + } - abc_after = { - c - for c in rt.rootNode.Children - if rt.classOf(c) == rt.AlembicContainer - } + # This should yield new AlembicContainer node + abc_containers = abc_after.difference(abc_before) - # This should yield new AlembicContainer node - abc_containers = abc_after.difference(abc_before) + if len(abc_containers) != 1: + self.log.error("Something failed when loading.") - if len(abc_containers) != 1: - self.log.error("Something failed when loading.") + abc_container = abc_containers.pop() + selections = rt.GetCurrentSelection() + for abc in selections: + for cam_shape in abc.Children: + cam_shape.playbackType = 0 - abc_container = abc_containers.pop() - selections = rt.GetCurrentSelection() - for abc in selections: - for cam_shape in abc.Children: - cam_shape.playbackType = 0 - - namespace = unique_namespace( - name + "_", - suffix="_", - ) - for abc_object in abc_container.Children: - abc_object.name = f"{namespace}:{abc_object.name}" - abc_objects.append(abc_object) - # rename the abc container with namespace - abc_container_name = f"{namespace}:{name}" - abc_container.name = abc_container_name - abc_objects.append(abc_container) + namespace = unique_namespace( + name + "_", + suffix="_", + ) + abc_objects = [] + for abc_object in abc_container.Children: + abc_object.name = f"{namespace}:{abc_object.name}" + abc_objects.append(abc_object) + # rename the abc container with namespace + abc_container_name = f"{namespace}:{name}" + abc_container.name = abc_container_name + abc_objects.append(abc_container) return containerise( name, abc_objects, context, From ee5d82994db8eca9c67a9b2923ebb28bf41831b8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 11 Jun 2024 14:11:28 +0200 Subject: [PATCH 34/66] Remove subproject integration for Unreal Engine. Deleted unused subproject integration for Unreal Engine. --- client/ayon_core/hosts/unreal/integration | 1 - 1 file changed, 1 deletion(-) delete mode 160000 client/ayon_core/hosts/unreal/integration diff --git a/client/ayon_core/hosts/unreal/integration b/client/ayon_core/hosts/unreal/integration deleted file mode 160000 index 04b35dbf5f..0000000000 --- a/client/ayon_core/hosts/unreal/integration +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 04b35dbf5fc42d905281fc30d3a22b139c1855e5 From 253ae11736fe75d5ae4ccb01a86605a35db2cea7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Jun 2024 20:31:44 +0800 Subject: [PATCH 35/66] code tweaks - big roy's comment --- server_addon/max/client/ayon_max/api/lib.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server_addon/max/client/ayon_max/api/lib.py b/server_addon/max/client/ayon_max/api/lib.py index eb29af5ec5..03310ff16f 100644 --- a/server_addon/max/client/ayon_max/api/lib.py +++ b/server_addon/max/client/ayon_max/api/lib.py @@ -616,8 +616,9 @@ def scene_fps(fps): Args: fps (float): fps value """ + fps_before = rt.frameRate try: - rt.frameRate = float(get_fps_for_current_context()) + rt.frameRate = float(fps) yield finally: - rt.frameRate = fps + rt.frameRate = fps_before From 9e4d1f19ebebcdc35f7e7ae1c1100c281e2042c4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 11 Jun 2024 14:32:25 +0200 Subject: [PATCH 36/66] Update version to 0.2.2 in package.py Bumped up the version number from 0.2.1 to 0.2.2 in the package.py file for DaVinci Resolve addon. --- server_addon/resolve/package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/resolve/package.py b/server_addon/resolve/package.py index 47b6c9a8b6..643e497253 100644 --- a/server_addon/resolve/package.py +++ b/server_addon/resolve/package.py @@ -1,6 +1,6 @@ name = "resolve" title = "DaVinci Resolve" -version = "0.2.1" +version = "0.2.2" client_dir = "ayon_resolve" From 92d49139a5c565ffa1f17314ff13ab0e5fbf7405 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Jun 2024 20:51:13 +0800 Subject: [PATCH 37/66] make sure the timeline and custom frame range matched to the export --- server_addon/max/client/ayon_max/api/lib.py | 15 --------------- .../ayon_max/plugins/load/load_pointcache.py | 9 ++++----- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/server_addon/max/client/ayon_max/api/lib.py b/server_addon/max/client/ayon_max/api/lib.py index 03310ff16f..7c140be72e 100644 --- a/server_addon/max/client/ayon_max/api/lib.py +++ b/server_addon/max/client/ayon_max/api/lib.py @@ -607,18 +607,3 @@ def suspended_refresh(): finally: rt.enableSceneRedraw() rt.resumeEditing() - - -@contextlib.contextmanager -def scene_fps(fps): - """Set scene fps during context - - Args: - fps (float): fps value - """ - fps_before = rt.frameRate - try: - rt.frameRate = float(fps) - yield - finally: - rt.frameRate = fps_before diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index 498c5c6ce7..3eb4ba3323 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -7,7 +7,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. import os from ayon_core.pipeline import load, get_representation_path from ayon_max.api import lib, maintained_selection -from ayon_max.api.lib import unique_namespace, scene_fps +from ayon_max.api.lib import unique_namespace from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, @@ -42,10 +42,9 @@ class AbcLoader(load.LoaderPlugin): if product_fps is None: # Just stick to current scene FPS product_fps = float(rt.frameRate) - with scene_fps(product_fps): - rt.importFile(file_path, - rt.name("noPrompt"), - using=rt.AlembicImport) + # TODO: remove after the post-system fps setup + rt.frameRange = product_fps + rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport) abc_after = { c From 21c86742d4de1abd928d373f3634b58cdf1fae98 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 11 Jun 2024 14:55:00 +0200 Subject: [PATCH 38/66] Bump addon version to 0.2.2 Update the addon version from 0.2.1 to 0.2.2 in the codebase. --- server_addon/resolve/client/ayon_resolve/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/resolve/client/ayon_resolve/version.py b/server_addon/resolve/client/ayon_resolve/version.py index 53e8882ed7..585f44b5a5 100644 --- a/server_addon/resolve/client/ayon_resolve/version.py +++ b/server_addon/resolve/client/ayon_resolve/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'resolve' version.""" -__version__ = "0.2.1" +__version__ = "0.2.2" From 7b1c4a8a080114e299dfe26ff52ccf63776edfcb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 12 Jun 2024 11:17:29 +0200 Subject: [PATCH 39/66] Refactor Resolve media loading logic for single files and sequences. - Refactored file path handling for importing media. - Updated method to determine if the file is a sequence. - Improved handling of frame numbers in file paths. --- .../ayon_resolve/plugins/load/load_media.py | 103 +++++++++++++----- 1 file changed, 75 insertions(+), 28 deletions(-) diff --git a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py index 6d42061eb0..c6f8b97ada 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py @@ -1,17 +1,19 @@ import json -import copy import contextlib +from pathlib import Path from collections import defaultdict -from typing import Union, List, Optional, TypedDict +from typing import Union, List, Optional, TypedDict, Tuple from ayon_api import version_is_latest from ayon_core.lib import StringTemplate from ayon_core.pipeline.colorspace import get_remapped_colorspace_to_native from ayon_core.pipeline import ( + Anatomy, LoaderPlugin, get_representation_path, registered_host ) +from ayon_core.pipeline.load import get_representation_path_with_anatomy from ayon_core.lib.transcoding import ( VIDEO_EXTENSIONS, IMAGE_EXTENSIONS @@ -21,6 +23,9 @@ from ayon_resolve.api import lib from ayon_resolve.api.pipeline import AVALON_CONTAINER_ID +FRAME_SPLITTER = "__frame_splitter__" + + class MetadataEntry(TypedDict): """Metadata entry is dict with {"name": "key", "value: "value"}""" name: str @@ -188,6 +193,7 @@ class LoadMedia(LoaderPlugin): self.timeline = lib.get_current_timeline() representation = context["representation"] + self._project_name = context["project"]["name"] project = lib.get_current_project() media_pool = project.GetMediaPool() @@ -221,10 +227,17 @@ class LoadMedia(LoaderPlugin): media_pool.SetCurrentFolder(folder) # Import media - path = self._get_filepath(context) - items = media_pool.ImportMedia([path]) + # Resolve API: ImportMedia function requires a list of dictionaries + # with keys "FilePath", "StartIndex" and "EndIndex" for sequences + # but only string with absolute path for single files. + is_sequence, file_info = self._get_file_info(context) + if is_sequence: + items = media_pool.ImportMedia([file_info]) + else: + items = media_pool.ImportMedia([file_info["FilePath"]]) assert len(items) == 1, "Must import only one media item" + item = items[0] self._set_metadata(item, context) @@ -384,40 +397,74 @@ class LoadMedia(LoaderPlugin): value_formatted = StringTemplate(value).format_strict(context) media_pool_item.SetClipProperty(clip_property, value_formatted) - def _get_filepath(self, context: dict) -> Union[str, dict]: + def _get_file_info(self, context: dict) -> Tuple[bool, Union[str, dict]]: + """Return file info for Resolve ImportMedia. + + Args: + context (dict): The context dictionary. + + Returns: + Tuple[bool, Union[str, dict]]: A tuple of whether the file is a + sequence and the file info dictionary. + """ representation = context["representation"] - is_sequence = bool(representation["context"].get("frame")) - if not is_sequence: - return get_representation_path(representation) + anatomy = Anatomy(self._project_name) - version = context["version"] + # Get path to representation with correct frame number + repre_path = get_representation_path_with_anatomy( + representation, anatomy) - # Get the start and end frame of the image sequence, incl. handles - frame_start = version["data"].get("frameStart", 0) - frame_end = version["data"].get("frameEnd", 0) - handle_start = version["data"].get("handleStart", 0) - handle_end = version["data"].get("handleEnd", 0) - frame_start_handle = frame_start - handle_start - frame_end_handle = frame_end + handle_end - padding = len(representation["context"].get("frame")) + first_frame = representation["context"].get("frame") - # We format the frame number to the required token. To do so - # we in-place change the representation context data to format the path - # with that replaced data - representation = copy.deepcopy(representation) - representation["context"]["frame"] = f"%0{padding}d" - path = get_representation_path(representation) + is_sequence = False + # is not sequence + if first_frame is None: + return ( + is_sequence, {"FilePath": repre_path} + ) + + # This is sequence + is_sequence = True + repre_files = [ + file["path"].format(root=anatomy.roots) + for file in representation["files"] + ] + + # Change frame in representation context to get path with frame + # splitter. + representation["context"]["frame"] = FRAME_SPLITTER + frame_repre_path = get_representation_path_with_anatomy( + representation, anatomy + ) + frame_repre_path = Path(frame_repre_path) + repre_dir, repre_filename = ( + frame_repre_path.parent, frame_repre_path.name) + # Get sequence prefix and suffix + file_prefix, file_suffix = repre_filename.split(FRAME_SPLITTER) + # Get frame number from path as string to get frame padding + frame_str = str(repre_path)[len(file_prefix):][:len(file_suffix)] + frame_padding = len(frame_str) + + file_name = f"{file_prefix}%0{frame_padding}d{file_suffix}" + + abs_filepath = Path(repre_dir, file_name) + + start_index = int(first_frame) + end_index = int(int(first_frame) + len(repre_files) - 1) # See Resolve API, to import for example clip "file_[001-100].dpx": # ImportMedia([{"FilePath":"file_%03d.dpx", # "StartIndex":1, # "EndIndex":100}]) - return { - "FilePath": path, - "StartIndex": frame_start_handle, - "EndIndex": frame_end_handle - } + return ( + is_sequence, + { + "FilePath": abs_filepath.as_posix(), + "StartIndex": start_index, + "EndIndex": end_index, + } + ) def _get_colorspace(self, representation: dict) -> Optional[str]: """Return Resolve native colorspace from OCIO colorspace data. From 31b614d71d7533f5dacbc7b3001cc01474ee17bd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:20:36 +0200 Subject: [PATCH 40/66] fixed implementation of users model --- client/ayon_core/tools/common_models/users.py | 108 +++++++++++++++--- client/ayon_core/tools/workfiles/control.py | 3 +- 2 files changed, 96 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/tools/common_models/users.py b/client/ayon_core/tools/common_models/users.py index f8beb31aa1..babb690f87 100644 --- a/client/ayon_core/tools/common_models/users.py +++ b/client/ayon_core/tools/common_models/users.py @@ -1,6 +1,80 @@ -import ayon_api +import json +import collections -from ayon_core.lib import CacheItem +import ayon_api +from ayon_api.graphql import FIELD_VALUE, GraphQlQuery, fields_to_dict + +from ayon_core.lib import NestedCacheItem + + +# --- Implementation that should be in ayon-python-api --- +# The implementation is not available in all versions of ayon-python-api. +def users_graphql_query(fields): + query = GraphQlQuery("Users") + names_var = query.add_variable("userNames", "[String!]") + project_name_var = query.add_variable("projectName", "String!") + + users_field = query.add_field_with_edges("users") + users_field.set_filter("names", names_var) + users_field.set_filter("projectName", project_name_var) + + nested_fields = fields_to_dict(set(fields)) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, users_field)) + + while query_queue: + item = query_queue.popleft() + key, value, parent = item + field = parent.add_field(key) + if value is FIELD_VALUE: + continue + + for k, v in value.items(): + query_queue.append((k, v, field)) + return query + + +def get_users(project_name=None, usernames=None, fields=None): + """Get Users. + + Only administrators and managers can fetch all users. For other users + it is required to pass in 'project_name' filter. + + Args: + project_name (Optional[str]): Project name. + usernames (Optional[Iterable[str]]): Filter by usernames. + fields (Optional[Iterable[str]]): Fields to be queried + for users. + + Returns: + Generator[dict[str, Any]]: Queried users. + + """ + filters = {} + if usernames is not None: + usernames = set(usernames) + if not usernames: + return + filters["userNames"] = list(usernames) + + if project_name is not None: + filters["projectName"] = project_name + + con = ayon_api.get_server_api_connection() + if not fields: + fields = con.get_default_fields_for_type("user") + + query = users_graphql_query(set(fields)) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(con): + for user in parsed_data["users"]: + user["accessGroups"] = json.loads(user["accessGroups"]) + yield user +# --- END of ayon-python-api implementation --- class UserItem: @@ -32,19 +106,19 @@ class UserItem: class UsersModel: def __init__(self, controller): self._controller = controller - self._users_cache = CacheItem(default_factory=list) + self._users_cache = NestedCacheItem(default_factory=list) - def get_user_items(self): + def get_user_items(self, project_name): """Get user items. Returns: List[UserItem]: List of user items. """ - self._invalidate_cache() - return self._users_cache.get_data() + self._invalidate_cache(project_name) + return self._users_cache[project_name].get_data() - def get_user_items_by_name(self): + def get_user_items_by_name(self, project_name): """Get user items by name. Implemented as most of cases using this model will need to find @@ -56,10 +130,10 @@ class UsersModel: """ return { user_item.username: user_item - for user_item in self.get_user_items() + for user_item in self.get_user_items(project_name) } - def get_user_item_by_username(self, username): + def get_user_item_by_username(self, project_name, username): """Get user item by username. Args: @@ -70,15 +144,21 @@ class UsersModel: """ self._invalidate_cache() - for user_item in self.get_user_items(): + for user_item in self.get_user_items(project_name): if user_item.username == username: return user_item return None - def _invalidate_cache(self): - if self._users_cache.is_valid: + def _invalidate_cache(self, project_name): + cache = self._users_cache[project_name] + if cache.is_valid: return - self._users_cache.update_data([ + + if project_name is None: + cache.update_data([]) + return + + self._users_cache[project_name].update_data([ UserItem.from_entity_data(user) - for user in ayon_api.get_users() + for user in get_users(project_name) ]) diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py index 8fa9135bc0..bf48894654 100644 --- a/client/ayon_core/tools/workfiles/control.py +++ b/client/ayon_core/tools/workfiles/control.py @@ -278,7 +278,8 @@ class BaseWorkfileController( ) def get_user_items_by_name(self): - return self._users_model.get_user_items_by_name() + project_name = self.get_current_project_name() + return self._users_model.get_user_items_by_name(project_name) # Host information def get_workfile_extensions(self): From c241546842d5c9278fa7cf5f6ac71805311d41b3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 12 Jun 2024 11:31:54 +0200 Subject: [PATCH 41/66] Refactor media import process in LoadMedia plugin Extracted media import logic to a separate method for clarity. The new method handles importing media to Resolve Media Pool and creating bins if necessary. --- .../ayon_resolve/plugins/load/load_media.py | 101 +++++++++++------- 1 file changed, 61 insertions(+), 40 deletions(-) diff --git a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py index c6f8b97ada..d7d4f33266 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py @@ -20,6 +20,7 @@ from ayon_core.lib.transcoding import ( ) from ayon_core.lib import BoolDef from ayon_resolve.api import lib +from ayon_resolve.api import bmdvr from ayon_resolve.api.pipeline import AVALON_CONTAINER_ID @@ -214,46 +215,7 @@ class LoadMedia(LoaderPlugin): item = container["_item"] if item is None: - # Create or set the bin folder, we add it in there - # If bin path is not set we just add into the current active bin - if self.media_pool_bin_path: - media_pool_bin_path = StringTemplate( - self.media_pool_bin_path).format_strict(context) - folder = lib.create_bin( - name=media_pool_bin_path, - root=media_pool.GetRootFolder(), - set_as_current=False - ) - media_pool.SetCurrentFolder(folder) - - # Import media - # Resolve API: ImportMedia function requires a list of dictionaries - # with keys "FilePath", "StartIndex" and "EndIndex" for sequences - # but only string with absolute path for single files. - is_sequence, file_info = self._get_file_info(context) - if is_sequence: - items = media_pool.ImportMedia([file_info]) - else: - items = media_pool.ImportMedia([file_info["FilePath"]]) - - assert len(items) == 1, "Must import only one media item" - - item = items[0] - - self._set_metadata(item, context) - self._set_colorspace_from_representation(item, representation) - - data = self._get_container_data(context) - - # Add containerise data only needed on first load - data.update({ - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, - "loader": str(self.__class__.__name__), - }) - - item.SetMetadata(lib.pype_tag_name, json.dumps(data)) - + item = self._import_media_to_bin(context, media_pool, representation) # Always update clip color - even if re-using existing clip color = self.get_item_color(context) item.SetClipColor(color) @@ -267,6 +229,65 @@ class LoadMedia(LoaderPlugin): timeline=timeline ) + def _import_media_to_bin( + self, context, media_pool, representation + ): + """Import media to Resolve Media Pool. + + Also create a bin if `media_pool_bin_path` is set. + + Args: + context (dict): The context dictionary. + media_pool (resolve.MediaPool): The Resolve Media Pool. + representation (dict): The representation data. + + Returns: + resolve.MediaPoolItem: The imported media pool item. + """ + # Create or set the bin folder, we add it in there + # If bin path is not set we just add into the current active bin + if self.media_pool_bin_path: + media_pool_bin_path = StringTemplate( + self.media_pool_bin_path).format_strict(context) + + folder = lib.create_bin( + # double slashes will create unconnected folders + name=media_pool_bin_path.replace("//", "/"), + root=media_pool.GetRootFolder(), + set_as_current=False + ) + media_pool.SetCurrentFolder(folder) + + # Import media + # Resolve API: ImportMedia function requires a list of dictionaries + # with keys "FilePath", "StartIndex" and "EndIndex" for sequences + # but only string with absolute path for single files. + is_sequence, file_info = self._get_file_info(context) + items = ( + media_pool.ImportMedia([file_info]) + if is_sequence + else media_pool.ImportMedia([file_info["FilePath"]]) + ) + assert len(items) == 1, "Must import only one media item" + + result = items[0] + + self._set_metadata(result, context) + self._set_colorspace_from_representation(result, representation) + + data = self._get_container_data(context) + + # Add containerise data only needed on first load + data.update({ + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "loader": str(self.__class__.__name__), + }) + + result.SetMetadata(lib.pype_tag_name, json.dumps(data)) + + return result + def switch(self, container, context): self.update(container, context) From 728e3d6a7301b046a9e105b18f3f9496c46dad92 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Jun 2024 18:50:20 +0800 Subject: [PATCH 42/66] update get_fps_for_current_context() function --- server_addon/max/client/ayon_max/api/lib.py | 19 +------------------ .../ayon_max/plugins/load/load_pointcache.py | 4 +++- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/server_addon/max/client/ayon_max/api/lib.py b/server_addon/max/client/ayon_max/api/lib.py index 7c140be72e..4b5b07880b 100644 --- a/server_addon/max/client/ayon_max/api/lib.py +++ b/server_addon/max/client/ayon_max/api/lib.py @@ -298,24 +298,7 @@ def get_fps_for_current_context(): Union[int, float]: FPS value. """ task_entity = get_current_task_entity(fields={"attrib"}) - fps = task_entity.get("attrib", {}).get("fps") - if not fps: - project_name = get_current_project_name() - folder_path = get_current_folder_path() - folder_entity = ayon_api.get_folder_by_path( - project_name, folder_path, fields={"attrib.fps"} - ) or {} - - fps = folder_entity.get("attrib", {}).get("fps") - if not fps: - project_entity = ayon_api.get_project( - project_name, fields=["attrib.fps"] - ) or {} - fps = project_entity.get("attrib", {}).get("fps") - - if not fps: - fps = 25 - return fps + return task_entity["attrib"]["fps"] def reset_unit_scale(): diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index 3eb4ba3323..8ab25f2eeb 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -7,7 +7,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. import os from ayon_core.pipeline import load, get_representation_path from ayon_max.api import lib, maintained_selection -from ayon_max.api.lib import unique_namespace +from ayon_max.api.lib import unique_namespace, get_fps_for_current_context from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, @@ -31,6 +31,8 @@ class AbcLoader(load.LoaderPlugin): file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) + rt.frameRate = get_fps_for_current_context() + abc_before = { c for c in rt.rootNode.Children From 80293ae26442a84692ed6f48b62f530dbbc7cc43 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Jun 2024 18:55:08 +0800 Subject: [PATCH 43/66] ruff cosmetic fix --- server_addon/max/client/ayon_max/api/lib.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/server_addon/max/client/ayon_max/api/lib.py b/server_addon/max/client/ayon_max/api/lib.py index 4b5b07880b..7acc18196f 100644 --- a/server_addon/max/client/ayon_max/api/lib.py +++ b/server_addon/max/client/ayon_max/api/lib.py @@ -7,11 +7,8 @@ from typing import Any, Dict, Union import six -import ayon_api - from ayon_core.pipeline import ( get_current_project_name, - get_current_folder_path, colorspace ) from ayon_core.settings import get_project_settings From 8bff418a0937979c20442cb57ee5fa3f82a2c822 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Wed, 12 Jun 2024 19:21:39 +0800 Subject: [PATCH 44/66] Update server_addon/max/client/ayon_max/plugins/load/load_pointcache.py Co-authored-by: Roy Nieterau --- server_addon/max/client/ayon_max/plugins/load/load_pointcache.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index 8ab25f2eeb..cb5aaca2fd 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -31,7 +31,6 @@ class AbcLoader(load.LoaderPlugin): file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) - rt.frameRate = get_fps_for_current_context() abc_before = { c From 6e6d3c02f603331560586430b4b180e3bb46b8f3 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Wed, 12 Jun 2024 19:22:54 +0800 Subject: [PATCH 45/66] Update server_addon/max/client/ayon_max/plugins/load/load_pointcache.py Co-authored-by: Roy Nieterau --- server_addon/max/client/ayon_max/plugins/load/load_pointcache.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index cb5aaca2fd..cb1ffaa430 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -31,7 +31,6 @@ class AbcLoader(load.LoaderPlugin): file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) - abc_before = { c for c in rt.rootNode.Children From e8107e2b971142626ebaf3ddc047a7f67db0094a Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Wed, 12 Jun 2024 19:23:01 +0800 Subject: [PATCH 46/66] Update server_addon/max/client/ayon_max/plugins/load/load_pointcache.py Co-authored-by: Roy Nieterau --- .../max/client/ayon_max/plugins/load/load_pointcache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index cb1ffaa430..3eb4ba3323 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -7,7 +7,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. import os from ayon_core.pipeline import load, get_representation_path from ayon_max.api import lib, maintained_selection -from ayon_max.api.lib import unique_namespace, get_fps_for_current_context +from ayon_max.api.lib import unique_namespace from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, From da1bb3fad53f3dbe020e1294a6f115618ea1eb2c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:28:20 +0200 Subject: [PATCH 47/66] pointcache does not append attr definitions to base list but to new list --- .../plugins/create/create_animation_pointcache.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py b/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py index ea4cdb57fe..521dc1a681 100644 --- a/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py +++ b/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py @@ -99,8 +99,8 @@ class CreateAnimation(plugin.MayaHiddenCreator): return node_data def get_instance_attr_defs(self): - defs = super(CreateAnimation, self).get_instance_attr_defs() - defs += _get_animation_attr_defs(self) + defs = list(super().get_instance_attr_defs()) + defs.extend(_get_animation_attr_defs(self)) return defs @@ -123,8 +123,8 @@ class CreatePointCache(plugin.MayaCreator): return node_data def get_instance_attr_defs(self): - defs = super(CreatePointCache, self).get_instance_attr_defs() - defs += _get_animation_attr_defs(self) + defs = list(super().get_instance_attr_defs()) + defs.extend(_get_animation_attr_defs(self)) return defs def create(self, product_name, instance_data, pre_create_data): From dc5c55bb1abaace10704b31f4d85a6fef970a30b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:31:46 +0200 Subject: [PATCH 48/66] hero version is smaller than standard version --- client/ayon_core/tools/loader/abstract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index a1c1e6a062..ba1dcb73b6 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -177,7 +177,7 @@ class VersionItem: other_version = abs(other.version) # Hero version is greater than non-hero if version == other_version: - return self.is_hero + return not self.is_hero return version > other_version def __lt__(self, other): @@ -188,7 +188,7 @@ class VersionItem: other_version = abs(other.version) # Non-hero version is lesser than hero if version == other_version: - return not self.is_hero + return self.is_hero return version < other_version def __ge__(self, other): From 820bb7318e51248237b0352e63c79e869faabf89 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:48:35 +0200 Subject: [PATCH 49/66] don't call 'super' in 'get_instance_attr_defs' --- .../plugins/create/create_animation_pointcache.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py b/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py index 521dc1a681..aa9b61b46e 100644 --- a/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py +++ b/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py @@ -99,9 +99,7 @@ class CreateAnimation(plugin.MayaHiddenCreator): return node_data def get_instance_attr_defs(self): - defs = list(super().get_instance_attr_defs()) - defs.extend(_get_animation_attr_defs(self)) - return defs + return _get_animation_attr_defs(self) class CreatePointCache(plugin.MayaCreator): @@ -123,9 +121,7 @@ class CreatePointCache(plugin.MayaCreator): return node_data def get_instance_attr_defs(self): - defs = list(super().get_instance_attr_defs()) - defs.extend(_get_animation_attr_defs(self)) - return defs + return _get_animation_attr_defs(self) def create(self, product_name, instance_data, pre_create_data): instance = super(CreatePointCache, self).create( From 5a74d5c986084ddd7537648b9e6784295ed901d0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:48:52 +0200 Subject: [PATCH 50/66] do not pass self to the function --- .../ayon_maya/plugins/create/create_animation_pointcache.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py b/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py index aa9b61b46e..d98b0dd5fa 100644 --- a/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py +++ b/server_addon/maya/client/ayon_maya/plugins/create/create_animation_pointcache.py @@ -8,7 +8,7 @@ from ayon_core.lib import ( ) -def _get_animation_attr_defs(cls): +def _get_animation_attr_defs(): """Get Animation generic definitions.""" defs = lib.collect_animation_defs() defs.extend( @@ -99,7 +99,7 @@ class CreateAnimation(plugin.MayaHiddenCreator): return node_data def get_instance_attr_defs(self): - return _get_animation_attr_defs(self) + return _get_animation_attr_defs() class CreatePointCache(plugin.MayaCreator): @@ -121,7 +121,7 @@ class CreatePointCache(plugin.MayaCreator): return node_data def get_instance_attr_defs(self): - return _get_animation_attr_defs(self) + return _get_animation_attr_defs() def create(self, product_name, instance_data, pre_create_data): instance = super(CreatePointCache, self).create( From fca9eaa4238cc440009193796acff83c1c5c705f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:49:02 +0200 Subject: [PATCH 51/66] fix pre create attributes in look --- .../maya/client/ayon_maya/plugins/create/create_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/maya/client/ayon_maya/plugins/create/create_look.py b/server_addon/maya/client/ayon_maya/plugins/create/create_look.py index 1f90d18607..3e1ec103ba 100644 --- a/server_addon/maya/client/ayon_maya/plugins/create/create_look.py +++ b/server_addon/maya/client/ayon_maya/plugins/create/create_look.py @@ -42,6 +42,6 @@ class CreateLook(plugin.MayaCreator): def get_pre_create_attr_defs(self): # Show same attributes on create but include use selection - defs = super(CreateLook, self).get_pre_create_attr_defs() + defs = list(super().get_pre_create_attr_defs()) defs.extend(self.get_instance_attr_defs()) return defs From 70b5ac77984fd34dc3eafd797b821c2b60667d96 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:49:23 +0200 Subject: [PATCH 52/66] bump maya version to '0.2.3' --- server_addon/maya/client/ayon_maya/version.py | 2 +- server_addon/maya/package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/version.py b/server_addon/maya/client/ayon_maya/version.py index 1655067287..fcad19941f 100644 --- a/server_addon/maya/client/ayon_maya/version.py +++ b/server_addon/maya/client/ayon_maya/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'maya' version.""" -__version__ = "0.2.2" +__version__ = "0.2.3" diff --git a/server_addon/maya/package.py b/server_addon/maya/package.py index 627e824413..e0fc2ee5cf 100644 --- a/server_addon/maya/package.py +++ b/server_addon/maya/package.py @@ -1,6 +1,6 @@ name = "maya" title = "Maya" -version = "0.2.2" +version = "0.2.3" client_dir = "ayon_maya" ayon_required_addons = { From f17aacd83e7b4de7c87f0e3baa31bdd8a1b5530c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 12 Jun 2024 15:51:56 +0200 Subject: [PATCH 53/66] :recycle: remove unused container name name isn't used anywhere but it is hard required, causing crashes in hosts that are not having it. Also adding broad try/except to catch any invalid containers so they won't crash the whole list --- .../tools/sceneinventory/models/containers.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index 5230827ef6..f0248494e9 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -90,7 +90,6 @@ class ContainerItem: representation_id, loader_name, namespace, - name, object_name, item_id ): @@ -98,7 +97,6 @@ class ContainerItem: self.loader_name = loader_name self.object_name = object_name self.namespace = namespace - self.name = name self.item_id = item_id @classmethod @@ -107,7 +105,6 @@ class ContainerItem: representation_id=container["representation"], loader_name=container["loader"], namespace=container["namespace"], - name=container["name"], object_name=container["objectName"], item_id=uuid.uuid4().hex, ) @@ -204,7 +201,7 @@ class ContainersModel: def get_container_items(self): self._update_cache() return list(self._items_cache) - + def get_container_items_by_id(self, item_ids): return { item_id: self._container_items_by_id.get(item_id) @@ -329,14 +326,23 @@ class ContainersModel: containers = list(host.ls()) else: containers = [] + container_items = [] containers_by_id = {} container_items_by_id = {} for container in containers: - item = ContainerItem.from_container_data(container) - containers_by_id[item.item_id] = container - container_items_by_id[item.item_id] = item - container_items.append(item) + try: + item = ContainerItem.from_container_data(container) + containers_by_id[item.item_id] = container + container_items_by_id[item.item_id] = item + container_items.append(item) + except Exception as e: + # skip item if required data are missing + self._controller.log_error( + f"Failed to create item: {e}" + ) + continue + self._containers_by_id = containers_by_id self._container_items_by_id = container_items_by_id From a8bace3780377b06dbf4a69e41a20e8ad6964536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 12 Jun 2024 15:56:04 +0200 Subject: [PATCH 54/66] :arrow_up: bump version --- client/ayon_core/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index e4297e2000..efcd78cd11 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON core addon version.""" -__version__ = "0.3.3-dev.1" +__version__ = "0.3.4-dev.1" From b2be138552c4758e75d599c3164234255560704f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 12 Jun 2024 16:01:37 +0200 Subject: [PATCH 55/66] :arrow_down: un-bump the version --- client/ayon_core/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index efcd78cd11..e4297e2000 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON core addon version.""" -__version__ = "0.3.4-dev.1" +__version__ = "0.3.3-dev.1" From 7d838eb216b171bc8f8d90f2618efa0d3cabfbae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:04:21 +0200 Subject: [PATCH 56/66] Update client/ayon_core/tools/sceneinventory/models/containers.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/sceneinventory/models/containers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index f0248494e9..95c5322343 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -333,9 +333,6 @@ class ContainersModel: for container in containers: try: item = ContainerItem.from_container_data(container) - containers_by_id[item.item_id] = container - container_items_by_id[item.item_id] = item - container_items.append(item) except Exception as e: # skip item if required data are missing self._controller.log_error( @@ -343,6 +340,10 @@ class ContainersModel: ) continue + containers_by_id[item.item_id] = container + container_items_by_id[item.item_id] = item + container_items.append(item) + self._containers_by_id = containers_by_id self._container_items_by_id = container_items_by_id From a5ba2ce5c8c74083c5db6bd98d62fbb2c8277b49 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:47:29 +0200 Subject: [PATCH 57/66] fix cache invalidation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: FabiĆ  Serra Arrizabalaga --- client/ayon_core/tools/common_models/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/common_models/users.py b/client/ayon_core/tools/common_models/users.py index babb690f87..f7939e5cd3 100644 --- a/client/ayon_core/tools/common_models/users.py +++ b/client/ayon_core/tools/common_models/users.py @@ -143,7 +143,7 @@ class UsersModel: Union[UserItem, None]: User item or None if not found. """ - self._invalidate_cache() + self._invalidate_cache(project_name) for user_item in self.get_user_items(project_name): if user_item.username == username: return user_item From 513826d6081ec8598e0c49495418b63e2b2eabf9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:52:44 +0200 Subject: [PATCH 58/66] remove unused import --- .../resolve/client/ayon_resolve/plugins/load/load_media.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py index d7d4f33266..c1aaeca6bd 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/load/load_media.py @@ -20,7 +20,6 @@ from ayon_core.lib.transcoding import ( ) from ayon_core.lib import BoolDef from ayon_resolve.api import lib -from ayon_resolve.api import bmdvr from ayon_resolve.api.pipeline import AVALON_CONTAINER_ID From 0f350d3704f2aa830186445bcff5531d471d7aab Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Jun 2024 19:01:23 +0200 Subject: [PATCH 59/66] try to auto-fix shell true --- client/ayon_core/scripts/otio_burnin.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/scripts/otio_burnin.py b/client/ayon_core/scripts/otio_burnin.py index f12d298ac6..6b132b9a6a 100644 --- a/client/ayon_core/scripts/otio_burnin.py +++ b/client/ayon_core/scripts/otio_burnin.py @@ -14,9 +14,10 @@ from ayon_core.lib import ( convert_ffprobe_fps_value, ) +FFMPEG_EXE_COMMAND = subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg")) FFMPEG = ( '{}%(input_args)s -i "%(input)s" %(filters)s %(args)s%(output)s' -).format(subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg"))) +).format(FFMPEG_EXE_COMMAND) DRAWTEXT = ( "drawtext@'%(label)s'=fontfile='%(font)s':text=\\'%(text)s\\':" @@ -482,10 +483,19 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): ) print("Launching command: {}".format(command)) + use_shell = True + try: + test_proc = subprocess.Popen( + f"{FFMPEG_EXE_COMMAND} --help", shell=True + ) + test_proc.wait() + except BaseException: + use_shell = False + kwargs = { "stdout": subprocess.PIPE, "stderr": subprocess.PIPE, - "shell": True, + "shell": use_shell, } proc = subprocess.Popen(command, **kwargs) From edd19c313585b48b975435b67e54e6d939359b62 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 13 Jun 2024 14:25:57 +0800 Subject: [PATCH 60/66] reset frame range & frame rate for the correct frame range & fps value --- .../client/ayon_max/plugins/load/load_pointcache.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py index 3eb4ba3323..87ea5c75bc 100644 --- a/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py +++ b/server_addon/max/client/ayon_max/plugins/load/load_pointcache.py @@ -7,7 +7,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. import os from ayon_core.pipeline import load, get_representation_path from ayon_max.api import lib, maintained_selection -from ayon_max.api.lib import unique_namespace +from ayon_max.api.lib import unique_namespace, reset_frame_range from ayon_max.api.pipeline import ( containerise, get_previous_loaded_object, @@ -38,12 +38,9 @@ class AbcLoader(load.LoaderPlugin): } rt.AlembicImport.ImportToRoot = False - product_fps = float(context["version"]["attrib"].get("fps")) - if product_fps is None: - # Just stick to current scene FPS - product_fps = float(rt.frameRate) - # TODO: remove after the post-system fps setup - rt.frameRange = product_fps + # TODO: it will be removed after the improvement + # on the post-system setup + reset_frame_range() rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport) abc_after = { From eb124b54ad3781b79f507ef270fc86f2737d9128 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:01:46 +0200 Subject: [PATCH 61/66] use task name as source of truth from ui --- client/ayon_core/tools/workfiles/abstract.py | 13 ++++++----- client/ayon_core/tools/workfiles/control.py | 23 +++++++++++++++---- .../tools/workfiles/models/workfiles.py | 12 ++++++---- .../widgets/files_widget_workarea.py | 10 ++++---- .../tools/workfiles/widgets/side_panel.py | 16 ++++++------- 5 files changed, 46 insertions(+), 28 deletions(-) diff --git a/client/ayon_core/tools/workfiles/abstract.py b/client/ayon_core/tools/workfiles/abstract.py index f345e20dca..330b413300 100644 --- a/client/ayon_core/tools/workfiles/abstract.py +++ b/client/ayon_core/tools/workfiles/abstract.py @@ -834,12 +834,13 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): pass @abstractmethod - def get_workarea_file_items(self, folder_id, task_id): + def get_workarea_file_items(self, folder_id, task_name, sender=None): """Get workarea file items. Args: folder_id (str): Folder id. - task_id (str): Task id. + task_name (str): Task name. + sender (Optional[str]): Who requested workarea file items. Returns: list[FileItem]: List of workarea file items. @@ -905,12 +906,12 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): pass @abstractmethod - def get_workfile_info(self, folder_id, task_id, filepath): + def get_workfile_info(self, folder_id, task_name, filepath): """Workfile info from database. Args: folder_id (str): Folder id. - task_id (str): Task id. + task_name (str): Task id. filepath (str): Workfile path. Returns: @@ -921,7 +922,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): pass @abstractmethod - def save_workfile_info(self, folder_id, task_id, filepath, note): + def save_workfile_info(self, folder_id, task_name, filepath, note): """Save workfile info to database. At this moment the only information which can be saved about @@ -932,7 +933,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): Args: folder_id (str): Folder id. - task_id (str): Task id. + task_name (str): Task id. filepath (str): Workfile path. note (Union[str, None]): Note. """ diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py index 8fa9135bc0..1f66b363e1 100644 --- a/client/ayon_core/tools/workfiles/control.py +++ b/client/ayon_core/tools/workfiles/control.py @@ -410,9 +410,11 @@ class BaseWorkfileController( return self._workfiles_model.get_workarea_dir_by_context( folder_id, task_id) - def get_workarea_file_items(self, folder_id, task_id): + def get_workarea_file_items(self, folder_id, task_name, sender=None): + task_id = self._get_task_id(folder_id, task_name) return self._workfiles_model.get_workarea_file_items( - folder_id, task_id) + folder_id, task_id, task_name + ) def get_workarea_save_as_data(self, folder_id, task_id): return self._workfiles_model.get_workarea_save_as_data( @@ -447,12 +449,14 @@ class BaseWorkfileController( return self._workfiles_model.get_published_file_items( folder_id, task_name) - def get_workfile_info(self, folder_id, task_id, filepath): + def get_workfile_info(self, folder_id, task_name, filepath): + task_id = self._get_task_id(folder_id, task_name) return self._workfiles_model.get_workfile_info( folder_id, task_id, filepath ) - def save_workfile_info(self, folder_id, task_id, filepath, note): + def save_workfile_info(self, folder_id, task_name, filepath, note): + task_id = self._get_task_id(folder_id, task_name) self._workfiles_model.save_workfile_info( folder_id, task_id, filepath, note ) @@ -627,6 +631,17 @@ class BaseWorkfileController( def _emit_event(self, topic, data=None): self.emit_event(topic, data, "controller") + def _get_task_id(self, folder_id, task_name, sender=None): + task_item = self._hierarchy_model.get_task_item_by_name( + self.get_current_project_name(), + folder_id, + task_name, + sender + ) + if not task_item: + return None + return task_item.id + # Expected selection # - expected selection is used to restore selection after refresh # or when current context should be used diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index c93bbb6637..a47e459a6e 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -173,7 +173,7 @@ class WorkareaModel: folder_mapping[task_id] = workdir return workdir - def get_file_items(self, folder_id, task_id): + def get_file_items(self, folder_id, task_id, task_name): items = [] if not folder_id or not task_id: return items @@ -192,7 +192,7 @@ class WorkareaModel: continue workfile_info = self._controller.get_workfile_info( - folder_id, task_id, filepath + folder_id, task_name, filepath ) modified = os.path.getmtime(filepath) items.append(FileItem( @@ -770,19 +770,21 @@ class WorkfilesModel: return self._workarea_model.get_workarea_dir_by_context( folder_id, task_id) - def get_workarea_file_items(self, folder_id, task_id): + def get_workarea_file_items(self, folder_id, task_id, task_name): """Workfile items for passed context from workarea. Args: folder_id (Union[str, None]): Folder id. task_id (Union[str, None]): Task id. + task_name (Union[str, None]): Task name. Returns: list[FileItem]: List of file items matching workarea of passed context. """ - - return self._workarea_model.get_file_items(folder_id, task_id) + return self._workarea_model.get_file_items( + folder_id, task_id, task_name + ) def get_workarea_save_as_data(self, folder_id, task_id): return self._workarea_model.get_workarea_save_as_data( diff --git a/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py b/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py index 5c102dcdd4..7f76b6a8ab 100644 --- a/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py +++ b/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py @@ -66,7 +66,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): self._empty_item_used = False self._published_mode = False self._selected_folder_id = None - self._selected_task_id = None + self._selected_task_name = None self._add_missing_context_item() @@ -153,7 +153,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): def _on_task_changed(self, event): self._selected_folder_id = event["folder_id"] - self._selected_task_id = event["task_id"] + self._selected_task_name = event["task_name"] if not self._published_mode: self._fill_items() @@ -179,13 +179,13 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): def _fill_items_impl(self): folder_id = self._selected_folder_id - task_id = self._selected_task_id - if not folder_id or not task_id: + task_name = self._selected_task_name + if not folder_id or not task_name: self._add_missing_context_item() return file_items = self._controller.get_workarea_file_items( - folder_id, task_id + folder_id, task_name ) root_item = self.invisibleRootItem() if not file_items: diff --git a/client/ayon_core/tools/workfiles/widgets/side_panel.py b/client/ayon_core/tools/workfiles/widgets/side_panel.py index 53fdf0e0ac..7ba60b5544 100644 --- a/client/ayon_core/tools/workfiles/widgets/side_panel.py +++ b/client/ayon_core/tools/workfiles/widgets/side_panel.py @@ -75,7 +75,7 @@ class SidePanelWidget(QtWidgets.QWidget): self._btn_note_save = btn_note_save self._folder_id = None - self._task_id = None + self._task_name = None self._filepath = None self._orig_note = "" self._controller = controller @@ -93,10 +93,10 @@ class SidePanelWidget(QtWidgets.QWidget): def _on_selection_change(self, event): folder_id = event["folder_id"] - task_id = event["task_id"] + task_name = event["task_name"] filepath = event["path"] - self._set_context(folder_id, task_id, filepath) + self._set_context(folder_id, task_name, filepath) def _on_note_change(self): text = self._note_input.toPlainText() @@ -106,19 +106,19 @@ class SidePanelWidget(QtWidgets.QWidget): note = self._note_input.toPlainText() self._controller.save_workfile_info( self._folder_id, - self._task_id, + self._task_name, self._filepath, note ) self._orig_note = note self._btn_note_save.setEnabled(False) - def _set_context(self, folder_id, task_id, filepath): + def _set_context(self, folder_id, task_name, filepath): workfile_info = None # Check if folder, task and file are selected - if bool(folder_id) and bool(task_id) and bool(filepath): + if bool(folder_id) and bool(task_name) and bool(filepath): workfile_info = self._controller.get_workfile_info( - folder_id, task_id, filepath + folder_id, task_name, filepath ) enabled = workfile_info is not None @@ -127,7 +127,7 @@ class SidePanelWidget(QtWidgets.QWidget): self._btn_note_save.setEnabled(enabled) self._folder_id = folder_id - self._task_id = task_id + self._task_name = task_name self._filepath = filepath # Disable inputs and remove texts if any required arguments are From 43713d7ac01f021ff8a81a59e87c7acc5142037c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:16:08 +0200 Subject: [PATCH 62/66] define id of workfile entity for updates --- client/ayon_core/tools/workfiles/models/workfiles.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index a47e459a6e..a268a9bd0e 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -1,6 +1,7 @@ import os import re import copy +import uuid import arrow import ayon_api @@ -587,6 +588,7 @@ class WorkfileEntitiesModel: username = self._get_current_username() workfile_info = { + "id": uuid.uuid4().hex, "path": rootless_path, "taskId": task_id, "attrib": { From ad23386d189453b1acaa1d8c86681d8a9185dfcd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:16:15 +0200 Subject: [PATCH 63/66] fix create new workfile --- client/ayon_core/tools/workfiles/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py index 1f66b363e1..fbd0bd3ac8 100644 --- a/client/ayon_core/tools/workfiles/control.py +++ b/client/ayon_core/tools/workfiles/control.py @@ -737,7 +737,7 @@ class BaseWorkfileController( self._host_save_workfile(dst_filepath) # Make sure workfile info exists - self.save_workfile_info(folder_id, task_id, dst_filepath, None) + self.save_workfile_info(folder_id, task_name, dst_filepath, None) # Create extra folders create_workdir_extra_folders( From 8c5f46dc13c22c0612936374126c2db81e9c747f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 14 Jun 2024 11:41:11 +0200 Subject: [PATCH 64/66] Refactor saving and opening workfiles, display push message. - Refactored saving and opening workfiles in `main()` function. - Display a message indicating the pushed workfile path after pushing to project. --- .../nuke/client/ayon_nuke/api/push_to_project.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/server_addon/nuke/client/ayon_nuke/api/push_to_project.py b/server_addon/nuke/client/ayon_nuke/api/push_to_project.py index f145ed652b..852e5d0e31 100644 --- a/server_addon/nuke/client/ayon_nuke/api/push_to_project.py +++ b/server_addon/nuke/client/ayon_nuke/api/push_to_project.py @@ -70,7 +70,6 @@ def main(): anatomy, project_settings=project_settings ) - # Save current workfile. current_file = host.current_file() host.save_file(current_file) @@ -109,8 +108,11 @@ def main(): node["file"].setValue(path.replace("\\", "/")) # Save current workfile to new context. - basename = os.path.basename(current_file) - host.save_file(os.path.join(workdir, basename)) + pushed_workfile = os.path.join( + workdir, os.path.basename(current_file)) + host.save_file(pushed_workfile) - # Open current contex workfile. + # Open current context workfile. host.open_file(current_file) + + nuke.message(f"Pushed to project: \n{pushed_workfile}") From 9f85d4a2405648d672c6178a9ae5b693ebdb6a96 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 14 Jun 2024 11:41:29 +0200 Subject: [PATCH 65/66] Remove unnecessary callback for workfile builder in pipeline.py. Improve script load settings. --- server_addon/nuke/client/ayon_nuke/api/pipeline.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/server_addon/nuke/client/ayon_nuke/api/pipeline.py b/server_addon/nuke/client/ayon_nuke/api/pipeline.py index 8a9b5cb666..edf1e2dc03 100644 --- a/server_addon/nuke/client/ayon_nuke/api/pipeline.py +++ b/server_addon/nuke/client/ayon_nuke/api/pipeline.py @@ -160,9 +160,6 @@ def add_nuke_callbacks(): # template builder callbacks nuke.addOnCreate(start_workfile_template_builder, nodeClass="Root") - # TODO: remove this callback once workfile builder will be removed - nuke.addOnCreate(process_workfile_builder, nodeClass="Root") - # fix ffmpeg settings on script nuke.addOnScriptLoad(on_script_load) From 4c8a6f07ff1df67c849b1d2f009c9be412494ddc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 14 Jun 2024 11:42:42 +0200 Subject: [PATCH 66/66] Remove unused process_workfile_builder function from pipeline module The commit removes the unused process_workfile_builder function from the pipeline module. --- server_addon/nuke/client/ayon_nuke/api/pipeline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/server_addon/nuke/client/ayon_nuke/api/pipeline.py b/server_addon/nuke/client/ayon_nuke/api/pipeline.py index edf1e2dc03..2ba430c272 100644 --- a/server_addon/nuke/client/ayon_nuke/api/pipeline.py +++ b/server_addon/nuke/client/ayon_nuke/api/pipeline.py @@ -37,8 +37,6 @@ from .lib import ( INSTANCE_DATA_KNOB, get_main_window, WorkfileSettings, - # TODO: remove this once workfile builder will be removed - process_workfile_builder, start_workfile_template_builder, launch_workfiles_app, check_inventory_versions,