From 42f7549e059e9bdf85b6c360b9808bf02b7727e5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Oct 2023 16:37:45 +0200 Subject: [PATCH 01/28] resolve: adding input arg to create new timeline --- openpype/hosts/resolve/api/lib.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index eaee3bb9ba..a88564a3ef 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -125,7 +125,7 @@ def get_any_timeline(): return project.GetTimelineByIndex(1) -def get_new_timeline(): +def get_new_timeline(timeline_name: str = None): """Get new timeline object. Returns: @@ -133,7 +133,8 @@ def get_new_timeline(): """ project = get_current_project() media_pool = project.GetMediaPool() - new_timeline = media_pool.CreateEmptyTimeline(self.pype_timeline_name) + new_timeline = media_pool.CreateEmptyTimeline( + timeline_name or self.pype_timeline_name) project.SetCurrentTimeline(new_timeline) return new_timeline From b8cee701a36742a40b8111227bd5c30bc8f183d9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Oct 2023 16:38:42 +0200 Subject: [PATCH 02/28] resolve: load multiple clips to new timeline fix --- openpype/hosts/resolve/api/plugin.py | 40 ++++++++++++------- .../hosts/resolve/plugins/load/load_clip.py | 6 --- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index e2bd76ffa2..ddf0df662b 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -291,17 +291,17 @@ class ClipLoader: active_bin = None data = dict() - def __init__(self, cls, context, path, **options): + def __init__(self, loader_obj, context, path, **options): """ Initialize object Arguments: - cls (openpype.pipeline.load.LoaderPlugin): plugin object + loader_obj (openpype.pipeline.load.LoaderPlugin): plugin object context (dict): loader plugin context options (dict)[optional]: possible keys: projectBinPath: "path/to/binItem" """ - self.__dict__.update(cls.__dict__) + self.__dict__.update(loader_obj.__dict__) self.context = context self.active_project = lib.get_current_project() self.fname = path @@ -319,23 +319,29 @@ class ClipLoader: # inject asset data to representation dict self._get_asset_data() - print("__init__ self.data: `{}`".format(self.data)) # add active components to class if self.new_timeline: - if options.get("timeline"): + loader_cls = loader_obj.__class__ + if loader_cls.timeline: # if multiselection is set then use options sequence - self.active_timeline = options["timeline"] + self.active_timeline = loader_cls.timeline else: # create new sequence - self.active_timeline = ( - lib.get_current_timeline() or - lib.get_new_timeline() + self.active_timeline = lib.get_new_timeline( + "{}_{}_{}".format( + self.subset, + self.representation, + str(uuid.uuid4())[:8] + ) ) + loader_cls.timeline = self.active_timeline + + print(self.active_timeline.GetName()) else: self.active_timeline = lib.get_current_timeline() - cls.timeline = self.active_timeline + def _populate_data(self): """ Gets context and convert it to self.data @@ -349,10 +355,14 @@ class ClipLoader: # create name repr = self.context["representation"] repr_cntx = repr["context"] - asset = str(repr_cntx["asset"]) - subset = str(repr_cntx["subset"]) - representation = str(repr_cntx["representation"]) - self.data["clip_name"] = "_".join([asset, subset, representation]) + self.asset = str(repr_cntx["asset"]) + self.subset = str(repr_cntx["subset"]) + self.representation = str(repr_cntx["representation"]) + self.data["clip_name"] = "_".join([ + self.asset, + self.subset, + self.representation + ]) self.data["versionData"] = self.context["version"]["data"] # gets file path file = self.fname @@ -367,7 +377,7 @@ class ClipLoader: hierarchy = str("/".join(( "Loader", repr_cntx["hierarchy"].replace("\\", "/"), - asset + self.asset ))) self.data["binPath"] = hierarchy diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index 3a59ecea80..1d66c97041 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -48,12 +48,6 @@ class LoadClip(plugin.TimelineItemLoader): def load(self, context, name, namespace, options): - # in case loader uses multiselection - if self.timeline: - options.update({ - "timeline": self.timeline, - }) - # load clip to timeline and get main variables path = self.filepath_from_context(context) timeline_item = plugin.ClipLoader( From 01eb8ade9bc522012f1621741d5d89622456f420 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Oct 2023 17:04:04 +0200 Subject: [PATCH 03/28] fixing inventory management for version update --- openpype/hosts/resolve/api/lib.py | 20 +++++++++++++------ .../hosts/resolve/plugins/load/load_clip.py | 6 +++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index a88564a3ef..65c91fcdf6 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -395,14 +395,22 @@ def get_current_timeline_items( def get_pype_timeline_item_by_name(name: str) -> object: - track_itmes = get_current_timeline_items() - for _ti in track_itmes: - tag_data = get_timeline_item_pype_tag(_ti["clip"]["item"]) - tag_name = tag_data.get("name") + """Get timeline item by name. + + Args: + name (str): name of timeline item + + Returns: + object: resolve.TimelineItem + """ + for _ti_data in get_current_timeline_items(): + _ti_clip = _ti_data["clip"]["item"] + tag_data = get_timeline_item_pype_tag(_ti_clip) + tag_name = tag_data.get("namespace") if not tag_name: continue - if tag_data.get("name") in name: - return _ti + if tag_name in name: + return _ti_clip return None diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index 1d66c97041..eea44a3726 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -102,8 +102,8 @@ class LoadClip(plugin.TimelineItemLoader): context.update({"representation": representation}) name = container['name'] namespace = container['namespace'] - timeline_item_data = lib.get_pype_timeline_item_by_name(namespace) - timeline_item = timeline_item_data["clip"]["item"] + timeline_item = lib.get_pype_timeline_item_by_name(namespace) + project_name = get_current_project_name() version = get_version_by_id(project_name, representation["parent"]) version_data = version.get("data", {}) @@ -111,8 +111,8 @@ class LoadClip(plugin.TimelineItemLoader): colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) path = get_representation_path(representation) - context["version"] = {"data": version_data} + context["version"] = {"data": version_data} loader = plugin.ClipLoader(self, context, path) timeline_item = loader.update(timeline_item) From f0b38dbb9a830d4475cdc34d87f8b5bd2d245645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 3 Oct 2023 12:49:37 +0200 Subject: [PATCH 04/28] Update openpype/hosts/resolve/api/lib.py Co-authored-by: Roy Nieterau --- openpype/hosts/resolve/api/lib.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 65c91fcdf6..92e600d55b 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -128,6 +128,9 @@ def get_any_timeline(): def get_new_timeline(timeline_name: str = None): """Get new timeline object. +Arguments: + timeline_name (str): New timeline name. + Returns: object: resolve.Timeline """ From 4bd820bc30299b018c5f21d5f1b659045c360279 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 3 Oct 2023 13:00:49 +0200 Subject: [PATCH 05/28] removing attachmets from self and moving into `timeline_basename` --- openpype/hosts/resolve/api/lib.py | 4 ++-- openpype/hosts/resolve/api/plugin.py | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 92e600d55b..9bdd62d52e 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -128,8 +128,8 @@ def get_any_timeline(): def get_new_timeline(timeline_name: str = None): """Get new timeline object. -Arguments: - timeline_name (str): New timeline name. + Arguments: + timeline_name (str): New timeline name. Returns: object: resolve.Timeline diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index ddf0df662b..1fc3ed226c 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -329,9 +329,8 @@ class ClipLoader: else: # create new sequence self.active_timeline = lib.get_new_timeline( - "{}_{}_{}".format( - self.subset, - self.representation, + "{}_{}".format( + self.data["timeline_basename"], str(uuid.uuid4())[:8] ) ) @@ -355,13 +354,13 @@ class ClipLoader: # create name repr = self.context["representation"] repr_cntx = repr["context"] - self.asset = str(repr_cntx["asset"]) - self.subset = str(repr_cntx["subset"]) - self.representation = str(repr_cntx["representation"]) + asset = str(repr_cntx["asset"]) + subset = str(repr_cntx["subset"]) + representation = str(repr_cntx["representation"]) self.data["clip_name"] = "_".join([ - self.asset, - self.subset, - self.representation + asset, + subset, + representation ]) self.data["versionData"] = self.context["version"]["data"] # gets file path @@ -372,12 +371,14 @@ class ClipLoader: "Representation id `{}` is failing to load".format(repr_id)) return None self.data["path"] = file.replace("\\", "/") + self.data["timeline_basename"] = "timeline_{}_{}".format( + subset, representation) # solve project bin structure path hierarchy = str("/".join(( "Loader", repr_cntx["hierarchy"].replace("\\", "/"), - self.asset + asset ))) self.data["binPath"] = hierarchy From f5e8f4d3faf50f3da237702dd2b26c2cdb24ef40 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 3 Oct 2023 13:02:17 +0200 Subject: [PATCH 06/28] removing debugging prints --- openpype/hosts/resolve/api/plugin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 1fc3ed226c..da5e649576 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -336,7 +336,6 @@ class ClipLoader: ) loader_cls.timeline = self.active_timeline - print(self.active_timeline.GetName()) else: self.active_timeline = lib.get_current_timeline() @@ -660,8 +659,6 @@ class PublishClip: # define ui inputs if non gui mode was used self.shot_num = self.ti_index - print( - "____ self.shot_num: {}".format(self.shot_num)) # ui_inputs data or default values if gui was not used self.rename = self.ui_inputs.get( From 4656e59759ad0ae6ac31564ece150802759779b2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 3 Oct 2023 13:10:51 +0200 Subject: [PATCH 07/28] debug logging cleaning --- openpype/hosts/resolve/api/lib.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 9bdd62d52e..735d2057f8 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -281,7 +281,6 @@ def create_timeline_item(media_pool_item: object, if source_end is not None: clip_data.update({"endFrame": source_end}) - print(clip_data) # add to timeline media_pool.AppendToTimeline([clip_data]) @@ -560,7 +559,6 @@ def get_pype_marker(timeline_item): note = timeline_item_markers[marker_frame]["note"] color = timeline_item_markers[marker_frame]["color"] name = timeline_item_markers[marker_frame]["name"] - print(f"_ marker data: {marker_frame} | {name} | {color} | {note}") if name == self.pype_marker_name and color == self.pype_marker_color: self.temp_marker_frame = marker_frame return json.loads(note) @@ -630,7 +628,7 @@ def create_compound_clip(clip_data, name, folder): if c.GetName() in name), None) if cct: - print(f"_ cct exists: {cct}") + print(f"Compound clip exists: {cct}") else: # Create empty timeline in current folder and give name: cct = mp.CreateEmptyTimeline(name) @@ -639,7 +637,7 @@ def create_compound_clip(clip_data, name, folder): clips = folder.GetClipList() cct = next((c for c in clips if c.GetName() in name), None) - print(f"_ cct created: {cct}") + print(f"Compound clip created: {cct}") with maintain_current_timeline(cct, tl_origin): # Add input clip to the current timeline: From 908e980a404bf33dd7657414658cbd801ceb86d0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 Oct 2023 13:21:25 +0200 Subject: [PATCH 08/28] updating importing to media pool to newer api --- openpype/hosts/resolve/api/lib.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 735d2057f8..fb4b08cc1e 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -196,7 +196,6 @@ def create_media_pool_item(fpath: str, object: resolve.MediaPoolItem """ # get all variables - media_storage = get_media_storage() media_pool = get_current_project().GetMediaPool() root_bin = root or media_pool.GetRootFolder() @@ -205,23 +204,10 @@ def create_media_pool_item(fpath: str, if existing_mpi: return existing_mpi + # add all data in folder to media pool + media_pool_items = media_pool.ImportMedia(fpath) - dirname, file = os.path.split(fpath) - _name, ext = os.path.splitext(file) - - # add all data in folder to mediapool - media_pool_items = media_storage.AddItemListToMediaPool( - os.path.normpath(dirname)) - - if not media_pool_items: - return False - - # if any are added then look into them for the right extension - media_pool_item = [mpi for mpi in media_pool_items - if ext in mpi.GetClipProperty("File Path")] - - # return only first found - return media_pool_item.pop() + return media_pool_items.pop() if media_pool_items else False def get_media_pool_item(fpath, root: object = None) -> object: From 26e0cacd3a676d085ff28719a6c52176d1757253 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 Oct 2023 14:03:15 +0200 Subject: [PATCH 09/28] removing test scrips --- .../utility_scripts/tests/test_otio_as_edl.py | 49 ------------- .../testing_create_timeline_item_from_path.py | 73 ------------------- .../tests/testing_load_media_pool_item.py | 24 ------ .../tests/testing_startup_script.py | 5 -- .../tests/testing_timeline_op.py | 13 ---- 5 files changed, 164 deletions(-) delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py diff --git a/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py b/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py deleted file mode 100644 index 92f2e43a72..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py +++ /dev/null @@ -1,49 +0,0 @@ -#! python3 -import os -import sys - -import opentimelineio as otio - -from openpype.pipeline import install_host - -import openpype.hosts.resolve.api as bmdvr -from openpype.hosts.resolve.api.testing_utils import TestGUI -from openpype.hosts.resolve.otio import davinci_export as otio_export - - -class ThisTestGUI(TestGUI): - extensions = [".exr", ".jpg", ".mov", ".png", ".mp4", ".ari", ".arx"] - - def __init__(self): - super(ThisTestGUI, self).__init__() - # activate resolve from openpype - install_host(bmdvr) - - def _open_dir_button_pressed(self, event): - # selected_path = self.fu.RequestFile(os.path.expanduser("~")) - selected_path = self.fu.RequestDir(os.path.expanduser("~")) - self._widgets["inputTestSourcesFolder"].Text = selected_path - - # main function - def process(self, event): - self.input_dir_path = self._widgets["inputTestSourcesFolder"].Text - project = bmdvr.get_current_project() - otio_timeline = otio_export.create_otio_timeline(project) - print(f"_ otio_timeline: `{otio_timeline}`") - edl_path = os.path.join(self.input_dir_path, "this_file_name.edl") - print(f"_ edl_path: `{edl_path}`") - # xml_string = otio_adapters.fcpx_xml.write_to_string(otio_timeline) - # print(f"_ xml_string: `{xml_string}`") - otio.adapters.write_to_file( - otio_timeline, edl_path, adapter_name="cmx_3600") - project = bmdvr.get_current_project() - media_pool = project.GetMediaPool() - timeline = media_pool.ImportTimelineFromFile(edl_path) - # at the end close the window - self._close_window(None) - - -if __name__ == "__main__": - test_gui = ThisTestGUI() - test_gui.show_gui() - sys.exit(not bool(True)) diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py b/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py deleted file mode 100644 index 91a361ec08..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py +++ /dev/null @@ -1,73 +0,0 @@ -#! python3 -import os -import sys - -import clique - -from openpype.pipeline import install_host -from openpype.hosts.resolve.api.testing_utils import TestGUI -import openpype.hosts.resolve.api as bmdvr -from openpype.hosts.resolve.api.lib import ( - create_media_pool_item, - create_timeline_item, -) - - -class ThisTestGUI(TestGUI): - extensions = [".exr", ".jpg", ".mov", ".png", ".mp4", ".ari", ".arx"] - - def __init__(self): - super(ThisTestGUI, self).__init__() - # activate resolve from openpype - install_host(bmdvr) - - def _open_dir_button_pressed(self, event): - # selected_path = self.fu.RequestFile(os.path.expanduser("~")) - selected_path = self.fu.RequestDir(os.path.expanduser("~")) - self._widgets["inputTestSourcesFolder"].Text = selected_path - - # main function - def process(self, event): - self.input_dir_path = self._widgets["inputTestSourcesFolder"].Text - - self.dir_processing(self.input_dir_path) - - # at the end close the window - self._close_window(None) - - def dir_processing(self, dir_path): - collections, reminders = clique.assemble(os.listdir(dir_path)) - - # process reminders - for _rem in reminders: - _rem_path = os.path.join(dir_path, _rem) - - # go deeper if directory - if os.path.isdir(_rem_path): - print(_rem_path) - self.dir_processing(_rem_path) - else: - self.file_processing(_rem_path) - - # process collections - for _coll in collections: - _coll_path = os.path.join(dir_path, list(_coll).pop()) - self.file_processing(_coll_path) - - def file_processing(self, fpath): - print(f"_ fpath: `{fpath}`") - _base, ext = os.path.splitext(fpath) - # skip if unwanted extension - if ext not in self.extensions: - return - media_pool_item = create_media_pool_item(fpath) - print(media_pool_item) - - track_item = create_timeline_item(media_pool_item) - print(track_item) - - -if __name__ == "__main__": - test_gui = ThisTestGUI() - test_gui.show_gui() - sys.exit(not bool(True)) diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py b/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py deleted file mode 100644 index 2e83188bde..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py +++ /dev/null @@ -1,24 +0,0 @@ -#! python3 -from openpype.pipeline import install_host -from openpype.hosts.resolve import api as bmdvr -from openpype.hosts.resolve.api.lib import ( - create_media_pool_item, - create_timeline_item, -) - - -def file_processing(fpath): - media_pool_item = create_media_pool_item(fpath) - print(media_pool_item) - - track_item = create_timeline_item(media_pool_item) - print(track_item) - - -if __name__ == "__main__": - path = "C:/CODE/__openpype_projects/jtest03dev/shots/sq01/mainsq01sh030/publish/plate/plateMain/v006/jt3d_mainsq01sh030_plateMain_v006.0996.exr" - - # activate resolve from openpype - install_host(bmdvr) - - file_processing(path) diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py b/openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py deleted file mode 100644 index b64714ab16..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py +++ /dev/null @@ -1,5 +0,0 @@ -#! python3 -from openpype.hosts.resolve.startup import main - -if __name__ == "__main__": - main() diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py b/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py deleted file mode 100644 index 8270496f64..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py +++ /dev/null @@ -1,13 +0,0 @@ -#! python3 -from openpype.pipeline import install_host -from openpype.hosts.resolve import api as bmdvr -from openpype.hosts.resolve.api.lib import get_current_project - -if __name__ == "__main__": - install_host(bmdvr) - project = get_current_project() - timeline_count = project.GetTimelineCount() - print(f"Timeline count: {timeline_count}") - timeline = project.GetTimelineByIndex(timeline_count) - print(f"Timeline name: {timeline.GetName()}") - print(timeline.GetTrackCount("video")) From 19840862426e74c6558803864ec212014ada186f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 Oct 2023 14:03:38 +0200 Subject: [PATCH 10/28] finalizing update of importing clips with multiple frames --- openpype/hosts/resolve/api/lib.py | 43 +++++++++++++++++++++-- openpype/hosts/resolve/api/pipeline.py | 1 - openpype/hosts/resolve/api/plugin.py | 47 ++++++++++++++++++++------ 3 files changed, 77 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index fb4b08cc1e..8564a24ac1 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -2,6 +2,7 @@ import sys import json import re import os +import glob import contextlib from opentimelineio import opentime @@ -183,8 +184,14 @@ def create_bin(name: str, root: object = None) -> object: return media_pool.GetCurrentFolder() -def create_media_pool_item(fpath: str, - root: object = None) -> object: +def create_media_pool_item( + fpath: str, + frame_start: int, + frame_end: int, + handle_start: int, + handle_end: int, + root: object = None, +) -> object: """ Create media pool item. @@ -204,8 +211,38 @@ def create_media_pool_item(fpath: str, if existing_mpi: return existing_mpi + + files = [] + first_frame = frame_start - handle_start + last_frame = frame_end + handle_end + dir_path = os.path.dirname(fpath) + base_name = os.path.basename(fpath) + + # prepare glob pattern for searching + padding = len(str(last_frame)) + str_first_frame = str(first_frame).zfill(padding) + + # convert str_first_frame to glob pattern + # replace all digits with `?` and all other chars with `[char]` + # example: `0001` -> `????` + glob_pattern = re.sub(r"\d", "?", str_first_frame) + + # in filename replace number with glob pattern + # example: `filename.0001.exr` -> `filename.????.exr` + base_name = re.sub(str_first_frame, glob_pattern, base_name) + + # get all files in folder + for file in glob.glob(os.path.join(dir_path, base_name)): + files.append(file) + + # iterate all files and check if they exists + # if not then remove them from list + for file in files[:]: + if not os.path.exists(file): + files.remove(file) + # add all data in folder to media pool - media_pool_items = media_pool.ImportMedia(fpath) + media_pool_items = media_pool.ImportMedia(files) return media_pool_items.pop() if media_pool_items else False diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py index 899cb825bb..b379c7b2e0 100644 --- a/openpype/hosts/resolve/api/pipeline.py +++ b/openpype/hosts/resolve/api/pipeline.py @@ -117,7 +117,6 @@ def containerise(timeline_item, for k, v in data.items(): data_imprint.update({k: v}) - print("_ data_imprint: {}".format(data_imprint)) lib.set_timeline_item_pype_tag(timeline_item, data_imprint) return timeline_item diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index da5e649576..b4c03d6809 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -1,6 +1,5 @@ import re import uuid - import qargparse from qtpy import QtWidgets, QtCore @@ -393,16 +392,15 @@ class ClipLoader: asset_name = self.context["representation"]["context"]["asset"] self.data["assetData"] = get_current_project_asset(asset_name)["data"] - def load(self): - # create project bin for the media to be imported into - self.active_bin = lib.create_bin(self.data["binPath"]) - + def _get_frame_data(self): # create mediaItem in active project bin # create clip media - - media_pool_item = lib.create_media_pool_item( - self.data["path"], self.active_bin) - _clip_property = media_pool_item.GetClipProperty + frame_start = self.data["versionData"].get("frameStart") + frame_end = self.data["versionData"].get("frameEnd") + if frame_start is None: + frame_start = int(self.data["assetData"]["frameStart"]) + if frame_end is None: + frame_end = int(self.data["assetData"]["frameEnd"]) # get handles handle_start = self.data["versionData"].get("handleStart") @@ -412,6 +410,26 @@ class ClipLoader: if handle_end is None: handle_end = int(self.data["assetData"]["handleEnd"]) + return frame_start, frame_end, handle_start, handle_end + + def load(self): + # create project bin for the media to be imported into + self.active_bin = lib.create_bin(self.data["binPath"]) + + frame_start, frame_end, handle_start, handle_end = \ + self._get_frame_data() + + media_pool_item = lib.create_media_pool_item( + self.data["path"], + frame_start, + frame_end, + handle_start, + handle_end, + self.active_bin + ) + _clip_property = media_pool_item.GetClipProperty + + source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) @@ -435,10 +453,19 @@ class ClipLoader: # create project bin for the media to be imported into self.active_bin = lib.create_bin(self.data["binPath"]) + frame_start, frame_end, handle_start, handle_end = \ + self._get_frame_data() + # create mediaItem in active project bin # create clip media media_pool_item = lib.create_media_pool_item( - self.data["path"], self.active_bin) + self.data["path"], + frame_start, + frame_end, + handle_start, + handle_end, + self.active_bin + ) _clip_property = media_pool_item.GetClipProperty source_in = int(_clip_property("Start")) From b2588636e9970d94a6537a2ac4a735a03978ee9c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 Oct 2023 15:11:19 +0200 Subject: [PATCH 11/28] add removing of media pool item for clip remove. no way to remove timeline item so they stay offline at timeline --- openpype/hosts/resolve/api/lib.py | 6 ++++++ openpype/hosts/resolve/plugins/load/load_clip.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 8564a24ac1..5d80866e6a 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -184,6 +184,12 @@ def create_bin(name: str, root: object = None) -> object: return media_pool.GetCurrentFolder() +def remove_media_pool_item(media_pool_item: object) -> bool: + print(media_pool_item) + media_pool = get_current_project().GetMediaPool() + return media_pool.DeleteClips([media_pool_item]) + + def create_media_pool_item( fpath: str, frame_start: int, diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index eea44a3726..fd181bae41 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -163,3 +163,10 @@ class LoadClip(plugin.TimelineItemLoader): timeline_item.SetClipColor(cls.clip_color_last) else: timeline_item.SetClipColor(cls.clip_color) + + def remove(self, container): + namespace = container['namespace'] + timeline_item = lib.get_pype_timeline_item_by_name(namespace) + take_mp_item = timeline_item.GetMediaPoolItem() + + lib.remove_media_pool_item(take_mp_item) From c7df127becf48474494f59087900c4aceaa39e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 6 Oct 2023 15:19:27 +0200 Subject: [PATCH 12/28] Update openpype/hosts/resolve/api/lib.py Co-authored-by: Roy Nieterau --- openpype/hosts/resolve/api/lib.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 5d80866e6a..c3ab1a263b 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -243,9 +243,7 @@ def create_media_pool_item( # iterate all files and check if they exists # if not then remove them from list - for file in files[:]: - if not os.path.exists(file): - files.remove(file) + files = [f for f in files if os.path.exists(f)] # add all data in folder to media pool media_pool_items = media_pool.ImportMedia(files) From 446ee7983113c5e36578ab9650d99d81a566a1a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 6 Oct 2023 15:19:35 +0200 Subject: [PATCH 13/28] Update openpype/hosts/resolve/api/lib.py Co-authored-by: Roy Nieterau --- openpype/hosts/resolve/api/lib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index c3ab1a263b..4d186e199d 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -241,8 +241,7 @@ def create_media_pool_item( for file in glob.glob(os.path.join(dir_path, base_name)): files.append(file) - # iterate all files and check if they exists - # if not then remove them from list + # keep only existing files files = [f for f in files if os.path.exists(f)] # add all data in folder to media pool From d4d48aacf894ac1e893b97d4c4a2c6b749c201e1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 Oct 2023 15:30:44 +0200 Subject: [PATCH 14/28] removing debugging print. --- openpype/hosts/resolve/api/lib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 5d80866e6a..0f24a71cff 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -185,7 +185,6 @@ def create_bin(name: str, root: object = None) -> object: def remove_media_pool_item(media_pool_item: object) -> bool: - print(media_pool_item) media_pool = get_current_project().GetMediaPool() return media_pool.DeleteClips([media_pool_item]) From c51ed6409c27b017c357a0ccf91016103b6850d1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 Oct 2023 15:51:47 +0200 Subject: [PATCH 15/28] removing also timeline item --- openpype/hosts/resolve/plugins/load/load_clip.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index fd181bae41..e9e83ad05d 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -168,5 +168,9 @@ class LoadClip(plugin.TimelineItemLoader): namespace = container['namespace'] timeline_item = lib.get_pype_timeline_item_by_name(namespace) take_mp_item = timeline_item.GetMediaPoolItem() + timeline = lib.get_current_timeline() + + if timeline.DeleteClips is not None: + timeline.DeleteClips([timeline_item]) lib.remove_media_pool_item(take_mp_item) From e03fe24a07126403b384f5cbae18653d55111356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 6 Oct 2023 18:18:28 +0200 Subject: [PATCH 16/28] Update openpype/hosts/resolve/plugins/load/load_clip.py Co-authored-by: Roy Nieterau --- openpype/hosts/resolve/plugins/load/load_clip.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index e9e83ad05d..799b85ea7f 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -170,6 +170,9 @@ class LoadClip(plugin.TimelineItemLoader): take_mp_item = timeline_item.GetMediaPoolItem() timeline = lib.get_current_timeline() + # DeleteClips function was added in Resolve 18.5+ + # by checking None we can detect whether the + # function exists in Resolve if timeline.DeleteClips is not None: timeline.DeleteClips([timeline_item]) From 2932debbaf959df7f54856c88c0757ac14d5aa78 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 19:45:23 +0200 Subject: [PATCH 17/28] Cleanup + fix updating/remove logic - Use container `_timeline_item` to ensure we act on the expected timeline item - otherwise `lib.get_pype_timeline_item_by_name` can take the wrong one if the same subset is loaded more than once which made update/remove actually pick an unexpected timeline item. - On update, remove media pool item if previous version now has no usage - On remove, only remove media pool item if it has no usage - Don't duplicate logic to define version data to put in tag data, now uses a `get_tag_data` method - Don't create a `fake context` but use the `get_representation_context` to get the context on load to ensure whatever uses it has the correct context. --- .../hosts/resolve/plugins/load/load_clip.py | 106 +++++++----------- 1 file changed, 42 insertions(+), 64 deletions(-) diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index e9e83ad05d..8c702a4dfc 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -1,12 +1,7 @@ -from copy import deepcopy - -from openpype.client import ( - get_version_by_id, - get_last_version_by_subset_id, -) -# from openpype.hosts import resolve +from openpype.client import get_last_version_by_subset_id from openpype.pipeline import ( get_representation_path, + get_representation_context, get_current_project_name, ) from openpype.hosts.resolve.api import lib, plugin @@ -53,37 +48,11 @@ class LoadClip(plugin.TimelineItemLoader): timeline_item = plugin.ClipLoader( self, context, path, **options).load() namespace = namespace or timeline_item.GetName() - version = context['version'] - version_data = version.get("data", {}) - version_name = version.get("name", None) - colorspace = version_data.get("colorspace", None) - object_name = "{}_{}".format(name, namespace) - - # add additional metadata from the version to imprint Avalon knob - add_keys = [ - "frameStart", "frameEnd", "source", "author", - "fps", "handleStart", "handleEnd" - ] - - # move all version data keys to tag data - data_imprint = {} - for key in add_keys: - data_imprint.update({ - key: version_data.get(key, str(None)) - }) - - # add variables related to version context - data_imprint.update({ - "version": version_name, - "colorspace": colorspace, - "objectName": object_name - }) # update color of clip regarding the version order - self.set_item_color(timeline_item, version) - - self.log.info("Loader done: `{}`".format(name)) + self.set_item_color(timeline_item, version=context["version"]) + data_imprint = self.get_tag_data(context, name, namespace) return containerise( timeline_item, name, namespace, context, @@ -97,53 +66,60 @@ class LoadClip(plugin.TimelineItemLoader): """ Updating previously loaded clips """ - # load clip to timeline and get main variables - context = deepcopy(representation["context"]) - context.update({"representation": representation}) + context = get_representation_context(representation) name = container['name'] namespace = container['namespace'] - timeline_item = lib.get_pype_timeline_item_by_name(namespace) + timeline_item = container["_timeline_item"] - project_name = get_current_project_name() - version = get_version_by_id(project_name, representation["parent"]) + media_pool_item = timeline_item.GetMediaPoolItem() + + path = get_representation_path(representation) + loader = plugin.ClipLoader(self, context, path) + timeline_item = loader.update(timeline_item) + + # update color of clip regarding the version order + self.set_item_color(timeline_item, version=context["version"]) + + # if original media pool item has no remaining usages left + # remove it from the media pool + if int(media_pool_item.GetClipProperty("Usage")) == 0: + lib.remove_media_pool_item(media_pool_item) + + data_imprint = self.get_tag_data(context, name, namespace) + return update_container(timeline_item, data_imprint) + + def get_tag_data(self, context, name, namespace): + """Return data to be imprinted on the timeline item marker""" + + representation = context["representation"] + version = context['version'] version_data = version.get("data", {}) version_name = version.get("name", None) colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) - path = get_representation_path(representation) - - context["version"] = {"data": version_data} - loader = plugin.ClipLoader(self, context, path) - timeline_item = loader.update(timeline_item) # add additional metadata from the version to imprint Avalon knob - add_keys = [ + # move all version data keys to tag data + add_version_data_keys = [ "frameStart", "frameEnd", "source", "author", "fps", "handleStart", "handleEnd" ] - - # move all version data keys to tag data - data_imprint = {} - for key in add_keys: - data_imprint.update({ - key: version_data.get(key, str(None)) - }) + data = { + key: version_data.get(key, "None") for key in add_version_data_keys + } # add variables related to version context - data_imprint.update({ + data.update({ "representation": str(representation["_id"]), "version": version_name, "colorspace": colorspace, "objectName": object_name }) - - # update color of clip regarding the version order - self.set_item_color(timeline_item, version) - - return update_container(timeline_item, data_imprint) + return data @classmethod def set_item_color(cls, timeline_item, version): + """Color timeline item based on whether it is outdated or latest""" # define version name version_name = version.get("name", None) # get all versions in list @@ -165,12 +141,14 @@ class LoadClip(plugin.TimelineItemLoader): timeline_item.SetClipColor(cls.clip_color) def remove(self, container): - namespace = container['namespace'] - timeline_item = lib.get_pype_timeline_item_by_name(namespace) - take_mp_item = timeline_item.GetMediaPoolItem() + timeline_item = container["_timeline_item"] + media_pool_item = timeline_item.GetMediaPoolItem() timeline = lib.get_current_timeline() if timeline.DeleteClips is not None: timeline.DeleteClips([timeline_item]) - lib.remove_media_pool_item(take_mp_item) + # if media pool item has no remaining usages left + # remove it from the media pool + if int(media_pool_item.GetClipProperty("Usage")) == 0: + lib.remove_media_pool_item(media_pool_item) From bb74f9b3ba7a9dea03dcd8451d7ec9d12ffbe92b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 19:45:49 +0200 Subject: [PATCH 18/28] Cosmetics --- openpype/hosts/resolve/api/pipeline.py | 3 +-- openpype/hosts/resolve/api/plugin.py | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py index 28be387ce9..93dec300fb 100644 --- a/openpype/hosts/resolve/api/pipeline.py +++ b/openpype/hosts/resolve/api/pipeline.py @@ -127,8 +127,7 @@ def containerise(timeline_item, }) if data: - for k, v in data.items(): - data_imprint.update({k: v}) + data_imprint.update(data) lib.set_timeline_item_pype_tag(timeline_item, data_imprint) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index b4c03d6809..85245a5d12 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -338,8 +338,6 @@ class ClipLoader: else: self.active_timeline = lib.get_current_timeline() - - def _populate_data(self): """ Gets context and convert it to self.data data structure: @@ -429,7 +427,6 @@ class ClipLoader: ) _clip_property = media_pool_item.GetClipProperty - source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) From 708aef05375ae41109e260797ff23fe9f9aa4097 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 20:02:10 +0200 Subject: [PATCH 19/28] Code cosmetics --- openpype/hosts/resolve/api/lib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 3139c32093..942caca72a 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -580,11 +580,11 @@ def set_pype_marker(timeline_item, tag_data): def get_pype_marker(timeline_item): timeline_item_markers = timeline_item.GetMarkers() - for marker_frame in timeline_item_markers: - note = timeline_item_markers[marker_frame]["note"] - color = timeline_item_markers[marker_frame]["color"] - name = timeline_item_markers[marker_frame]["name"] + for marker_frame, marker in timeline_item_markers.items(): + color = marker["color"] + name = marker["name"] if name == self.pype_marker_name and color == self.pype_marker_color: + note = marker["note"] self.temp_marker_frame = marker_frame return json.loads(note) From 26bbb702df9eaa9c86117e6dbe7654268f4e590a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 20:04:21 +0200 Subject: [PATCH 20/28] Implement legacy logic where we remove the pype tag in older versions of Resolve - Unfortunately due to API limitations cannot remove the TimelineItem from the Timeline in old versions of Resolve --- openpype/hosts/resolve/plugins/load/load_clip.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index a17db376be..5e81441332 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -150,6 +150,15 @@ class LoadClip(plugin.TimelineItemLoader): # function exists in Resolve if timeline.DeleteClips is not None: timeline.DeleteClips([timeline_item]) + else: + # Resolve versions older than 18.5 can't delete clips via API + # so all we can do is just remove the pype marker to 'untag' it + if lib.get_pype_marker(timeline_item): + # Note: We must call `get_pype_marker` because + # `delete_pype_marker` uses a global variable set by + # `get_pype_marker` to delete the right marker + # TODO: Improve code to avoid the global `temp_marker_frame` + lib.delete_pype_marker(timeline_item) # if media pool item has no remaining usages left # remove it from the media pool From 0a71b89ddd857676d1561c3cdbfeb690ebae6103 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Oct 2023 13:57:53 +0200 Subject: [PATCH 21/28] global: adding abstracted `get_representation_files` --- openpype/pipeline/__init__.py | 2 ++ openpype/pipeline/load/__init__.py | 2 ++ openpype/pipeline/load/utils.py | 50 ++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index 8f370d389b..ca2a6bcf2c 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -48,6 +48,7 @@ from .load import ( loaders_from_representation, get_representation_path, get_representation_context, + get_representation_files, get_repres_contexts, ) @@ -152,6 +153,7 @@ __all__ = ( "loaders_from_representation", "get_representation_path", "get_representation_context", + "get_representation_files", "get_repres_contexts", # --- Publish --- diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index 7320a9f0e8..c07388fd45 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -11,6 +11,7 @@ from .utils import ( get_contexts_for_repre_docs, get_subset_contexts, get_representation_context, + get_representation_files, load_with_repre_context, load_with_subset_context, @@ -64,6 +65,7 @@ __all__ = ( "get_contexts_for_repre_docs", "get_subset_contexts", "get_representation_context", + "get_representation_files", "load_with_repre_context", "load_with_subset_context", diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index b10d6032b3..81175a8261 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -1,4 +1,6 @@ import os +import re +import glob import platform import copy import getpass @@ -286,6 +288,54 @@ def get_representation_context(representation): return context +def get_representation_files(context, filepath): + """Return list of files for representation. + + Args: + representation (dict): Representation document. + filepath (str): Filepath of the representation. + + Returns: + list[str]: List of files for representation. + """ + version = context["version"] + frame_start = version["data"]["frameStart"] + frame_end = version["data"]["frameEnd"] + handle_start = version["data"]["handleStart"] + handle_end = version["data"]["handleEnd"] + + first_frame = frame_start - handle_start + last_frame = frame_end + handle_end + dir_path = os.path.dirname(filepath) + base_name = os.path.basename(filepath) + + # prepare glob pattern for searching + padding = len(str(last_frame)) + str_first_frame = str(first_frame).zfill(padding) + + # convert str_first_frame to glob pattern + # replace all digits with `?` and all other chars with `[char]` + # example: `0001` -> `????` + glob_pattern = re.sub(r"\d", "?", str_first_frame) + + # in filename replace number with glob pattern + # example: `filename.0001.exr` -> `filename.????.exr` + base_name = re.sub(str_first_frame, glob_pattern, base_name) + + files = [] + # get all files in folder + for file in glob.glob(os.path.join(dir_path, base_name)): + files.append(file) + + # keep only existing files + files = [f for f in files if os.path.exists(f)] + + # sort files by frame number + files.sort(key=lambda f: int(re.findall(r"\d+", f)[-1])) + + return files + + def load_with_repre_context( Loader, repre_context, namespace=None, name=None, options=None, **kwargs ): From 26b2817a7067cf74d8754579236292fa22752e86 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Oct 2023 13:58:19 +0200 Subject: [PATCH 22/28] refactor loading for abstracted `get_representation_files` --- openpype/hosts/resolve/api/lib.py | 40 ++-------- openpype/hosts/resolve/api/plugin.py | 75 +++++-------------- .../hosts/resolve/plugins/load/load_clip.py | 15 ++-- 3 files changed, 35 insertions(+), 95 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 942caca72a..70a7680d8d 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -190,11 +190,7 @@ def remove_media_pool_item(media_pool_item: object) -> bool: def create_media_pool_item( - fpath: str, - frame_start: int, - frame_end: int, - handle_start: int, - handle_end: int, + files: list, root: object = None, ) -> object: """ @@ -212,49 +208,23 @@ def create_media_pool_item( root_bin = root or media_pool.GetRootFolder() # try to search in bin if the clip does not exist - existing_mpi = get_media_pool_item(fpath, root_bin) + existing_mpi = get_media_pool_item(files[0], root_bin) if existing_mpi: return existing_mpi - files = [] - first_frame = frame_start - handle_start - last_frame = frame_end + handle_end - dir_path = os.path.dirname(fpath) - base_name = os.path.basename(fpath) - - # prepare glob pattern for searching - padding = len(str(last_frame)) - str_first_frame = str(first_frame).zfill(padding) - - # convert str_first_frame to glob pattern - # replace all digits with `?` and all other chars with `[char]` - # example: `0001` -> `????` - glob_pattern = re.sub(r"\d", "?", str_first_frame) - - # in filename replace number with glob pattern - # example: `filename.0001.exr` -> `filename.????.exr` - base_name = re.sub(str_first_frame, glob_pattern, base_name) - - # get all files in folder - for file in glob.glob(os.path.join(dir_path, base_name)): - files.append(file) - - # keep only existing files - files = [f for f in files if os.path.exists(f)] - # add all data in folder to media pool media_pool_items = media_pool.ImportMedia(files) return media_pool_items.pop() if media_pool_items else False -def get_media_pool_item(fpath, root: object = None) -> object: +def get_media_pool_item(filepath, root: object = None) -> object: """ Return clip if found in folder with use of input file path. Args: - fpath (str): absolute path to a file + filepath (str): absolute path to a file root (resolve.Folder)[optional]: root folder / bin object Returns: @@ -262,7 +232,7 @@ def get_media_pool_item(fpath, root: object = None) -> object: """ media_pool = get_current_project().GetMediaPool() root = root or media_pool.GetRootFolder() - fname = os.path.basename(fpath) + fname = os.path.basename(filepath) for _mpi in root.GetClipList(): _mpi_name = _mpi.GetClipProperty("File Name") diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 85245a5d12..b1d6b595c1 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -290,7 +290,7 @@ class ClipLoader: active_bin = None data = dict() - def __init__(self, loader_obj, context, path, **options): + def __init__(self, loader_obj, context, **options): """ Initialize object Arguments: @@ -303,7 +303,6 @@ class ClipLoader: self.__dict__.update(loader_obj.__dict__) self.context = context self.active_project = lib.get_current_project() - self.fname = path # try to get value from options or evaluate key value for `handles` self.with_handles = options.get("handles") or bool( @@ -343,37 +342,29 @@ class ClipLoader: data structure: { "name": "assetName_subsetName_representationName" - "path": "path/to/file/created/by/get_repr..", "binPath": "projectBinPath", } """ # create name - repr = self.context["representation"] - repr_cntx = repr["context"] - asset = str(repr_cntx["asset"]) - subset = str(repr_cntx["subset"]) - representation = str(repr_cntx["representation"]) + representation = self.context["representation"] + representation_context = representation["context"] + asset = str(representation_context["asset"]) + subset = str(representation_context["subset"]) + representation_name = str(representation_context["representation"]) self.data["clip_name"] = "_".join([ asset, subset, - representation + representation_name ]) self.data["versionData"] = self.context["version"]["data"] - # gets file path - file = self.fname - if not file: - repr_id = repr["_id"] - print( - "Representation id `{}` is failing to load".format(repr_id)) - return None - self.data["path"] = file.replace("\\", "/") + self.data["timeline_basename"] = "timeline_{}_{}".format( - subset, representation) + subset, representation_name) # solve project bin structure path hierarchy = str("/".join(( "Loader", - repr_cntx["hierarchy"].replace("\\", "/"), + representation_context["hierarchy"].replace("\\", "/"), asset ))) @@ -390,39 +381,20 @@ class ClipLoader: asset_name = self.context["representation"]["context"]["asset"] self.data["assetData"] = get_current_project_asset(asset_name)["data"] - def _get_frame_data(self): - # create mediaItem in active project bin - # create clip media - frame_start = self.data["versionData"].get("frameStart") - frame_end = self.data["versionData"].get("frameEnd") - if frame_start is None: - frame_start = int(self.data["assetData"]["frameStart"]) - if frame_end is None: - frame_end = int(self.data["assetData"]["frameEnd"]) - # get handles - handle_start = self.data["versionData"].get("handleStart") - handle_end = self.data["versionData"].get("handleEnd") - if handle_start is None: - handle_start = int(self.data["assetData"]["handleStart"]) - if handle_end is None: - handle_end = int(self.data["assetData"]["handleEnd"]) + def load(self, files): + """Load clip into timeline - return frame_start, frame_end, handle_start, handle_end - - def load(self): + Arguments: + files (list): list of files to load into timeline + """ # create project bin for the media to be imported into self.active_bin = lib.create_bin(self.data["binPath"]) - - frame_start, frame_end, handle_start, handle_end = \ - self._get_frame_data() + handle_start = self.data["versionData"].get("handleStart", 0) + handle_end = self.data["versionData"].get("handleEnd", 0) media_pool_item = lib.create_media_pool_item( - self.data["path"], - frame_start, - frame_end, - handle_start, - handle_end, + files, self.active_bin ) _clip_property = media_pool_item.GetClipProperty @@ -446,21 +418,14 @@ class ClipLoader: print("Loading clips: `{}`".format(self.data["clip_name"])) return timeline_item - def update(self, timeline_item): + def update(self, timeline_item, files): # create project bin for the media to be imported into self.active_bin = lib.create_bin(self.data["binPath"]) - frame_start, frame_end, handle_start, handle_end = \ - self._get_frame_data() - # create mediaItem in active project bin # create clip media media_pool_item = lib.create_media_pool_item( - self.data["path"], - frame_start, - frame_end, - handle_start, - handle_end, + files, self.active_bin ) _clip_property = media_pool_item.GetClipProperty diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index 5e81441332..35a6b97eea 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -3,6 +3,7 @@ from openpype.pipeline import ( get_representation_path, get_representation_context, get_current_project_name, + get_representation_files ) from openpype.hosts.resolve.api import lib, plugin from openpype.hosts.resolve.api.pipeline import ( @@ -44,9 +45,11 @@ class LoadClip(plugin.TimelineItemLoader): def load(self, context, name, namespace, options): # load clip to timeline and get main variables - path = self.filepath_from_context(context) + filepath = self.filepath_from_context(context) + files = get_representation_files(context, filepath) + timeline_item = plugin.ClipLoader( - self, context, path, **options).load() + self, context, **options).load(files) namespace = namespace or timeline_item.GetName() # update color of clip regarding the version order @@ -73,9 +76,11 @@ class LoadClip(plugin.TimelineItemLoader): media_pool_item = timeline_item.GetMediaPoolItem() - path = get_representation_path(representation) - loader = plugin.ClipLoader(self, context, path) - timeline_item = loader.update(timeline_item) + filepath = get_representation_path(representation) + files = get_representation_files(context, filepath) + + loader = plugin.ClipLoader(self, context) + timeline_item = loader.update(timeline_item, files) # update color of clip regarding the version order self.set_item_color(timeline_item, version=context["version"]) From 366bfb24354f62896db7f34baba80d28e54d431d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Oct 2023 15:30:37 +0200 Subject: [PATCH 23/28] hound --- openpype/hosts/resolve/api/lib.py | 1 - openpype/hosts/resolve/api/plugin.py | 1 - 2 files changed, 2 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 70a7680d8d..4066dd34fd 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -2,7 +2,6 @@ import sys import json import re import os -import glob import contextlib from opentimelineio import opentime diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index b1d6b595c1..f3a65034fb 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -381,7 +381,6 @@ class ClipLoader: asset_name = self.context["representation"]["context"]["asset"] self.data["assetData"] = get_current_project_asset(asset_name)["data"] - def load(self, files): """Load clip into timeline From 92a256d7571afa6023ab95fdb54dfea38754d9f0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Oct 2023 15:41:03 +0200 Subject: [PATCH 24/28] false docstring --- openpype/pipeline/load/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 81175a8261..5193eaa86e 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -292,7 +292,7 @@ def get_representation_files(context, filepath): """Return list of files for representation. Args: - representation (dict): Representation document. + context (dict): The full loading context. filepath (str): Filepath of the representation. Returns: From 32000bd160657a784e9b74b171901d228f80a9ec Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 Oct 2023 22:32:22 +0200 Subject: [PATCH 25/28] reverting global abstraction --- openpype/pipeline/__init__.py | 2 -- openpype/pipeline/load/__init__.py | 2 -- openpype/pipeline/load/utils.py | 50 ------------------------------ 3 files changed, 54 deletions(-) diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index ca2a6bcf2c..8f370d389b 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -48,7 +48,6 @@ from .load import ( loaders_from_representation, get_representation_path, get_representation_context, - get_representation_files, get_repres_contexts, ) @@ -153,7 +152,6 @@ __all__ = ( "loaders_from_representation", "get_representation_path", "get_representation_context", - "get_representation_files", "get_repres_contexts", # --- Publish --- diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index c07388fd45..7320a9f0e8 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -11,7 +11,6 @@ from .utils import ( get_contexts_for_repre_docs, get_subset_contexts, get_representation_context, - get_representation_files, load_with_repre_context, load_with_subset_context, @@ -65,7 +64,6 @@ __all__ = ( "get_contexts_for_repre_docs", "get_subset_contexts", "get_representation_context", - "get_representation_files", "load_with_repre_context", "load_with_subset_context", diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 5193eaa86e..b10d6032b3 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -1,6 +1,4 @@ import os -import re -import glob import platform import copy import getpass @@ -288,54 +286,6 @@ def get_representation_context(representation): return context -def get_representation_files(context, filepath): - """Return list of files for representation. - - Args: - context (dict): The full loading context. - filepath (str): Filepath of the representation. - - Returns: - list[str]: List of files for representation. - """ - version = context["version"] - frame_start = version["data"]["frameStart"] - frame_end = version["data"]["frameEnd"] - handle_start = version["data"]["handleStart"] - handle_end = version["data"]["handleEnd"] - - first_frame = frame_start - handle_start - last_frame = frame_end + handle_end - dir_path = os.path.dirname(filepath) - base_name = os.path.basename(filepath) - - # prepare glob pattern for searching - padding = len(str(last_frame)) - str_first_frame = str(first_frame).zfill(padding) - - # convert str_first_frame to glob pattern - # replace all digits with `?` and all other chars with `[char]` - # example: `0001` -> `????` - glob_pattern = re.sub(r"\d", "?", str_first_frame) - - # in filename replace number with glob pattern - # example: `filename.0001.exr` -> `filename.????.exr` - base_name = re.sub(str_first_frame, glob_pattern, base_name) - - files = [] - # get all files in folder - for file in glob.glob(os.path.join(dir_path, base_name)): - files.append(file) - - # keep only existing files - files = [f for f in files if os.path.exists(f)] - - # sort files by frame number - files.sort(key=lambda f: int(re.findall(r"\d+", f)[-1])) - - return files - - def load_with_repre_context( Loader, repre_context, namespace=None, name=None, options=None, **kwargs ): From 9145072d514d0ef33edd49a8245d412aa73a6379 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 Oct 2023 22:50:11 +0200 Subject: [PATCH 26/28] resolve: get representation files from host api plugin and as suggested here https://github.com/ynput/OpenPype/pull/5673#discussion_r1350315699 --- openpype/hosts/resolve/api/plugin.py | 10 ++++++++++ openpype/hosts/resolve/plugins/load/load_clip.py | 10 +++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index f3a65034fb..a0dba6fd05 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -8,6 +8,7 @@ from openpype.pipeline.context_tools import get_current_project_asset from openpype.pipeline import ( LegacyCreator, LoaderPlugin, + Anatomy ) from . import lib @@ -825,3 +826,12 @@ class PublishClip: for key in par_split: parent = self._convert_to_entity(key) self.parents.append(parent) + + +def get_representation_files(representation): + anatomy = Anatomy() + files = [] + for file_data in representation["files"]: + path = anatomy.fill_root(file_data["path"]) + files.append(path) + return files diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index 35a6b97eea..d3f83c7f24 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -1,9 +1,7 @@ from openpype.client import get_last_version_by_subset_id from openpype.pipeline import ( - get_representation_path, get_representation_context, - get_current_project_name, - get_representation_files + get_current_project_name ) from openpype.hosts.resolve.api import lib, plugin from openpype.hosts.resolve.api.pipeline import ( @@ -45,8 +43,7 @@ class LoadClip(plugin.TimelineItemLoader): def load(self, context, name, namespace, options): # load clip to timeline and get main variables - filepath = self.filepath_from_context(context) - files = get_representation_files(context, filepath) + files = plugin.get_representation_files(context["representation"]) timeline_item = plugin.ClipLoader( self, context, **options).load(files) @@ -76,8 +73,7 @@ class LoadClip(plugin.TimelineItemLoader): media_pool_item = timeline_item.GetMediaPoolItem() - filepath = get_representation_path(representation) - files = get_representation_files(context, filepath) + files = plugin.get_representation_files(representation) loader = plugin.ClipLoader(self, context) timeline_item = loader.update(timeline_item, files) From 61f381cb5cee14f9f2c85b6db04c9144f9818ac5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 Oct 2023 15:20:51 +0200 Subject: [PATCH 27/28] resolve: make sure of file existence --- openpype/hosts/resolve/api/lib.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 4066dd34fd..37410c9727 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -196,7 +196,7 @@ def create_media_pool_item( Create media pool item. Args: - fpath (str): absolute path to a file + files (list[str]): list of absolute paths to files root (resolve.Folder)[optional]: root folder / bin object Returns: @@ -206,8 +206,13 @@ def create_media_pool_item( media_pool = get_current_project().GetMediaPool() root_bin = root or media_pool.GetRootFolder() + # make sure files list is not empty and first available file exists + filepath = next((f for f in files if os.path.isfile(f)), None) + if not filepath: + raise FileNotFoundError("No file found in input files list") + # try to search in bin if the clip does not exist - existing_mpi = get_media_pool_item(files[0], root_bin) + existing_mpi = get_media_pool_item(filepath, root_bin) if existing_mpi: return existing_mpi From f03be42e9d62882a501303abfbee37b83463c946 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 Oct 2023 15:30:39 +0200 Subject: [PATCH 28/28] resolve: improving key calling from version data --- openpype/hosts/resolve/api/plugin.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index a0dba6fd05..8381f81acb 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -386,12 +386,13 @@ class ClipLoader: """Load clip into timeline Arguments: - files (list): list of files to load into timeline + files (list[str]): list of files to load into timeline """ # create project bin for the media to be imported into self.active_bin = lib.create_bin(self.data["binPath"]) - handle_start = self.data["versionData"].get("handleStart", 0) - handle_end = self.data["versionData"].get("handleEnd", 0) + + handle_start = self.data["versionData"].get("handleStart") or 0 + handle_end = self.data["versionData"].get("handleEnd") or 0 media_pool_item = lib.create_media_pool_item( files,