Merge pull request #5673 from ynput/bugfix/OP-6952_Resolve-broken-version-management-for-loaded-containers

This commit is contained in:
Jakub Ježek 2023-10-17 11:58:24 +02:00 committed by GitHub
commit 581fc5da81
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 168 additions and 322 deletions

View file

@ -125,15 +125,19 @@ def get_any_timeline():
return project.GetTimelineByIndex(1)
def get_new_timeline():
def get_new_timeline(timeline_name: str = None):
"""Get new timeline object.
Arguments:
timeline_name (str): New timeline name.
Returns:
object: resolve.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
@ -179,53 +183,52 @@ 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 remove_media_pool_item(media_pool_item: object) -> bool:
media_pool = get_current_project().GetMediaPool()
return media_pool.DeleteClips([media_pool_item])
def create_media_pool_item(
files: list,
root: object = None,
) -> object:
"""
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:
object: resolve.MediaPoolItem
"""
# get all variables
media_storage = get_media_storage()
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(fpath, root_bin)
existing_mpi = get_media_pool_item(filepath, root_bin)
if existing_mpi:
return existing_mpi
dirname, file = os.path.split(fpath)
_name, ext = os.path.splitext(file)
# add all data in folder to media pool
media_pool_items = media_pool.ImportMedia(files)
# 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:
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:
@ -233,7 +236,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")
@ -277,7 +280,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])
@ -394,14 +396,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
@ -544,12 +554,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"]
print(f"_ marker data: {marker_frame} | {name} | {color} | {note}")
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)
@ -618,7 +627,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)
@ -627,7 +636,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:

View file

@ -127,10 +127,8 @@ def containerise(timeline_item,
})
if data:
for k, v in data.items():
data_imprint.update({k: v})
data_imprint.update(data)
print("_ data_imprint: {}".format(data_imprint))
lib.set_timeline_item_pype_tag(timeline_item, data_imprint)
return timeline_item

View file

@ -1,6 +1,5 @@
import re
import uuid
import qargparse
from qtpy import QtWidgets, QtCore
@ -9,6 +8,7 @@ from openpype.pipeline.context_tools import get_current_project_asset
from openpype.pipeline import (
LegacyCreator,
LoaderPlugin,
Anatomy
)
from . import lib
@ -291,20 +291,19 @@ class ClipLoader:
active_bin = None
data = dict()
def __init__(self, cls, context, path, **options):
def __init__(self, loader_obj, context, **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
# try to get value from options or evaluate key value for `handles`
self.with_handles = options.get("handles") or bool(
@ -319,54 +318,54 @@ 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.data["timeline_basename"],
str(uuid.uuid4())[:8]
)
)
loader_cls.timeline = self.active_timeline
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
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"])
self.data["clip_name"] = "_".join([asset, subset, 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_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_name)
# solve project bin structure path
hierarchy = str("/".join((
"Loader",
repr_cntx["hierarchy"].replace("\\", "/"),
representation_context["hierarchy"].replace("\\", "/"),
asset
)))
@ -383,25 +382,24 @@ class ClipLoader:
asset_name = self.context["representation"]["context"]["asset"]
self.data["assetData"] = get_current_project_asset(asset_name)["data"]
def load(self):
def load(self, files):
"""Load clip into timeline
Arguments:
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"])
# create mediaItem in active project bin
# create clip media
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(
self.data["path"], self.active_bin)
files,
self.active_bin
)
_clip_property = media_pool_item.GetClipProperty
# 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"])
source_in = int(_clip_property("Start"))
source_out = int(_clip_property("End"))
@ -421,14 +419,16 @@ 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"])
# create mediaItem in active project bin
# create clip media
media_pool_item = lib.create_media_pool_item(
self.data["path"], self.active_bin)
files,
self.active_bin
)
_clip_property = media_pool_item.GetClipProperty
source_in = int(_clip_property("Start"))
@ -649,8 +649,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(
@ -829,3 +827,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

View file

@ -1,13 +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_current_project_name,
get_representation_context,
get_current_project_name
)
from openpype.hosts.resolve.api import lib, plugin
from openpype.hosts.resolve.api.pipeline import (
@ -48,48 +42,17 @@ 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)
files = plugin.get_representation_files(context["representation"])
timeline_item = plugin.ClipLoader(
self, context, path, **options).load()
self, context, **options).load(files)
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,
@ -103,53 +66,61 @@ 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_data = lib.get_pype_timeline_item_by_name(namespace)
timeline_item = timeline_item_data["clip"]["item"]
project_name = get_current_project_name()
version = get_version_by_id(project_name, representation["parent"])
timeline_item = container["_timeline_item"]
media_pool_item = timeline_item.GetMediaPoolItem()
files = plugin.get_representation_files(representation)
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"])
# 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
@ -169,3 +140,28 @@ class LoadClip(plugin.TimelineItemLoader):
timeline_item.SetClipColor(cls.clip_color_last)
else:
timeline_item.SetClipColor(cls.clip_color)
def remove(self, container):
timeline_item = container["_timeline_item"]
media_pool_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])
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
if int(media_pool_item.GetClipProperty("Usage")) == 0:
lib.remove_media_pool_item(media_pool_item)

View file

@ -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))

View file

@ -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))

View file

@ -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)

View file

@ -1,5 +0,0 @@
#! python3
from openpype.hosts.resolve.startup import main
if __name__ == "__main__":
main()

View file

@ -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"))