From 424e206f099e2e85e6faf4e078a8fbe3b9468497 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 29 Feb 2024 16:53:17 +0100 Subject: [PATCH 01/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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 f861dfa73d8a17c33e82c263f01341e410401b44 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Jun 2024 12:47:49 +0200 Subject: [PATCH 13/23] 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 14/23] 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 15/23] 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 16/23] 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 17/23] 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 a0ba192306429ce64921869c1161ce8593be536c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 11 Jun 2024 14:05:07 +0200 Subject: [PATCH 18/23] 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 ee5d82994db8eca9c67a9b2923ebb28bf41831b8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 11 Jun 2024 14:11:28 +0200 Subject: [PATCH 19/23] 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 9e4d1f19ebebcdc35f7e7ae1c1100c281e2042c4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 11 Jun 2024 14:32:25 +0200 Subject: [PATCH 20/23] 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 21c86742d4de1abd928d373f3634b58cdf1fae98 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 11 Jun 2024 14:55:00 +0200 Subject: [PATCH 21/23] 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 22/23] 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 c241546842d5c9278fa7cf5f6ac71805311d41b3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 12 Jun 2024 11:31:54 +0200 Subject: [PATCH 23/23] 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)