From f7e514ae730fd5abd6b9c9d10146b20d0b9774b3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Mar 2024 12:17:29 +0100 Subject: [PATCH 01/21] Updating Resolve API reference --- ...ld6.txt => RESOLVE_API_v18.6.5-build7.txt} | 89 ++++++++++++++++++- 1 file changed, 85 insertions(+), 4 deletions(-) rename client/ayon_core/hosts/resolve/{RESOLVE_API_v18.5.1-build6.txt => RESOLVE_API_v18.6.5-build7.txt} (92%) diff --git a/client/ayon_core/hosts/resolve/RESOLVE_API_v18.5.1-build6.txt b/client/ayon_core/hosts/resolve/RESOLVE_API_v18.6.5-build7.txt similarity index 92% rename from client/ayon_core/hosts/resolve/RESOLVE_API_v18.5.1-build6.txt rename to client/ayon_core/hosts/resolve/RESOLVE_API_v18.6.5-build7.txt index 7d1d6edf61..52858b4eb7 100644 --- a/client/ayon_core/hosts/resolve/RESOLVE_API_v18.5.1-build6.txt +++ b/client/ayon_core/hosts/resolve/RESOLVE_API_v18.6.5-build7.txt @@ -1,4 +1,4 @@ -Updated as of 26 May 2023 +Updated as of 18 December 2023 ---------------------------- In this package, you will find a brief introduction to the Scripting API for DaVinci Resolve Studio. Apart from this README.txt file, this package contains folders containing the basic import modules for scripting access (DaVinciResolve.py) and some representative examples. @@ -101,6 +101,10 @@ Resolve SaveLayoutPreset(presetName) --> Bool # Saves current UI layout as a preset named 'presetName'. ImportLayoutPreset(presetFilePath, presetName) --> Bool # Imports preset from path 'presetFilePath'. The optional argument 'presetName' specifies how the preset shall be named. If not specified, the preset is named based on the filename. Quit() --> None # Quits the Resolve App. + ImportRenderPreset(presetPath) --> Bool # Import a preset from presetPath (string) and set it as current preset for rendering. + ExportRenderPreset(presetName, exportPath) --> Bool # Export a preset to a given path (string) if presetName(string) exists. + ImportBurnInPreset(presetPath) --> Bool # Import a data burn in preset from a given presetPath (string) + ExportBurnInPreset(presetName, exportPath) --> Bool # Export a data burn in preset to a given path (string) if presetName (string) exists. ProjectManager ArchiveProject(projectName, @@ -131,6 +135,14 @@ ProjectManager # 'DbType': 'Disk' or 'PostgreSQL' (string) # 'DbName': database name (string) # 'IpAddress': IP address of the PostgreSQL server (string, optional key - defaults to '127.0.0.1') + CreateCloudProject({cloudSettings}) --> Project # Creates and returns a cloud project. + # '{cloudSettings}': Check 'Cloud Projects Settings' subsection below for more information. + ImportCloudProject(filePath, {cloudSettings}) --> Bool # Returns True if import cloud project is successful; False otherwise + # 'filePath': String; filePath of file to import + # '{cloudSettings}': Check 'Cloud Projects Settings' subsection below for more information. + RestoreCloudProject(folderPath, {cloudSettings}) --> Bool # Returns True if restore cloud project is successful; False otherwise + # 'folderPath': String; path of folder to restore + # '{cloudSettings}': Check 'Cloud Projects Settings' subsection below for more information. Project GetMediaPool() --> MediaPool # Returns the Media Pool object. @@ -198,7 +210,7 @@ MediaPool CreateTimelineFromClips(name, clip1, clip2,...) --> Timeline # Creates new timeline with specified name, and appends the specified MediaPoolItem objects. CreateTimelineFromClips(name, [clips]) --> Timeline # Creates new timeline with specified name, and appends the specified MediaPoolItem objects. CreateTimelineFromClips(name, [{clipInfo}]) --> Timeline # Creates new timeline with specified name, appending the list of clipInfos specified as a dict of "mediaPoolItem", "startFrame" (int), "endFrame" (int), "recordFrame" (int). - ImportTimelineFromFile(filePath, {importOptions}) --> Timeline # Creates timeline based on parameters within given file (AAF/EDL/XML/FCPXML/DRT/ADL) and optional importOptions dict, with support for the keys: + ImportTimelineFromFile(filePath, {importOptions}) --> Timeline # Creates timeline based on parameters within given file (AAF/EDL/XML/FCPXML/DRT/ADL/OTIO) and optional importOptions dict, with support for the keys: # "timelineName": string, specifies the name of the timeline to be created. Not valid for DRT import # "importSourceClips": Bool, specifies whether source clips should be imported, True by default. Not valid for DRT import # "sourceClipsPath": string, specifies a filesystem path to search for source clips if the media is inaccessible in their original path and if "importSourceClips" is True @@ -225,6 +237,8 @@ MediaPool ExportMetadata(fileName, [clips]) --> Bool # Exports metadata of specified clips to 'fileName' in CSV format. # If no clips are specified, all clips from media pool will be used. GetUniqueId() --> string # Returns a unique ID for the media pool + CreateStereoClip(LeftMediaPoolItem, + RightMediaPoolItem) --> MediaPoolItem # Takes in two existing media pool items and creates a new 3D stereoscopic media pool entry replacing the input media in the media pool. Folder GetClipList() --> [clips...] # Returns a list of clips (items) within the folder. @@ -233,6 +247,8 @@ Folder GetIsFolderStale() --> bool # Returns true if folder is stale in collaboration mode, false otherwise GetUniqueId() --> string # Returns a unique ID for the media pool folder Export(filePath) --> bool # Returns true if export of DRB folder to filePath is successful, false otherwise + TranscribeAudio() --> Bool # Transcribes audio of the MediaPoolItems within the folder and nested folders. Returns True if successful; False otherwise + ClearTranscription() --> Bool # Clears audio transcription of the MediaPoolItems within the folder and nested folders. Returns True if successful; False otherwise. MediaPoolItem GetName() --> string # Returns the clip name. @@ -340,8 +356,11 @@ Timeline GrabStill() --> galleryStill # Grabs still from the current video clip. Returns a GalleryStill object. GrabAllStills(stillFrameSource) --> [galleryStill] # Grabs stills from all the clips of the timeline at 'stillFrameSource' (1 - First frame, 2 - Middle frame). Returns the list of GalleryStill objects. GetUniqueId() --> string # Returns a unique ID for the timeline - CreateSubtitlesFromAudio() --> Bool # Creates subtitles from audio for the timeline. Returns True on success, False otherwise. + CreateSubtitlesFromAudio({autoCaptionSettings}) --> Bool # Creates subtitles from audio for the timeline. + # Takes in optional dictionary {autoCaptionSettings}. Check 'Auto Caption Settings' subsection below for more information. + # Returns True on success, False otherwise. DetectSceneCuts() --> Bool # Detects and makes scene cuts along the timeline. Returns True if successful, False otherwise. + ConvertTimelineToStereo() --> Bool # Converts timeline to stereo. Returns True if successful; False otherwise. TimelineItem GetName() --> string # Returns the item name. @@ -428,7 +447,8 @@ GalleryStillAlbum GetStills() --> [galleryStill] # Returns the list of GalleryStill objects in the album. GetLabel(galleryStill) --> string # Returns the label of the galleryStill. SetLabel(galleryStill, label) --> Bool # Sets the new 'label' to GalleryStill object 'galleryStill'. - ExportStills([galleryStill], folderPath, filePrefix, format) --> Bool # Exports list of GalleryStill objects '[galleryStill]' to directory 'folderPath', with filename prefix 'filePrefix', using file format 'format' (supported formats: dpx, cin, tif, jpg, png, ppm, bmp, xpm). + ImportStills([filePaths]) --> Bool # Imports GalleryStill from each filePath in [filePaths] list. True if at least one still is imported successfully. False otherwise. + ExportStills([galleryStill], folderPath, filePrefix, format) --> Bool # Exports list of GalleryStill objects '[galleryStill]' to directory 'folderPath', with filename prefix 'filePrefix', using file format 'format' (supported formats: dpx, cin, tif, jpg, png, ppm, bmp, xpm, drx). DeleteStills([galleryStill]) --> Bool # Deletes specified list of GalleryStill objects '[galleryStill]'. GalleryStill # This class does not provide any API functions but the object type is used by functions in other classes. @@ -439,6 +459,24 @@ Beside primitive data types, Resolve's Python API mainly uses list and dict data As Lua does not support list and dict data structures, the Lua API implements "list" as a table with indices, e.g. { [1] = listValue1, [2] = listValue2, ... }. Similarly the Lua API implements "dict" as a table with the dictionary key as first element, e.g. { [dictKey1] = dictValue1, [dictKey2] = dictValue2, ... }. +Cloud Projects Settings +-------------------------------------- +This section covers additional notes for the functions "ProjectManager:CreateCloudProject," "ProjectManager:ImportCloudProject," and "ProjectManager:RestoreCloudProject" + +All three functions take in a {cloudSettings} dict, that have the following keys: +* resolve.CLOUD_SETTING_PROJECT_NAME: String, ["" by default] +* resolve.CLOUD_SETTING_PROJECT_MEDIA_PATH: String, ["" by default] +* resolve.CLOUD_SETTING_IS_COLLAB: Bool, [False by default] +* resolve.CLOUD_SETTING_SYNC_MODE: syncMode (see below), [resolve.CLOUD_SYNC_PROXY_ONLY by default] +* resolve.CLOUD_SETTING_IS_CAMERA_ACCESS: Bool [False by default] + +Where syncMode is one of the following values: +* resolve.CLOUD_SYNC_NONE, +* resolve.CLOUD_SYNC_PROXY_ONLY, +* resolve.CLOUD_SYNC_PROXY_AND_ORIG + +All three "ProjectManager:CreateCloudProject," "ProjectManager:ImportCloudProject," and "ProjectManager:RestoreCloudProject" require resolve.PROJECT_MEDIA_PATH to be defined. "ProjectManager:CreateCloudProject" also requires resolve.PROJECT_NAME to be defined. + Looking up Project and Clip properties -------------------------------------- This section covers additional notes for the functions "Project:GetSetting", "Project:SetSetting", "Timeline:GetSetting", "Timeline:SetSetting", "MediaPoolItem:GetClipProperty" and @@ -478,6 +516,49 @@ Affects: • x = MediaPoolItem:GetClipProperty('Super Scale') and MediaPoolItem:SetClipProperty('Super Scale', x) • for '2x Enhanced' --> MediaPoolItem:SetClipProperty('Super Scale', 2, sharpnessValue, noiseReductionValue), where sharpnessValue is a float in the range [0.0, 1.0] and noiseReductionValue is a float in the range [0.0, 1.0] +Auto Caption Settings +---------------------- +This section covers the supported settings for the method Timeline.CreateSubtitlesFromAudio({autoCaptionSettings}) + +The parameter setting is a dictionary containing the following keys: +* resolve.SUBTITLE_LANGUAGE: languageID (see below), [resolve.AUTO_CAPTION_AUTO by default] +* resolve.SUBTITLE_CAPTION_PRESET: presetType (see below), [resolve.AUTO_CAPTION_SUBTITLE_DEFAULT by default] +* resolve.SUBTITLE_CHARS_PER_LINE: Number between 1 and 60 inclusive [42 by default] +* resolve.SUBTITLE_LINE_BREAK: lineBreakType (see below), [resolve.AUTO_CAPTION_LINE_SINGLE by default] +* resolve.SUBTITLE_GAP: Number between 0 and 10 inclusive [0 by default] + +Note that the default values for some keys may change based on values defined for other keys, as per the UI. +For example, if the following dictionary is supplied, + CreateSubtitlesFromAudio( { resolve.SUBTITLE_LANGUAGE = resolve.AUTO_CAPTION_KOREAN, + resolve.SUBTITLE_CAPTION_PRESET = resolve.AUTO_CAPTION_NETFLIX } ) +the default value for resolve.SUBTITLE_CHARS_PER_LINE will be 16 instead of 42 + +languageIDs: +* resolve.AUTO_CAPTION_AUTO +* resolve.AUTO_CAPTION_DANISH +* resolve.AUTO_CAPTION_DUTCH +* resolve.AUTO_CAPTION_ENGLISH +* resolve.AUTO_CAPTION_FRENCH +* resolve.AUTO_CAPTION_GERMAN +* resolve.AUTO_CAPTION_ITALIAN +* resolve.AUTO_CAPTION_JAPANESE +* resolve.AUTO_CAPTION_KOREAN +* resolve.AUTO_CAPTION_MANDARIN_SIMPLIFIED +* resolve.AUTO_CAPTION_MANDARIN_TRADITIONAL +* resolve.AUTO_CAPTION_NORWEGIAN +* resolve.AUTO_CAPTION_PORTUGUESE +* resolve.AUTO_CAPTION_RUSSIAN +* resolve.AUTO_CAPTION_SPANISH +* resolve.AUTO_CAPTION_SWEDISH + +presetTypes: +* resolve.AUTO_CAPTION_SUBTITLE_DEFAULT +* resolve.AUTO_CAPTION_TELETEXT +* resolve.AUTO_CAPTION_NETFLIX + +lineBreakTypes: +* resolve.AUTO_CAPTION_LINE_SINGLE +* resolve.AUTO_CAPTION_LINE_DOUBLE Looking up Render Settings -------------------------- From f943fe74276e0b2b43a8f349b51c84fd10e3096f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Mar 2024 14:24:38 +0100 Subject: [PATCH 02/21] Add Ayon core constants and menu changes, update AyonMenu class, and remove menu_style.qss file. Update pipeline with ResolveHost class changes. --- .../ayon_core/hosts/resolve/api/constants.py | 18 ++++ client/ayon_core/hosts/resolve/api/menu.py | 95 +++++-------------- .../hosts/resolve/api/menu_style.qss | 71 -------------- .../ayon_core/hosts/resolve/api/pipeline.py | 69 ++++---------- client/ayon_core/hosts/resolve/api/workio.py | 43 +++++---- 5 files changed, 84 insertions(+), 212 deletions(-) create mode 100644 client/ayon_core/hosts/resolve/api/constants.py delete mode 100644 client/ayon_core/hosts/resolve/api/menu_style.qss diff --git a/client/ayon_core/hosts/resolve/api/constants.py b/client/ayon_core/hosts/resolve/api/constants.py new file mode 100644 index 0000000000..4b809e8786 --- /dev/null +++ b/client/ayon_core/hosts/resolve/api/constants.py @@ -0,0 +1,18 @@ +# Ayon sequential rename variables +rename_index = 0 +rename_add = 0 + +publish_clip_color = "Pink" +ayon_marker_workflow = True + +# Ayon compound clip workflow variable +ayon_tag_name = "VFX Notes" + +# Ayon marker workflow variables +ayon_marker_name = "AyonData" +ayon_marker_duration = 1 +ayon_marker_color = "Mint" +temp_marker_frame = None + +# Ayon default timeline +ayon_timeline_name = "AyonTimeline" diff --git a/client/ayon_core/hosts/resolve/api/menu.py b/client/ayon_core/hosts/resolve/api/menu.py index 59eba14d83..ce0ca386e4 100644 --- a/client/ayon_core/hosts/resolve/api/menu.py +++ b/client/ayon_core/hosts/resolve/api/menu.py @@ -5,45 +5,22 @@ from ayon_core.tools.utils import host_tools from ayon_core.pipeline import registered_host - +from ayon_core.style import load_stylesheet +from ayon_core.resources import get_ayon_icon_filepath MENU_LABEL = os.environ["AYON_MENU_LABEL"] -def load_stylesheet(): - path = os.path.join(os.path.dirname(__file__), "menu_style.qss") - if not os.path.exists(path): - print("Unable to load stylesheet, file not found in resources") - return "" - - with open(path, "r") as file_stream: - stylesheet = file_stream.read() - return stylesheet - - -class Spacer(QtWidgets.QWidget): - def __init__(self, height, *args, **kwargs): - super(Spacer, self).__init__(*args, **kwargs) - - self.setFixedHeight(height) - - real_spacer = QtWidgets.QWidget(self) - real_spacer.setObjectName("Spacer") - real_spacer.setFixedHeight(height) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(real_spacer) - - self.setLayout(layout) - - -class OpenPypeMenu(QtWidgets.QWidget): +class AyonMenu(QtWidgets.QWidget): def __init__(self, *args, **kwargs): - super(OpenPypeMenu, self).__init__(*args, **kwargs) + super(AyonMenu, self).__init__(*args, **kwargs) self.setObjectName(f"{MENU_LABEL}Menu") + icon_path = get_ayon_icon_filepath() + icon = QtGui.QIcon(icon_path) + self.setWindowIcon(icon) + self.setWindowFlags( QtCore.Qt.Window | QtCore.Qt.CustomizeWindowHint @@ -56,15 +33,11 @@ def __init__(self, *args, **kwargs): save_current_btn = QtWidgets.QPushButton("Save current file", self) workfiles_btn = QtWidgets.QPushButton("Workfiles ...", self) create_btn = QtWidgets.QPushButton("Create ...", self) - publish_btn = QtWidgets.QPushButton("Publish ...", self) + publish_btn = QtWidgets.QPushButton("Publish...", self) load_btn = QtWidgets.QPushButton("Load ...", self) - inventory_btn = QtWidgets.QPushButton("Manager ...", self) - subsetm_btn = QtWidgets.QPushButton("Subset Manager ...", self) - libload_btn = QtWidgets.QPushButton("Library ...", self) - experimental_btn = QtWidgets.QPushButton( - "Experimental tools ...", self - ) - # rename_btn = QtWidgets.QPushButton("Rename", self) + inventory_btn = QtWidgets.QPushButton("Manage...", self) + libload_btn = QtWidgets.QPushButton("Library...", self) + # set_colorspace_btn = QtWidgets.QPushButton( # "Set colorspace from presets", self # ) @@ -77,32 +50,20 @@ def __init__(self, *args, **kwargs): layout.addWidget(save_current_btn) - layout.addWidget(Spacer(15, self)) - + layout.addSpacing(15) layout.addWidget(workfiles_btn) + + layout.addSpacing(15) + layout.addWidget(create_btn) - layout.addWidget(publish_btn) layout.addWidget(load_btn) + layout.addWidget(publish_btn) layout.addWidget(inventory_btn) - layout.addWidget(subsetm_btn) - layout.addWidget(Spacer(15, self)) + layout.addSpacing(15) layout.addWidget(libload_btn) - # layout.addWidget(Spacer(15, self)) - - # layout.addWidget(rename_btn) - - # layout.addWidget(Spacer(15, self)) - - # layout.addWidget(set_colorspace_btn) - # layout.addWidget(reset_resolution_btn) - layout.addWidget(Spacer(15, self)) - layout.addWidget(experimental_btn) - - self.setLayout(layout) - save_current_btn.clicked.connect(self.on_save_current_clicked) save_current_btn.setShortcut(QtGui.QKeySequence.Save) workfiles_btn.clicked.connect(self.on_workfile_clicked) @@ -110,12 +71,13 @@ def __init__(self, *args, **kwargs): publish_btn.clicked.connect(self.on_publish_clicked) load_btn.clicked.connect(self.on_load_clicked) inventory_btn.clicked.connect(self.on_inventory_clicked) - subsetm_btn.clicked.connect(self.on_subsetm_clicked) libload_btn.clicked.connect(self.on_libload_clicked) - # rename_btn.clicked.connect(self.on_rename_clicked) + # set_colorspace_btn.clicked.connect(self.on_set_colorspace_clicked) # reset_resolution_btn.clicked.connect(self.on_set_resolution_clicked) - experimental_btn.clicked.connect(self.on_experimental_clicked) + + # Resize width, make height as small fitting as possible + self.resize(200, 1) def on_save_current_clicked(self): host = registered_host() @@ -135,11 +97,11 @@ def on_workfile_clicked(self): def on_create_clicked(self): print("Clicked Create") - host_tools.show_creator() + host_tools.show_publisher(tab="create") def on_publish_clicked(self): print("Clicked Publish") - host_tools.show_publish(parent=None) + host_tools.show_publisher(tab="publish") def on_load_clicked(self): print("Clicked Load") @@ -149,10 +111,6 @@ def on_inventory_clicked(self): print("Clicked Inventory") host_tools.show_scene_inventory() - def on_subsetm_clicked(self): - print("Clicked Subset Manager") - host_tools.show_subset_manager() - def on_libload_clicked(self): print("Clicked Library") host_tools.show_library_loader() @@ -166,14 +124,11 @@ def on_set_colorspace_clicked(self): def on_set_resolution_clicked(self): print("Clicked Set Resolution") - def on_experimental_clicked(self): - host_tools.show_experimental_tools_dialog() - def launch_pype_menu(): app = QtWidgets.QApplication(sys.argv) - pype_menu = OpenPypeMenu() + pype_menu = AyonMenu() stylesheet = load_stylesheet() pype_menu.setStyleSheet(stylesheet) diff --git a/client/ayon_core/hosts/resolve/api/menu_style.qss b/client/ayon_core/hosts/resolve/api/menu_style.qss deleted file mode 100644 index 3d51c7139f..0000000000 --- a/client/ayon_core/hosts/resolve/api/menu_style.qss +++ /dev/null @@ -1,71 +0,0 @@ -QWidget { - background-color: #282828; - border-radius: 3; - font-size: 13px; -} - -QComboBox { - border: 1px solid #090909; - background-color: #201f1f; - color: #ffffff; -} - -QComboBox QAbstractItemView -{ - color: white; -} - -QPushButton { - border: 1px solid #090909; - background-color: #201f1f; - color: #ffffff; - padding: 5; -} - -QPushButton:focus { - background-color: "#171717"; - color: #d0d0d0; -} - -QPushButton:hover { - background-color: "#171717"; - color: #e64b3d; -} - -QSpinBox { - border: 1px solid #090909; - background-color: #201f1f; - color: #ffffff; - padding: 2; - max-width: 8em; - qproperty-alignment: AlignCenter; -} - -QLineEdit { - border: 1px solid #090909; - border-radius: 3px; - background-color: #201f1f; - color: #ffffff; - padding: 2; - min-width: 10em; - qproperty-alignment: AlignCenter; -} - -#OpenPypeMenu { - qproperty-alignment: AlignLeft; - min-width: 10em; - border: 1px solid #fef9ef; -} - -QVBoxLayout { - background-color: #282828; -} - -#Divider { - border: 1px solid #090909; - background-color: #585858; -} - -QLabel { - color: #77776b; -} diff --git a/client/ayon_core/hosts/resolve/api/pipeline.py b/client/ayon_core/hosts/resolve/api/pipeline.py index 19d33971dc..b7d25d5d6f 100644 --- a/client/ayon_core/hosts/resolve/api/pipeline.py +++ b/client/ayon_core/hosts/resolve/api/pipeline.py @@ -17,7 +17,8 @@ from ayon_core.host import ( HostBase, IWorkfileHost, - ILoadHost + ILoadHost, + IPublishHost ) from . import lib @@ -42,7 +43,7 @@ AVALON_CONTAINERS = ":AVALON_CONTAINERS" -class ResolveHost(HostBase, IWorkfileHost, ILoadHost): +class ResolveHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "resolve" def install(self): @@ -93,6 +94,22 @@ def get_workfile_extensions(self): def get_containers(self): return ls() + def get_context_data(self): + # data = cmds.fileInfo("OpenPypeContext", query=True) + # if not data: + # return {} + # + # data = data[0] # Maya seems to return a list + # decoded = base64.b64decode(data).decode("utf-8") + # return json.loads(decoded) + return {} + + def update_context_data(self, data, changes): + # json_str = json.dumps(data) + # encoded = base64.b64encode(json_str.encode("utf-8")) + # return cmds.fileInfo("OpenPypeContext", encoded) + pass + def containerise(timeline_item, name, @@ -253,51 +270,3 @@ def on_pyblish_instance_toggled(instance, old_value, new_value): # Whether instances should be passthrough based on new value timeline_item = instance.data["item"] set_publish_attribute(timeline_item, new_value) - - -def remove_instance(instance): - """Remove instance marker from track item.""" - instance_id = instance.get("uuid") - - selected_timeline_items = lib.get_current_timeline_items( - filter=True, selecting_color=lib.publish_clip_color) - - found_ti = None - for timeline_item_data in selected_timeline_items: - timeline_item = timeline_item_data["clip"]["item"] - - # get openpype tag data - tag_data = lib.get_timeline_item_pype_tag(timeline_item) - _ti_id = tag_data.get("uuid") - if _ti_id == instance_id: - found_ti = timeline_item - break - - if found_ti is None: - return - - # removing instance by marker color - print(f"Removing instance: {found_ti.GetName()}") - found_ti.DeleteMarkersByColor(lib.pype_marker_color) - - -def list_instances(): - """List all created instances from current workfile.""" - listed_instances = [] - selected_timeline_items = lib.get_current_timeline_items( - filter=True, selecting_color=lib.publish_clip_color) - - for timeline_item_data in selected_timeline_items: - timeline_item = timeline_item_data["clip"]["item"] - ti_name = timeline_item.GetName().split(".")[0] - - # get openpype tag data - tag_data = lib.get_timeline_item_pype_tag(timeline_item) - - if tag_data: - asset = tag_data.get("asset") - product_name = tag_data.get("productName") - tag_data["label"] = f"{ti_name} [{asset}-{product_name}]" - listed_instances.append(tag_data) - - return listed_instances diff --git a/client/ayon_core/hosts/resolve/api/workio.py b/client/ayon_core/hosts/resolve/api/workio.py index b6c2f63432..fc61d9bb10 100644 --- a/client/ayon_core/hosts/resolve/api/workio.py +++ b/client/ayon_core/hosts/resolve/api/workio.py @@ -4,7 +4,7 @@ from ayon_core.lib import Logger from .lib import ( get_project_manager, - get_current_project + get_current_resolve_project ) @@ -16,27 +16,28 @@ def file_extensions(): def has_unsaved_changes(): - get_project_manager().SaveProject() + project_manager = get_project_manager() + project_manager.SaveProject() return False def save_file(filepath): - pm = get_project_manager() + project_manager = get_project_manager() file = os.path.basename(filepath) fname, _ = os.path.splitext(file) - project = get_current_project() - name = project.GetName() + resolve_project = get_current_resolve_project() + name = resolve_project.GetName() response = False if name == "Untitled Project": - response = pm.CreateProject(fname) + response = project_manager.CreateProject(fname) log.info("New project created: {}".format(response)) - pm.SaveProject() + project_manager.SaveProject() elif name != fname: - response = project.SetName(fname) + response = resolve_project.SetName(fname) log.info("Project renamed: {}".format(response)) - exported = pm.ExportProject(fname, filepath) + exported = project_manager.ExportProject(fname, filepath) log.info("Project exported: {}".format(exported)) @@ -47,41 +48,41 @@ def open_file(filepath): from . import bmdvr - pm = get_project_manager() + project_manager = get_project_manager() page = bmdvr.GetCurrentPage() if page is not None: # Save current project only if Resolve has an active page, otherwise # we consider Resolve being in a pre-launch state (no open UI yet) - project = pm.GetCurrentProject() - print(f"Saving current project: {project}") - pm.SaveProject() + resolve_project = get_current_resolve_project() + print(f"Saving current resolve project: {resolve_project}") + project_manager.SaveProject() file = os.path.basename(filepath) fname, _ = os.path.splitext(file) try: # load project from input path - project = pm.LoadProject(fname) - log.info(f"Project {project.GetName()} opened...") + resolve_project = project_manager.LoadProject(fname) + log.info(f"Project {resolve_project.GetName()} opened...") except AttributeError: log.warning((f"Project with name `{fname}` does not exist! It will " f"be imported from {filepath} and then loaded...")) - if pm.ImportProject(filepath): + if project_manager.ImportProject(filepath): # load project from input path - project = pm.LoadProject(fname) - log.info(f"Project imported/loaded {project.GetName()}...") + resolve_project = project_manager.LoadProject(fname) + log.info(f"Project imported/loaded {resolve_project.GetName()}...") return True return False return True def current_file(): - pm = get_project_manager() + resolve_project = get_current_resolve_project() file_ext = file_extensions()[0] workdir_path = os.getenv("AYON_WORKDIR") - project = pm.GetCurrentProject() - project_name = project.GetName() + + project_name = resolve_project.GetName() file_name = project_name + file_ext # create current file path From 09d7dc6dc156c24f37c01e6277f344e28c9e230d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Mar 2024 14:28:20 +0100 Subject: [PATCH 03/21] Update Resolve API functions and markers for Ayon compatibility. - Renamed functions to use "ayon" instead of "pype" for consistency. - Added backward compatibility for old function names. - Updated marker handling in various plugins for Ayon integration. --- .../ayon_core/hosts/resolve/api/__init__.py | 29 ++++++++++++------- .../ayon_core/hosts/resolve/api/pipeline.py | 10 +++---- .../hosts/resolve/plugins/load/load_clip.py | 10 +++---- .../plugins/publish/precollect_instances.py | 4 +-- .../plugins/publish/precollect_workfile.py | 12 ++++---- .../utility_scripts/develop/OTIO_export.py | 6 ++-- 6 files changed, 39 insertions(+), 32 deletions(-) diff --git a/client/ayon_core/hosts/resolve/api/__init__.py b/client/ayon_core/hosts/resolve/api/__init__.py index dba275e6c4..95d9147ae4 100644 --- a/client/ayon_core/hosts/resolve/api/__init__.py +++ b/client/ayon_core/hosts/resolve/api/__init__.py @@ -11,15 +11,14 @@ containerise, update_container, maintained_selection, - remove_instance, - list_instances ) from .lib import ( maintain_current_timeline, publish_clip_color, get_project_manager, - get_current_project, + get_current_resolve_project, + get_current_project, # backward compatibility get_current_timeline, get_any_timeline, get_new_timeline, @@ -31,8 +30,10 @@ get_video_track_names, get_current_timeline_items, get_pype_timeline_item_by_name, - get_timeline_item_pype_tag, - set_timeline_item_pype_tag, + get_timeline_item_ayon_tag, + get_timeline_item_pype_tag, # backward compatibility + set_timeline_item_ayon_tag, + set_timeline_item_pype_tag, # backward compatibility imprint, set_publish_attribute, get_publish_attribute, @@ -64,13 +65,18 @@ from .testing_utils import TestGUI - +# Resolve specific singletons bmdvr = None bmdvf = None +project_manager = None +media_storage = None + __all__ = [ "bmdvr", "bmdvf", + "project_manager", + "media_storage", # pipeline "ResolveHost", @@ -78,8 +84,6 @@ "containerise", "update_container", "maintained_selection", - "remove_instance", - "list_instances", # utils "get_resolve_module", @@ -88,7 +92,8 @@ "maintain_current_timeline", "publish_clip_color", "get_project_manager", - "get_current_project", + "get_current_resolve_project", + "get_current_project", # backward compatibility "get_current_timeline", "get_any_timeline", "get_new_timeline", @@ -100,8 +105,10 @@ "get_video_track_names", "get_current_timeline_items", "get_pype_timeline_item_by_name", - "get_timeline_item_pype_tag", - "set_timeline_item_pype_tag", + "get_timeline_item_ayon_tag", + "get_timeline_item_pype_tag", # backward compatibility + "set_timeline_item_ayon_tag", + "set_timeline_item_pype_tag", # backward compatibility "imprint", "set_publish_attribute", "get_publish_attribute", diff --git a/client/ayon_core/hosts/resolve/api/pipeline.py b/client/ayon_core/hosts/resolve/api/pipeline.py index b7d25d5d6f..bc034639cb 100644 --- a/client/ayon_core/hosts/resolve/api/pipeline.py +++ b/client/ayon_core/hosts/resolve/api/pipeline.py @@ -146,7 +146,7 @@ def containerise(timeline_item, if data: data_imprint.update(data) - lib.set_timeline_item_pype_tag(timeline_item, data_imprint) + lib.set_timeline_item_ayon_tag(timeline_item, data_imprint) return timeline_item @@ -184,7 +184,7 @@ def parse_container(timeline_item, validate=True): """ # convert tag metadata to normal keys names - data = lib.get_timeline_item_pype_tag(timeline_item) + data = lib.get_timeline_item_ayon_tag(timeline_item) if validate and data and data.get("schema"): schema.validate(data) @@ -220,9 +220,9 @@ def update_container(timeline_item, data=None): bool: True if container was updated correctly """ - data = data or dict() + data = data or {} - container = lib.get_timeline_item_pype_tag(timeline_item) + container = lib.get_timeline_item_ayon_tag(timeline_item) for _key, _value in container.items(): try: @@ -231,7 +231,7 @@ def update_container(timeline_item, data=None): pass log.info("Updating container: `{}`".format(timeline_item)) - return bool(lib.set_timeline_item_pype_tag(timeline_item, container)) + return bool(lib.set_timeline_item_ayon_tag(timeline_item, container)) @contextlib.contextmanager diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_clip.py b/client/ayon_core/hosts/resolve/plugins/load/load_clip.py index 04b2aaaf15..d00af41c50 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_clip.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_clip.py @@ -154,12 +154,12 @@ def remove(self, container): 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 + if lib.get_ayon_marker(timeline_item): + # Note: We must call `get_ayon_marker` because + # `delete_ayon_marker` uses a global variable set by + # `get_ayon_marker` to delete the right marker # TODO: Improve code to avoid the global `temp_marker_frame` - lib.delete_pype_marker(timeline_item) + lib.delete_ayon_marker(timeline_item) # if media pool item has no remaining usages left # remove it from the media pool diff --git a/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py b/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py index b1374859e3..a01d3f1c32 100644 --- a/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py +++ b/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py @@ -5,7 +5,7 @@ from ayon_core.pipeline import AYON_INSTANCE_ID, AVALON_INSTANCE_ID from ayon_core.hosts.resolve.api.lib import ( get_current_timeline_items, - get_timeline_item_pype_tag, + get_timeline_item_ayon_tag, publish_clip_color, get_publish_attribute, get_otio_clip_instance_data, @@ -34,7 +34,7 @@ def process(self, context): timeline_item = timeline_item_data["clip"]["item"] # get pype tag data - tag_data = get_timeline_item_pype_tag(timeline_item) + tag_data = get_timeline_item_ayon_tag(timeline_item) self.log.debug(f"__ tag_data: {pformat(tag_data)}") if not tag_data: diff --git a/client/ayon_core/hosts/resolve/plugins/publish/precollect_workfile.py b/client/ayon_core/hosts/resolve/plugins/publish/precollect_workfile.py index a147c9a905..6b9060d942 100644 --- a/client/ayon_core/hosts/resolve/plugins/publish/precollect_workfile.py +++ b/client/ayon_core/hosts/resolve/plugins/publish/precollect_workfile.py @@ -18,17 +18,17 @@ def process(self, context): asset_name = current_asset_name.split("/")[-1] product_name = "workfileMain" - project = rapi.get_current_project() - fps = project.GetSetting("timelineFrameRate") + resolve_project = rapi.get_current_resolve_project() + fps = resolve_project.GetSetting("timelineFrameRate") video_tracks = rapi.get_video_track_names() # adding otio timeline to context - otio_timeline = davinci_export.create_otio_timeline(project) + otio_timeline = davinci_export.create_otio_timeline(resolve_project) instance_data = { "name": "{}_{}".format(asset_name, product_name), "label": "{} {}".format(current_asset_name, product_name), - "item": project, + "item": resolve_project, "folderPath": current_asset_name, "productName": product_name, "productType": "workfile", @@ -41,10 +41,10 @@ def process(self, context): # update context with main project attributes context_data = { - "activeProject": project, + "activeProject": resolve_project, "otioTimeline": otio_timeline, "videoTracks": video_tracks, - "currentFile": project.GetName(), + "currentFile": resolve_project.GetName(), "fps": fps, } context.data.update(context_data) diff --git a/client/ayon_core/hosts/resolve/utility_scripts/develop/OTIO_export.py b/client/ayon_core/hosts/resolve/utility_scripts/develop/OTIO_export.py index c1c83eb060..000b17ac29 100644 --- a/client/ayon_core/hosts/resolve/utility_scripts/develop/OTIO_export.py +++ b/client/ayon_core/hosts/resolve/utility_scripts/develop/OTIO_export.py @@ -57,9 +57,9 @@ def _close_window(event): def _export_button(event): pm = resolve.GetProjectManager() - project = pm.GetCurrentProject() - timeline = project.GetCurrentTimeline() - otio_timeline = otio_export.create_otio_timeline(project) + resolve_project = pm.GetCurrentProject() + timeline = resolve_project.GetCurrentTimeline() + otio_timeline = otio_export.create_otio_timeline(resolve_project) otio_path = os.path.join( itm["exportfilebttn"].Text, timeline.GetName() + ".otio") From fbd0fc5fd3f49bae5048f84835f5a0e52d54a2b5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Mar 2024 16:34:00 +0100 Subject: [PATCH 04/21] Update plugin classes and imports for backward compatibility with new class names. Refactor Creator to ResolveCreator and PublishClip to PublishableClip for consistency. Remove unused imports and code related to LegacyCreator. --- .../ayon_core/hosts/resolve/api/__init__.py | 12 +- client/ayon_core/hosts/resolve/api/plugin.py | 556 +++++------------- 2 files changed, 159 insertions(+), 409 deletions(-) diff --git a/client/ayon_core/hosts/resolve/api/__init__.py b/client/ayon_core/hosts/resolve/api/__init__.py index 95d9147ae4..a8d285082c 100644 --- a/client/ayon_core/hosts/resolve/api/__init__.py +++ b/client/ayon_core/hosts/resolve/api/__init__.py @@ -50,8 +50,10 @@ from .plugin import ( ClipLoader, TimelineItemLoader, - Creator, - PublishClip + ResolveCreator, + Creator, # backward compatibility + PublishableClip, + PublishClip, # backward compatibility ) from .workio import ( @@ -125,8 +127,10 @@ # plugin "ClipLoader", "TimelineItemLoader", - "Creator", - "PublishClip", + "ResolveCreator", + "Creator", # backward compatibility + "PublishableClip", + "PublishClip", # backward compatibility # workio "open_file", diff --git a/client/ayon_core/hosts/resolve/api/plugin.py b/client/ayon_core/hosts/resolve/api/plugin.py index dfce3ea37a..d6edc5f1a4 100644 --- a/client/ayon_core/hosts/resolve/api/plugin.py +++ b/client/ayon_core/hosts/resolve/api/plugin.py @@ -1,290 +1,18 @@ import re import uuid -import copy import qargparse -from qtpy import QtWidgets, QtCore -from ayon_core.settings import get_current_project_settings +from ayon_core.pipeline.context_tools import get_current_project_asset + +from ayon_core.lib import BoolDef + from ayon_core.pipeline import ( - LegacyCreator, LoaderPlugin, - Anatomy + Creator as NewCreator ) from . import lib -from .menu import load_stylesheet - - -class CreatorWidget(QtWidgets.QDialog): - - # output items - items = {} - - def __init__(self, name, info, ui_inputs, parent=None): - super(CreatorWidget, self).__init__(parent) - - self.setObjectName(name) - - self.setWindowFlags( - QtCore.Qt.Window - | QtCore.Qt.CustomizeWindowHint - | QtCore.Qt.WindowTitleHint - | QtCore.Qt.WindowCloseButtonHint - | QtCore.Qt.WindowStaysOnTopHint - ) - self.setWindowTitle(name or "OpenPype Creator Input") - self.resize(500, 700) - - # Where inputs and labels are set - self.content_widget = [QtWidgets.QWidget(self)] - top_layout = QtWidgets.QFormLayout(self.content_widget[0]) - top_layout.setObjectName("ContentLayout") - top_layout.addWidget(Spacer(5, self)) - - # first add widget tag line - top_layout.addWidget(QtWidgets.QLabel(info)) - - # main dynamic layout - self.scroll_area = QtWidgets.QScrollArea(self, widgetResizable=True) - self.scroll_area.setVerticalScrollBarPolicy( - QtCore.Qt.ScrollBarAsNeeded) - self.scroll_area.setVerticalScrollBarPolicy( - QtCore.Qt.ScrollBarAlwaysOn) - self.scroll_area.setHorizontalScrollBarPolicy( - QtCore.Qt.ScrollBarAlwaysOff) - self.scroll_area.setWidgetResizable(True) - - self.content_widget.append(self.scroll_area) - - scroll_widget = QtWidgets.QWidget(self) - in_scroll_area = QtWidgets.QVBoxLayout(scroll_widget) - self.content_layout = [in_scroll_area] - - # add preset data into input widget layout - self.items = self.populate_widgets(ui_inputs) - self.scroll_area.setWidget(scroll_widget) - - # Confirmation buttons - btns_widget = QtWidgets.QWidget(self) - btns_layout = QtWidgets.QHBoxLayout(btns_widget) - - cancel_btn = QtWidgets.QPushButton("Cancel") - btns_layout.addWidget(cancel_btn) - - ok_btn = QtWidgets.QPushButton("Ok") - btns_layout.addWidget(ok_btn) - - # Main layout of the dialog - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.setContentsMargins(10, 10, 10, 10) - main_layout.setSpacing(0) - - # adding content widget - for w in self.content_widget: - main_layout.addWidget(w) - - main_layout.addWidget(btns_widget) - - ok_btn.clicked.connect(self._on_ok_clicked) - cancel_btn.clicked.connect(self._on_cancel_clicked) - - stylesheet = load_stylesheet() - self.setStyleSheet(stylesheet) - - def _on_ok_clicked(self): - self.result = self.value(self.items) - self.close() - - def _on_cancel_clicked(self): - self.result = None - self.close() - - def value(self, data, new_data=None): - new_data = new_data or {} - for k, v in data.items(): - new_data[k] = { - "target": None, - "value": None - } - if v["type"] == "dict": - new_data[k]["target"] = v["target"] - new_data[k]["value"] = self.value(v["value"]) - if v["type"] == "section": - new_data.pop(k) - new_data = self.value(v["value"], new_data) - elif getattr(v["value"], "currentText", None): - new_data[k]["target"] = v["target"] - new_data[k]["value"] = v["value"].currentText() - elif getattr(v["value"], "isChecked", None): - new_data[k]["target"] = v["target"] - new_data[k]["value"] = v["value"].isChecked() - elif getattr(v["value"], "value", None): - new_data[k]["target"] = v["target"] - new_data[k]["value"] = v["value"].value() - elif getattr(v["value"], "text", None): - new_data[k]["target"] = v["target"] - new_data[k]["value"] = v["value"].text() - - return new_data - - def camel_case_split(self, text): - matches = re.finditer( - '.+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)', text) - return " ".join([str(m.group(0)).capitalize() for m in matches]) - - def create_row(self, layout, type, text, **kwargs): - # get type attribute from qwidgets - attr = getattr(QtWidgets, type) - - # convert label text to normal capitalized text with spaces - label_text = self.camel_case_split(text) - - # assign the new text to label widget - label = QtWidgets.QLabel(label_text) - label.setObjectName("LineLabel") - - # create attribute name text strip of spaces - attr_name = text.replace(" ", "") - - # create attribute and assign default values - setattr( - self, - attr_name, - attr(parent=self)) - - # assign the created attribute to variable - item = getattr(self, attr_name) - for func, val in kwargs.items(): - if getattr(item, func): - func_attr = getattr(item, func) - if isinstance(val, tuple): - func_attr(*val) - else: - func_attr(val) - - # add to layout - layout.addRow(label, item) - - return item - - def populate_widgets(self, data, content_layout=None): - """ - Populate widget from input dict. - - Each plugin has its own set of widget rows defined in dictionary - each row values should have following keys: `type`, `target`, - `label`, `order`, `value` and optionally also `toolTip`. - - Args: - data (dict): widget rows or organized groups defined - by types `dict` or `section` - content_layout (QtWidgets.QFormLayout)[optional]: used when nesting - - Returns: - dict: redefined data dict updated with created widgets - - """ - - content_layout = content_layout or self.content_layout[-1] - # fix order of process by defined order value - ordered_keys = list(data.keys()) - for k, v in data.items(): - try: - # try removing a key from index which should - # be filled with new - ordered_keys.pop(v["order"]) - except IndexError: - pass - # add key into correct order - ordered_keys.insert(v["order"], k) - - # process ordered - for k in ordered_keys: - v = data[k] - tool_tip = v.get("toolTip", "") - if v["type"] == "dict": - # adding spacer between sections - self.content_layout.append(QtWidgets.QWidget(self)) - content_layout.addWidget(self.content_layout[-1]) - self.content_layout[-1].setObjectName("sectionHeadline") - - headline = QtWidgets.QVBoxLayout(self.content_layout[-1]) - headline.addWidget(Spacer(20, self)) - headline.addWidget(QtWidgets.QLabel(v["label"])) - - # adding nested layout with label - self.content_layout.append(QtWidgets.QWidget(self)) - self.content_layout[-1].setObjectName("sectionContent") - - nested_content_layout = QtWidgets.QFormLayout( - self.content_layout[-1]) - nested_content_layout.setObjectName("NestedContentLayout") - content_layout.addWidget(self.content_layout[-1]) - - # add nested key as label - data[k]["value"] = self.populate_widgets( - v["value"], nested_content_layout) - - if v["type"] == "section": - # adding spacer between sections - self.content_layout.append(QtWidgets.QWidget(self)) - content_layout.addWidget(self.content_layout[-1]) - self.content_layout[-1].setObjectName("sectionHeadline") - - headline = QtWidgets.QVBoxLayout(self.content_layout[-1]) - headline.addWidget(Spacer(20, self)) - headline.addWidget(QtWidgets.QLabel(v["label"])) - - # adding nested layout with label - self.content_layout.append(QtWidgets.QWidget(self)) - self.content_layout[-1].setObjectName("sectionContent") - - nested_content_layout = QtWidgets.QFormLayout( - self.content_layout[-1]) - nested_content_layout.setObjectName("NestedContentLayout") - content_layout.addWidget(self.content_layout[-1]) - - # add nested key as label - data[k]["value"] = self.populate_widgets( - v["value"], nested_content_layout) - - elif v["type"] == "QLineEdit": - data[k]["value"] = self.create_row( - content_layout, "QLineEdit", v["label"], - setText=v["value"], setToolTip=tool_tip) - elif v["type"] == "QComboBox": - data[k]["value"] = self.create_row( - content_layout, "QComboBox", v["label"], - addItems=v["value"], setToolTip=tool_tip) - elif v["type"] == "QCheckBox": - data[k]["value"] = self.create_row( - content_layout, "QCheckBox", v["label"], - setChecked=v["value"], setToolTip=tool_tip) - elif v["type"] == "QSpinBox": - data[k]["value"] = self.create_row( - content_layout, "QSpinBox", v["label"], - setRange=(0, 99999), - setValue=v["value"], - setToolTip=tool_tip) - return data - - -class Spacer(QtWidgets.QWidget): - def __init__(self, height, *args, **kwargs): - super(self.__class__, self).__init__(*args, **kwargs) - - self.setFixedHeight(height) - - real_spacer = QtWidgets.QWidget(self) - real_spacer.setObjectName("Spacer") - real_spacer.setFixedHeight(height) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(real_spacer) - - self.setLayout(layout) class ClipLoader: @@ -554,45 +282,47 @@ def remove(self, container): pass -class Creator(LegacyCreator): - """Creator class wrapper - """ - marker_color = "Purple" +class ResolveCreator(NewCreator): + """ Resolve Creator class wrapper""" - def __init__(self, *args, **kwargs): - super(Creator, self).__init__(*args, **kwargs) + marker_color = "Purple" + presets = {} - resolve_p_settings = get_current_project_settings().get("resolve") - self.presets = {} - if resolve_p_settings: - self.presets = resolve_p_settings["create"].get( - self.__class__.__name__, {}) + def apply_settings(self, project_settings): + resolve_create_settings = ( + project_settings.get("resolve", {}).get("create") + ) + self.presets = resolve_create_settings.get( + self.__class__.__name__, {} + ) + def create(self, subset_name, instance_data, pre_create_data): # adding basic current context resolve objects - self.project = lib.get_current_project() + self.project = lib.get_current_resolve_project() self.timeline = lib.get_current_timeline() - if (self.options or {}).get("useSelection"): + if pre_create_data.get("use_selection", False): self.selected = lib.get_current_timeline_items(filter=True) else: self.selected = lib.get_current_timeline_items(filter=False) - self.widget = CreatorWidget + # TODO: Add a way to store/imprint data + def get_pre_create_attr_defs(self): + return [ + BoolDef("use_selection", + label="Use selection", + default=True) + ] -class PublishClip: - """ - Convert a track item to publishable instance +# alias for backward compatibility +Creator = ResolveCreator # noqa - Args: - timeline_item (hiero.core.TrackItem): hiero track item object - kwargs (optional): additional data needed for rename=True (presets) - Returns: - hiero.core.TrackItem: hiero track item object with openpype tag +class PublishableClip: + """ + Convert a track item to publishable instance """ - vertical_clip_match = {} - tag_data = {} types = { "shot": "shot", "folder": "folder", @@ -608,7 +338,7 @@ class PublishClip: rename_default = False hierarchy_default = "{_folder_}/{_sequence_}/{_track_}" clip_name_default = "shot_{_trackIndex_:0>3}_{_clipIndex_:0>4}" - base_product_name_default = "" + variant_default = "" review_track_default = "< none >" product_type_default = "plate" count_from_default = 10 @@ -616,9 +346,39 @@ class PublishClip: vertical_sync_default = False driving_layer_default = "" - def __init__(self, cls, timeline_item_data, **kwargs): - # populate input cls attribute onto self.[attr] - self.__dict__.update(cls.__dict__) + # Define which keys of the pre create data should also be 'tag data' + tag_keys = { + # renameHierarchy + "hierarchy", + # hierarchyData + "folder", "episode", "sequence", "track", "shot", + # publish settings + "audio", "sourceResolution", + # shot attributes + "workfileFrameStart", "handleStart", "handleEnd" + } + + def __init__( + self, + timeline_item_data: dict, + pre_create_data: dict = None, + media_pool_folder: str = None, + rename_index: int = 0, + data: dict = None + ): + """ Initialize object + + Args: + timeline_item_data (dict): timeline item data + pre_create_data (dict): pre create data + media_pool_folder (str): media pool folder + rename_index (int): rename index + data (dict): additional data + + """ + self.rename_index = rename_index + self.vertical_clip_match = {} + self.tag_data = data or {} # get main parent objects self.timeline_item_data = timeline_item_data @@ -635,14 +395,11 @@ def __init__(self, cls, timeline_item_data, **kwargs): self.track_name = str(track_name).replace(" ", "_") self.track_index = int(timeline_item_data["track"]["index"]) - if kwargs.get("avalon"): - self.tag_data.update(kwargs["avalon"]) - # adding ui inputs if any - self.ui_inputs = kwargs.get("ui_inputs", {}) + self.pre_create_data = pre_create_data or {} # adding media pool folder if any - self.mp_folder = kwargs.get("mp_folder") + self.media_pool_folder = media_pool_folder # populate default data before we get other attributes self._populate_timeline_item_default_data() @@ -654,37 +411,37 @@ def __init__(self, cls, timeline_item_data, **kwargs): self._create_parents() def convert(self): + """ Convert track item to publishable instance. + + Returns: + timeline_item (resolve.TimelineItem): timeline item with imprinted + data in marker + """ # solve track item data and add them to tag data self._convert_to_tag_data() # if track name is in review track name and also if driving track name # is not in review track name: skip tag creation - if (self.track_name in self.review_layer) and ( - self.driving_layer not in self.review_layer): + if ( + self.track_name in self.review_track and + self.hero_track not in self.review_track + ): return # deal with clip name new_name = self.tag_data.pop("newClipName") if self.rename: - self.tag_data["asset_name"] = new_name + self.tag_data["asset"] = new_name else: - self.tag_data["asset_name"] = self.ti_name - - # AYON unique identifier - folder_path = "/{}/{}".format( - self.tag_data["hierarchy"], - self.tag_data["asset_name"] - ) - self.tag_data["folder_path"] = folder_path + self.tag_data["asset"] = self.ti_name - # create new name for track item - if not lib.pype_marker_workflow: + if not lib.ayon_marker_workflow: # create compound clip workflow lib.create_compound_clip( self.timeline_item_data, - self.tag_data["asset_name"], - self.mp_folder + self.tag_data["asset"], + self.media_pool_folder ) # add timeline_item_data selection to tag @@ -718,39 +475,35 @@ def _populate_attributes(self): # define ui inputs if non gui mode was used self.shot_num = self.ti_index - # ui_inputs data or default values if gui was not used - self.rename = self.ui_inputs.get( - "clipRename", {}).get("value") or self.rename_default - self.clip_name = self.ui_inputs.get( - "clipName", {}).get("value") or self.clip_name_default - self.hierarchy = self.ui_inputs.get( - "hierarchy", {}).get("value") or self.hierarchy_default - self.hierarchy_data = self.ui_inputs.get( - "hierarchyData", {}).get("value") or \ - self.timeline_item_default_data.copy() - self.count_from = self.ui_inputs.get( - "countFrom", {}).get("value") or self.count_from_default - self.count_steps = self.ui_inputs.get( - "countSteps", {}).get("value") or self.count_steps_default - self.base_product_name = self.ui_inputs.get( - "productName", {}).get("value") or self.base_product_name_default - self.product_type = self.ui_inputs.get( - "productType", {}).get("value") or self.product_type_default - self.vertical_sync = self.ui_inputs.get( - "vSyncOn", {}).get("value") or self.vertical_sync_default - self.driving_layer = self.ui_inputs.get( - "vSyncTrack", {}).get("value") or self.driving_layer_default - self.review_track = self.ui_inputs.get( - "reviewTrack", {}).get("value") or self.review_track_default - - # build product name from layer name - if self.base_product_name == "": - self.base_product_name = self.track_name - - # create product name for publishing - self.product_name = ( - self.product_type + self.base_product_name.capitalize() - ) + # publisher ui attribute inputs or default values if gui was not used + def get(key): + """Shorthand access for code readability""" + return self.pre_create_data.get(key) + + self.rename = get("clipRename") or self.rename_default + self.clip_name = get("clipName") or self.clip_name_default + self.hierarchy = get("hierarchy") or self.hierarchy_default + self.count_from = get("countFrom") or self.count_from_default + self.count_steps = get("countSteps") or self.count_steps_default + self.variant = get("variant") or self.variant_default + self.product_type = get("productType") or self.product_type_default + self.vertical_sync = get("vSyncOn") or self.vertical_sync_default + self.hero_track = get("vSyncTrack") or self.driving_layer_default + self.review_media_track = ( + get("reviewTrack") or self.review_track_default) + + self.hierarchy_data = { + key: get(key) or self.timeline_item_default_data[key] + for key in ["folder", "episode", "sequence", "track", "shot"] + } + + # build subset name from layer name + if self.variant == "": + self.variant = self.track_name + + # create subset for publishing + # TODO: Use creator `get_subset_name` to correctly define name + self.product_name = self.product_type + self.variant.capitalize() def _replace_hash_to_expression(self, name, text): """ Replace hash with number in correct padding. """ @@ -761,37 +514,38 @@ def _replace_hash_to_expression(self, name, text): return new_text def _convert_to_tag_data(self): - """ Convert internal data to tag data. + """Convert internal data to tag data. Populating the tag data into internal variable self.tag_data """ # define vertical sync attributes hero_track = True - self.review_layer = "" - if self.vertical_sync: - # check if track name is not in driving layer - if self.track_name not in self.driving_layer: - # if it is not then define vertical sync as None - hero_track = False + self.review_track = "" + + if self.vertical_sync and self.track_name not in self.hero_track: + hero_track = False # increasing steps by index of rename iteration self.count_steps *= self.rename_index hierarchy_formatting_data = {} _data = self.timeline_item_default_data.copy() - if self.ui_inputs: + if self.pre_create_data: + # adding tag metadata from ui - for _k, _v in self.ui_inputs.items(): - if _v["target"] == "tag": - self.tag_data[_k] = _v["value"] + for _key, _value in self.pre_create_data.items(): + if _key in self.tag_keys: + self.tag_data[_key] = _value # driving layer is set as positive match if hero_track or self.vertical_sync: - # mark review layer - if self.review_track and ( - self.review_track not in self.review_track_default): - # if review layer is defined and not the same as default - self.review_layer = self.review_track + # mark review track + if ( + self.review_media_track + and self.review_media_track not in self.review_track_default # noqa + ): + # if review track is defined and not the same as default + self.review_track = self.review_media_track # shot num calculate if self.rename_index == 0: self.shot_num = self.count_from @@ -802,16 +556,16 @@ def _convert_to_tag_data(self): _data.update({"shot": self.shot_num}) # solve # in test to pythonic expression - for _k, _v in self.hierarchy_data.items(): - if "#" not in _v["value"]: + for _key, _value in self.hierarchy_data.items(): + if "#" not in _value: continue - self.hierarchy_data[ - _k]["value"] = self._replace_hash_to_expression( - _k, _v["value"]) + self.hierarchy_data[_key] = self._replace_hash_to_expression( + _key, _value + ) # fill up pythonic expresisons in hierarchy data - for k, _v in self.hierarchy_data.items(): - hierarchy_formatting_data[k] = _v["value"].format(**_data) + for _key, _value in self.hierarchy_data.items(): + hierarchy_formatting_data[_key] = _value.format(**_data) else: # if no gui mode then just pass default data hierarchy_formatting_data = self.hierarchy_data @@ -830,19 +584,17 @@ def _convert_to_tag_data(self): # driving layer is set as negative match for (_in, _out), hero_data in self.vertical_clip_match.items(): hero_data.update({"heroTrack": False}) - if _in != self.clip_in or _out != self.clip_out: - continue - - data_product_name = hero_data["productName"] - # add track index in case duplicity of names in hero data - if self.product_name in data_product_name: - hero_data["productName"] = self.product_name + str( - self.track_index) - # in case track name and product name is the same then add - if self.base_product_name == self.track_name: - hero_data["productName"] = self.product_name - # assign data to return hierarchy data to tag - tag_hierarchy_data = hero_data + if _in == self.clip_in and _out == self.clip_out: + data_subset = hero_data["product_name"] + # add track index in case duplicity of names in hero data + if self.product_name in data_subset: + hero_data["product_name"] = self.product_name + str( + self.track_index) + # in case track name and subset name is the same then add + if self.variant == self.track_name: + hero_data["product_name"] = self.product_name + # assign data to return hierarchy data to tag + tag_hierarchy_data = hero_data # add data to return data dict self.tag_data.update(tag_hierarchy_data) @@ -851,8 +603,8 @@ def _convert_to_tag_data(self): self.tag_data["uuid"] = str(uuid.uuid4()) # add review track only to hero track - if hero_track and self.review_layer: - self.tag_data.update({"reviewTrack": self.review_layer}) + if hero_track and self.review_track: + self.tag_data.update({"reviewTrack": self.review_track}) else: self.tag_data.update({"reviewTrack": None}) @@ -882,7 +634,7 @@ def _convert_to_entity(self, key): return { "entity_type": entity_type, - "entity_name": self.hierarchy_data[key]["value"].format( + "entity_name": self.hierarchy_data[key].format( **self.timeline_item_default_data ) } @@ -899,11 +651,5 @@ def _create_parents(self): 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 +# alias for backward compatibility +PublishClip = PublishableClip # noqa From b692cde77c1ff63ebf713c37b7dfb3cd0bc17a80 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Mar 2024 16:34:32 +0100 Subject: [PATCH 05/21] Add import statement, update class inheritance, and define new attributes and methods for clip creation. Improve GUI settings for shot hierarchy and renaming. --- .../plugins/create/create_shot_clip.py | 535 ++++++++++-------- 1 file changed, 300 insertions(+), 235 deletions(-) diff --git a/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py b/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py index 3a2a0345ea..4f6cc6609c 100644 --- a/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py +++ b/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py @@ -1,245 +1,237 @@ -# from pprint import pformat +import copy + from ayon_core.hosts.resolve.api import plugin, lib from ayon_core.hosts.resolve.api.lib import ( get_video_track_names, create_bin, ) +from ayon_core.pipeline.create import CreatorError, CreatedInstance +from ayon_core.lib import BoolDef, EnumDef, TextDef, UILabelDef, NumberDef -class CreateShotClip(plugin.Creator): +class CreateShotClip(plugin.ResolveCreator): """Publishable clip""" + identifier = "io.ayon.creators.resolve.clip" label = "Create Publishable Clip" product_type = "clip" icon = "film" defaults = ["Main"] - gui_tracks = get_video_track_names() - gui_name = "AYON publish attributes creator" - gui_info = "Define sequential rename and fill hierarchy data." - gui_inputs = { - "renameHierarchy": { - "type": "section", - "label": "Shot Hierarchy And Rename Settings", - "target": "ui", - "order": 0, - "value": { - "hierarchy": { - "value": "{folder}/{sequence}", - "type": "QLineEdit", - "label": "Shot Parent Hierarchy", - "target": "tag", - "toolTip": "Parents folder for shot root folder, Template filled with `Hierarchy Data` section", # noqa - "order": 0}, - "clipRename": { - "value": False, - "type": "QCheckBox", - "label": "Rename clips", - "target": "ui", - "toolTip": "Renaming selected clips on fly", # noqa - "order": 1}, - "clipName": { - "value": "{sequence}{shot}", - "type": "QLineEdit", - "label": "Clip Name Template", - "target": "ui", - "toolTip": "template for creating shot namespaused for renaming (use rename: on)", # noqa - "order": 2}, - "countFrom": { - "value": 10, - "type": "QSpinBox", - "label": "Count sequence from", - "target": "ui", - "toolTip": "Set when the sequence number stafrom", # noqa - "order": 3}, - "countSteps": { - "value": 10, - "type": "QSpinBox", - "label": "Stepping number", - "target": "ui", - "toolTip": "What number is adding every new step", # noqa - "order": 4}, - } - }, - "hierarchyData": { - "type": "dict", - "label": "Shot Template Keywords", - "target": "tag", - "order": 1, - "value": { - "folder": { - "value": "shots", - "type": "QLineEdit", - "label": "{folder}", - "target": "tag", - "toolTip": "Name of folder used for root of generated shots.\nUsable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa - "order": 0}, - "episode": { - "value": "ep01", - "type": "QLineEdit", - "label": "{episode}", - "target": "tag", - "toolTip": "Name of episode.\nUsable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa - "order": 1}, - "sequence": { - "value": "sq01", - "type": "QLineEdit", - "label": "{sequence}", - "target": "tag", - "toolTip": "Name of sequence of shots.\nUsable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa - "order": 2}, - "track": { - "value": "{_track_}", - "type": "QLineEdit", - "label": "{track}", - "target": "tag", - "toolTip": "Name of sequence of shots.\nUsable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa - "order": 3}, - "shot": { - "value": "sh###", - "type": "QLineEdit", - "label": "{shot}", - "target": "tag", - "toolTip": "Name of shot. `#` is converted to paded number. \nAlso could be used with usable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa - "order": 4} - } - }, - "verticalSync": { - "type": "section", - "label": "Vertical Synchronization Of Attributes", - "target": "ui", - "order": 2, - "value": { - "vSyncOn": { - "value": True, - "type": "QCheckBox", - "label": "Enable Vertical Sync", - "target": "ui", - "toolTip": "Switch on if you want clips above each other to share its attributes", # noqa - "order": 0}, - "vSyncTrack": { - "value": gui_tracks, # noqa - "type": "QComboBox", - "label": "Hero track", - "target": "ui", - "toolTip": "Select driving track name which should be mastering all others", # noqa - "order": 1 - } - } - }, - "publishSettings": { - "type": "section", - "label": "Publish Settings", - "target": "ui", - "order": 3, - "value": { - "productName": { - "value": ["", "main", "bg", "fg", "bg", - "animatic"], - "type": "QComboBox", - "label": "Product Name", - "target": "ui", - "toolTip": "chose product name pattern, if is selected, name of track layer will be used", # noqa - "order": 0}, - "productType": { - "value": ["plate", "take"], - "type": "QComboBox", - "label": "Product type", - "target": "ui", "toolTip": "What use of this product is for", # noqa - "order": 1}, - "reviewTrack": { - "value": ["< none >"] + gui_tracks, - "type": "QComboBox", - "label": "Use Review Track", - "target": "ui", - "toolTip": "Generate preview videos on fly, if `< none >` is defined nothing will be generated.", # noqa - "order": 2}, - "audio": { - "value": False, - "type": "QCheckBox", - "label": "Include audio", - "target": "tag", - "toolTip": "Process products with corresponding audio", # noqa - "order": 3}, - "sourceResolution": { - "value": False, - "type": "QCheckBox", - "label": "Source resolution", - "target": "tag", - "toolTip": "Is resloution taken from timeline or source?", # noqa - "order": 4}, - } - }, - "shotAttr": { - "type": "section", - "label": "Shot Attributes", - "target": "ui", - "order": 4, - "value": { - "workfileFrameStart": { - "value": 1001, - "type": "QSpinBox", - "label": "Workfiles Start Frame", - "target": "tag", - "toolTip": "Set workfile starting frame number", # noqa - "order": 0 - }, - "handleStart": { - "value": 0, - "type": "QSpinBox", - "label": "Handle start (head)", - "target": "tag", - "toolTip": "Handle at start of clip", # noqa - "order": 1 - }, - "handleEnd": { - "value": 0, - "type": "QSpinBox", - "label": "Handle end (tail)", - "target": "tag", - "toolTip": "Handle at end of clip", # noqa - "order": 2 - } - } - } - } + create_allow_context_change = False + create_allow_thumbnail = False + + def get_pre_create_attr_defs(self): + + def header_label(text): + return f"
{text}" + + tokens_help = """\nUsable tokens: + {_clip_}: name of used clip + {_track_}: name of parent track layer + {_sequence_}: name of parent sequence (timeline)""" + # gui_name = "OpenPype publish attributes creator" + # gui_info = "Define sequential rename and fill hierarchy data." + gui_tracks = get_video_track_names() + + # Project settings might be applied to this creator via + # the inherited `Creator.apply_settings` + presets = self.presets + + return [ + + BoolDef("use_selection", + label="Use only clips with Chocolate clip color", + tooltip=( + "When enabled only clips of Chocolate clip color are " + "considered.\n\n" + "Acts as a replacement to 'Use selection' because " + "Resolves API exposes no functionality to retrieve " + "the currently selected timeline items." + ), + default=True), + + # renameHierarchy + UILabelDef( + label=header_label("Shot Hierarchy And Rename Settings") + ), + TextDef( + "hierarchy", + label="Shot Parent Hierarchy", + tooltip="Parents folder for shot root folder, " + "Template filled with *Hierarchy Data* section", + default=presets.get("hierarchy", "{folder}/{sequence}"), + ), + BoolDef( + "clipRename", + label="Rename clips", + tooltip="Renaming selected clips on fly", + default=presets.get("clipRename", False), + ), + TextDef( + "clipName", + label="Clip Name Template", + tooltip="template for creating shot names, used for " + "renaming (use rename: on)", + default=presets.get("clipName", "{sequence}{shot}"), + ), + NumberDef( + "countFrom", + label="Count sequence from", + tooltip="Set where the sequence number starts from", + default=presets.get("countFrom", 10), + ), + NumberDef( + "countSteps", + label="Stepping number", + tooltip="What number is adding every new step", + default=presets.get("countSteps", 10), + ), + + # hierarchyData + UILabelDef( + label=header_label("Shot Template Keywords") + ), + TextDef( + "folder", + label="{folder}", + tooltip="Name of folder used for root of generated shots.\n" + f"{tokens_help}", + default=presets.get("folder", "shots"), + ), + TextDef( + "episode", + label="{episode}", + tooltip=f"Name of episode.\n{tokens_help}", + default=presets.get("episode", "ep01"), + ), + TextDef( + "sequence", + label="{sequence}", + tooltip=f"Name of sequence of shots.\n{tokens_help}", + default=presets.get("sequence", "sq01"), + ), + TextDef( + "track", + label="{track}", + tooltip=f"Name of timeline track.\n{tokens_help}", + default=presets.get("track", "{_track_}"), + ), + TextDef( + "shot", + label="{shot}", + tooltip="Name of shot. '#' is converted to padded number." + f"\n{tokens_help}", + default=presets.get("shot", "sh###"), + ), + + # verticalSync + UILabelDef( + label=header_label("Vertical Synchronization Of Attributes") + ), + BoolDef( + "vSyncOn", + label="Enable Vertical Sync", + tooltip="Switch on if you want clips above " + "each other to share its attributes", + default=presets.get("vSyncOn", True), + ), + EnumDef( + "vSyncTrack", + label="Hero track", + tooltip="Select driving track name which should " + "be mastering all others", + items=gui_tracks or [""], + ), + + # publishSettings + UILabelDef( + label=header_label("Publish Settings") + ), + EnumDef( + "variant", + label="Product Variant", + tooltip="Chose variant which will be then used for " + "product name, if " + "is selected, name of track layer will be used", + items=['', 'main', 'bg', 'fg', 'bg', 'animatic'], + ), + EnumDef( + "productType", + label="Product Type", + tooltip="How the product will be used", + items=['plate'], # it is prepared for more types + ), + EnumDef( + "reviewTrack", + label="Use Review Track", + tooltip="Generate preview videos on fly, if " + "'< none >' is defined nothing will be generated.", + items=['< none >'] + gui_tracks, + ), + BoolDef( + "audio", + label="Include audio", + tooltip="Process subsets with corresponding audio", + default=False, + ), + BoolDef( + "sourceResolution", + label="Source resolution", + tooltip="Is resoloution taken from timeline or source?", + default=False, + ), + + # shotAttr + UILabelDef( + label=header_label("Shot Attributes"), + ), + NumberDef( + "workfileFrameStart", + label="Workfiles Start Frame", + tooltip="Set workfile starting frame number", + default=presets.get("workfileFrameStart", 1001), + ), + NumberDef( + "handleStart", + label="Handle start (head)", + tooltip="Handle at start of clip", + default=presets.get("handleStart", 0), + ), + NumberDef( + "handleEnd", + label="Handle end (tail)", + tooltip="Handle at end of clip", + default=presets.get("handleEnd", 0), + ), + ] presets = None + rename_index = 0 - def process(self): - # get key pares from presets and match it on ui inputs - for k, v in self.gui_inputs.items(): - if v["type"] in ("dict", "section"): - # nested dictionary (only one level allowed - # for sections and dict) - for _k, _v in v["value"].items(): - if self.presets.get(_k) is not None: - self.gui_inputs[k][ - "value"][_k]["value"] = self.presets[_k] - if self.presets.get(k): - self.gui_inputs[k]["value"] = self.presets[k] - - # open widget for plugins inputs - widget = self.widget(self.gui_name, self.gui_info, self.gui_inputs) - widget.exec_() + def create(self, subset_name, instance_data, pre_create_data): + super(CreateShotClip, self).create(subset_name, + instance_data, + pre_create_data) if len(self.selected) < 1: return - if not widget.result: - print("Operation aborted") - return + self.log.info(self.selected) - self.rename_add = 0 + if not self.timeline: + raise CreatorError( + "You must be in an active timeline to " + "create the publishable clips.\n\n" + "Go into a timeline and then reset the publisher." + ) - # get ui output for track name for vertical sync - v_sync_track = widget.result["vSyncTrack"]["value"] + self.log.debug(f"Selected: {self.selected}") - # sort selected trackItems by + # sort selected trackItems by vSync track sorted_selected_track_items = [] unsorted_selected_track_items = [] - print("_____ selected ______") - print(self.selected) + v_sync_track = pre_create_data.get("vSyncTrack", "") for track_item_data in self.selected: if track_item_data["track"]["name"] in v_sync_track: sorted_selected_track_items.append(track_item_data) @@ -248,25 +240,98 @@ def process(self): sorted_selected_track_items.extend(unsorted_selected_track_items) - # sequence attrs - sq_frame_start = self.timeline.GetStartFrame() - sq_markers = self.timeline.GetMarkers() - # create media bin for compound clips (trackItems) - mp_folder = create_bin(self.timeline.GetName()) - - kwargs = { - "ui_inputs": widget.result, - "avalon": self.data, - "mp_folder": mp_folder, - "sq_frame_start": sq_frame_start, - "sq_markers": sq_markers - } - print(kwargs) - for i, track_item_data in enumerate(sorted_selected_track_items): - self.rename_index = i - self.log.info(track_item_data) + media_pool_folder = create_bin(self.timeline.GetName()) + + instances = [] + for index, track_item_data in enumerate(sorted_selected_track_items): + self.log.info( + "Processing track item data: {}".format(track_item_data) + ) + # convert track item to timeline media pool item - track_item = plugin.PublishClip( - self, track_item_data, **kwargs).convert() + publish_clip = plugin.PublishableClip( + track_item_data, + pre_create_data, + media_pool_folder, + rename_index=index, + data=instance_data + ) + + track_item = publish_clip.convert() + if track_item is None: + # Ignore input clips that do not convert into a track item + # from `PublishableClip.convert` + continue + track_item.SetClipColor(lib.publish_clip_color) + + instance_data = copy.deepcopy(instance_data) + # TODO: here we need to replicate Traypublisher Editorial workflow + # and create shot, plate, review, and audio instances with own + # dedicated plugin + + # Create the Publisher instance + instance = CreatedInstance( + product_type=self.product_type, + product_name=publish_clip.product_name, + data=instance_data, + creator=self + ) + instance.transient_data["track_item"] = track_item + self._add_instance_to_context(instance) + + # self.imprint_instance_node(instance_node, + # data=instance.data_to_store()) + instances.append(instance) + return instances + + def collect_instances(self): + """Collect all created instances from current timeline.""" + selected_timeline_items = lib.get_current_timeline_items( + filter=True, selecting_color=lib.publish_clip_color) + + instances = [] + for timeline_item_data in selected_timeline_items: + timeline_item = timeline_item_data["clip"]["item"] + + # get openpype tag data + tag_data = lib.get_timeline_item_pype_tag(timeline_item) + if not tag_data: + continue + + instance = CreatedInstance.from_existing(tag_data, self) + instance.transient_data["track_item"] = timeline_item + self._add_instance_to_context(instance) + + return instances + + def update_instances(self, update_list): + """Store changes of existing instances so they can be recollected. + + Args: + update_list(List[UpdateData]): Gets list of tuples. Each item + contain changed instance and it's changes. + """ + for created_inst, _changes in update_list: + track_item = created_inst.transient_data["track_item"] + data = created_inst.data_to_store() + self.log.info(f"Storing data: {data}") + + lib.imprint(track_item, data) + + def remove_instances(self, instances): + """Remove instance marker from track item. + + Args: + instance(List[CreatedInstance]): Instance objects which should be + removed. + """ + for instance in instances: + track_item = instance.transient_data["track_item"] + + # removing instance by marker color + print(f"Removing instance: {track_item.GetName()}") + track_item.DeleteMarkersByColor(lib.pype_marker_color) + + self._remove_instance_from_context(instance) From f634f19ebca21f4a53c6928245bc512471e28f78 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Mar 2024 16:35:59 +0100 Subject: [PATCH 06/21] Add workfile auto-creator plugin with instance creation logic. --- .../resolve/plugins/create/create_workfile.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 client/ayon_core/hosts/resolve/plugins/create/create_workfile.py diff --git a/client/ayon_core/hosts/resolve/plugins/create/create_workfile.py b/client/ayon_core/hosts/resolve/plugins/create/create_workfile.py new file mode 100644 index 0000000000..227a2ca500 --- /dev/null +++ b/client/ayon_core/hosts/resolve/plugins/create/create_workfile.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating workfiles.""" +from ayon_core.pipeline import CreatedInstance, AutoCreator +from ayon_core.client import get_asset_by_name + + +class CreateWorkfile(AutoCreator): + """Workfile auto-creator.""" + identifier = "io.ayon.creators.resolve.workfile" + label = "Workfile" + family = "workfile" + icon = "fa5.file" + + default_variant = "Main" + + def create(self): + + variant = self.default_variant + current_instance = next( + ( + instance for instance in self.create_context.instances + if instance.creator_identifier == self.identifier + ), None) + + project_name = self.project_name + asset_name = self.create_context.get_current_asset_name() + task_name = self.create_context.get_current_task_name() + host_name = self.create_context.host_name + + if current_instance is None: + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + variant, task_name, asset_doc, project_name, host_name + ) + data = { + "asset": asset_name, + "task": task_name, + "variant": variant, + } + data.update( + self.get_dynamic_data( + variant, task_name, asset_doc, + project_name, host_name, current_instance) + ) + self.log.info("Auto-creating workfile instance...") + current_instance = CreatedInstance( + self.family, subset_name, data, self + ) + self._add_instance_to_context(current_instance) + elif ( + current_instance["asset"] != asset_name + or current_instance["task"] != task_name + ): + # Update instance context if is not the same + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + variant, task_name, asset_doc, project_name, host_name + ) + current_instance["asset"] = asset_name + current_instance["task"] = task_name + current_instance["subset"] = subset_name + + def collect_instances(self): + # TODO: Implement + pass + + def update_instances(self, update_list): + # TODO: Implement + # This needs to be implemented to allow persisting any instance + # data on resets. We'll need to decide where to store workfile + # instance data reliably. Likely metadata on the *current project*? + pass From 666c76aa76f501986221565dd9f9041a4c13d3b7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Mar 2024 16:45:03 +0100 Subject: [PATCH 07/21] Update timeline frame rate settings and add otio metadata to Resolve project. Introduce plugin for collecting current project data and creating otio timeline. Refactor file extraction process for better representation handling. --- .../hosts/resolve/otio/davinci_export.py | 2 +- .../publish/collect_current_project.py | 32 +++++++++++ .../plugins/publish/extract_workfile.py | 22 ++++---- .../plugins/publish/precollect_workfile.py | 54 ------------------- 4 files changed, 44 insertions(+), 66 deletions(-) create mode 100644 client/ayon_core/hosts/resolve/plugins/publish/collect_current_project.py delete mode 100644 client/ayon_core/hosts/resolve/plugins/publish/precollect_workfile.py diff --git a/client/ayon_core/hosts/resolve/otio/davinci_export.py b/client/ayon_core/hosts/resolve/otio/davinci_export.py index 5f11c81fc5..ab1a03dddc 100644 --- a/client/ayon_core/hosts/resolve/otio/davinci_export.py +++ b/client/ayon_core/hosts/resolve/otio/davinci_export.py @@ -259,8 +259,8 @@ def add_otio_metadata(otio_item, media_pool_item, **kwargs): def create_otio_timeline(resolve_project): # get current timeline - self.project_fps = resolve_project.GetSetting("timelineFrameRate") timeline = resolve_project.GetCurrentTimeline() + self.project_fps = timeline.GetSetting("timelineFrameRate") # convert timeline to otio otio_timeline = _create_otio_timeline( diff --git a/client/ayon_core/hosts/resolve/plugins/publish/collect_current_project.py b/client/ayon_core/hosts/resolve/plugins/publish/collect_current_project.py new file mode 100644 index 0000000000..27605593ce --- /dev/null +++ b/client/ayon_core/hosts/resolve/plugins/publish/collect_current_project.py @@ -0,0 +1,32 @@ +import pyblish.api + +from ayon_core.hosts.resolve import api as rapi +from ayon_core.hosts.resolve.otio import davinci_export + + +class CollectResolveProject(pyblish.api.ContextPlugin): + """Collect the current Resolve project and current timeline data""" + + label = "Collect Project and Current Timeline" + order = pyblish.api.CollectorOrder - 0.499 + + def process(self, context): + resolve_project = rapi.get_current_project() + timeline = resolve_project.GetCurrentTimeline() + fps = timeline.GetSetting("timelineFrameRate") + + video_tracks = rapi.get_video_track_names() + + # adding otio timeline to context + otio_timeline = davinci_export.create_otio_timeline(resolve_project) + + # update context with main project attributes + context.data.update({ + # project + "activeProject": resolve_project, + "currentFile": resolve_project.GetName(), + # timeline + "otioTimeline": otio_timeline, + "videoTracks": video_tracks, + "fps": fps, + }) diff --git a/client/ayon_core/hosts/resolve/plugins/publish/extract_workfile.py b/client/ayon_core/hosts/resolve/plugins/publish/extract_workfile.py index 48ebdee7e3..3d6f743900 100644 --- a/client/ayon_core/hosts/resolve/plugins/publish/extract_workfile.py +++ b/client/ayon_core/hosts/resolve/plugins/publish/extract_workfile.py @@ -1,8 +1,8 @@ import os import pyblish.api -from ayon_core.pipeline import publish -from ayon_core.hosts.resolve.api.lib import get_project_manager +from openpype.pipeline import publish +from openpype.hosts.resolve.api.lib import get_project_manager class ExtractWorkfile(publish.Extractor): @@ -24,9 +24,8 @@ def process(self, instance): project = instance.context.data["activeProject"] staging_dir = self.staging_dir(instance) - resolve_workfile_ext = ".drp" - drp_file_name = name + resolve_workfile_ext - + ext = ".drp" + drp_file_name = name + ext drp_file_path = os.path.normpath( os.path.join(staging_dir, drp_file_name)) @@ -36,17 +35,18 @@ def process(self, instance): # create drp workfile representation representation_drp = { - 'name': resolve_workfile_ext[1:], - 'ext': resolve_workfile_ext[1:], + 'name': ext.lstrip("."), + 'ext': ext.lstrip("."), 'files': drp_file_name, "stagingDir": staging_dir, } - - instance.data["representations"].append(representation_drp) + representations = instance.data.setdefault("representations", []) + representations.append(representation_drp) # add sourcePath attribute to instance if not instance.data.get("sourcePath"): instance.data["sourcePath"] = drp_file_path - self.log.info("Added Resolve file representation: {}".format( - representation_drp)) + self.log.debug( + "Added Resolve file representation: {}".format(representation_drp) + ) diff --git a/client/ayon_core/hosts/resolve/plugins/publish/precollect_workfile.py b/client/ayon_core/hosts/resolve/plugins/publish/precollect_workfile.py deleted file mode 100644 index 6b9060d942..0000000000 --- a/client/ayon_core/hosts/resolve/plugins/publish/precollect_workfile.py +++ /dev/null @@ -1,54 +0,0 @@ -import pyblish.api -from pprint import pformat - -from ayon_core.pipeline import get_current_asset_name - -from ayon_core.hosts.resolve import api as rapi -from ayon_core.hosts.resolve.otio import davinci_export - - -class PrecollectWorkfile(pyblish.api.ContextPlugin): - """Precollect the current working file into context""" - - label = "Precollect Workfile" - order = pyblish.api.CollectorOrder - 0.5 - - def process(self, context): - current_asset_name = get_current_asset_name() - asset_name = current_asset_name.split("/")[-1] - - product_name = "workfileMain" - resolve_project = rapi.get_current_resolve_project() - fps = resolve_project.GetSetting("timelineFrameRate") - video_tracks = rapi.get_video_track_names() - - # adding otio timeline to context - otio_timeline = davinci_export.create_otio_timeline(resolve_project) - - instance_data = { - "name": "{}_{}".format(asset_name, product_name), - "label": "{} {}".format(current_asset_name, product_name), - "item": resolve_project, - "folderPath": current_asset_name, - "productName": product_name, - "productType": "workfile", - "family": "workfile", - "families": [] - } - - # create instance with workfile - instance = context.create_instance(**instance_data) - - # update context with main project attributes - context_data = { - "activeProject": resolve_project, - "otioTimeline": otio_timeline, - "videoTracks": video_tracks, - "currentFile": resolve_project.GetName(), - "fps": fps, - } - context.data.update(context_data) - - self.log.info("Creating instance: {}".format(instance)) - self.log.debug("__ instance.data: {}".format(pformat(instance.data))) - self.log.debug("__ context_data: {}".format(pformat(context_data))) From 6c42394d7261720b8bc3562aa955ed3ff508e5e9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Mar 2024 16:52:22 +0100 Subject: [PATCH 08/21] Refactor timeline item retrieval functions and menu launchers - Updated function names for consistency and clarity - Renamed menu launcher functions to reflect the current context - Removed unnecessary variables and commented-out code in lib.py --- .../ayon_core/hosts/resolve/api/__init__.py | 10 +- client/ayon_core/hosts/resolve/api/lib.py | 306 ++++++++++-------- client/ayon_core/hosts/resolve/api/menu.py | 8 +- .../plugins/create/create_shot_clip.py | 2 +- client/ayon_core/hosts/resolve/startup.py | 2 +- .../resolve/utility_scripts/AYON__Menu.py | 4 +- 6 files changed, 180 insertions(+), 152 deletions(-) diff --git a/client/ayon_core/hosts/resolve/api/__init__.py b/client/ayon_core/hosts/resolve/api/__init__.py index a8d285082c..711d0c4a71 100644 --- a/client/ayon_core/hosts/resolve/api/__init__.py +++ b/client/ayon_core/hosts/resolve/api/__init__.py @@ -29,7 +29,8 @@ get_timeline_item, get_video_track_names, get_current_timeline_items, - get_pype_timeline_item_by_name, + get_timeline_item_by_name, + get_pype_timeline_item_by_name, # backward compatibility get_timeline_item_ayon_tag, get_timeline_item_pype_tag, # backward compatibility set_timeline_item_ayon_tag, @@ -45,7 +46,7 @@ get_reformated_path ) -from .menu import launch_pype_menu +from .menu import launch_ayon_menu from .plugin import ( ClipLoader, @@ -106,7 +107,8 @@ "get_timeline_item", "get_video_track_names", "get_current_timeline_items", - "get_pype_timeline_item_by_name", + "get_timeline_item_by_name", + "get_pype_timeline_item_by_name", # backward compatibility "get_timeline_item_ayon_tag", "get_timeline_item_pype_tag", # backward compatibility "set_timeline_item_ayon_tag", @@ -122,7 +124,7 @@ "get_reformated_path", # menu - "launch_pype_menu", + "launch_ayon_menu", # plugin "ClipLoader", diff --git a/client/ayon_core/hosts/resolve/api/lib.py b/client/ayon_core/hosts/resolve/api/lib.py index 6e4e17811f..42dc9500bd 100644 --- a/client/ayon_core/hosts/resolve/api/lib.py +++ b/client/ayon_core/hosts/resolve/api/lib.py @@ -1,4 +1,3 @@ -import sys import json import re import os @@ -10,34 +9,11 @@ is_overlapping_otio_ranges, frames_to_timecode ) - +from . import constants from ..otio import davinci_export as otio_export log = Logger.get_logger(__name__) -self = sys.modules[__name__] -self.project_manager = None -self.media_storage = None - -# OpenPype sequential rename variables -self.rename_index = 0 -self.rename_add = 0 - -self.publish_clip_color = "Pink" -self.pype_marker_workflow = True - -# OpenPype compound clip workflow variable -self.pype_tag_name = "VFX Notes" - -# OpenPype marker workflow variables -self.pype_marker_name = "OpenPypeData" -self.pype_marker_duration = 1 -self.pype_marker_color = "Mint" -self.temp_marker_frame = None - -# OpenPype default timeline -self.pype_timeline_name = "OpenPypeTimeline" - @contextlib.contextmanager def maintain_current_timeline(to_timeline: object, @@ -59,38 +35,56 @@ def maintain_current_timeline(to_timeline: object, >>> print(get_current_timeline().GetName()) timeline1 """ - project = get_current_project() - working_timeline = from_timeline or project.GetCurrentTimeline() + resolve_project = get_current_resolve_project() + working_timeline = from_timeline or resolve_project.GetCurrentTimeline() # switch to the input timeline - project.SetCurrentTimeline(to_timeline) + resolve_project.SetCurrentTimeline(to_timeline) try: # do a work yield finally: # put the original working timeline to context - project.SetCurrentTimeline(working_timeline) + resolve_project.SetCurrentTimeline(working_timeline) def get_project_manager(): - from . import bmdvr - if not self.project_manager: - self.project_manager = bmdvr.GetProjectManager() - return self.project_manager + """Get project manager object. + + Returns: + resolve.ProjectManager + """ + from . import bmdvr, project_manager + if not project_manager: + project_manager = bmdvr.GetProjectManager() + + return project_manager def get_media_storage(): - from . import bmdvr - if not self.media_storage: - self.media_storage = bmdvr.GetMediaStorage() - return self.media_storage + """Get media storage object. + + Returns: + resolve.MediaStorage + """ + from . import bmdvr, media_storage + if not media_storage: + media_storage = bmdvr.GetMediaStorage() + return media_storage + +def get_current_resolve_project(): + """Get current resolve project object. -def get_current_project(): - """Get current project object. + Returns: + resolve.Project """ - return get_project_manager().GetCurrentProject() + project_manager = get_project_manager() + return project_manager.GetCurrentProject() + +# alias for backward compatibility +get_current_project = get_current_resolve_project def get_current_timeline(new=False): @@ -104,8 +98,8 @@ def get_current_timeline(new=False): TODO: will need to reflect future `None` object: resolve.Timeline """ - project = get_current_project() - timeline = project.GetCurrentTimeline() + resolve_project = get_current_resolve_project() + timeline = resolve_project.GetCurrentTimeline() # return current timeline if any if timeline: @@ -122,10 +116,10 @@ def get_any_timeline(): Returns: object | None: resolve.Timeline """ - project = get_current_project() - timeline_count = project.GetTimelineCount() + resolve_project = get_current_resolve_project() + timeline_count = resolve_project.GetTimelineCount() if timeline_count > 0: - return project.GetTimelineByIndex(1) + return resolve_project.GetTimelineByIndex(1) def get_new_timeline(timeline_name: str = None): @@ -137,11 +131,11 @@ def get_new_timeline(timeline_name: str = None): Returns: object: resolve.Timeline """ - project = get_current_project() - media_pool = project.GetMediaPool() + resolve_project = get_current_resolve_project() + media_pool = resolve_project.GetMediaPool() new_timeline = media_pool.CreateEmptyTimeline( - timeline_name or self.pype_timeline_name) - project.SetCurrentTimeline(new_timeline) + timeline_name or constants.ayon_timeline_name) + resolve_project.SetCurrentTimeline(new_timeline) return new_timeline @@ -161,7 +155,7 @@ def create_bin(name: str, root: object = None) -> object: object: resolve.Folder """ # get all variables - media_pool = get_current_project().GetMediaPool() + media_pool = get_current_resolve_project().GetMediaPool() root_bin = root or media_pool.GetRootFolder() # create hierarchy of bins in case there is slash in name @@ -187,43 +181,58 @@ def create_bin(name: 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]) + """Remove media pool item. + Args: + media_pool_item (resolve.MediaPoolItem): resolve's object -def create_media_pool_item( - files: list, - root: object = None, -) -> object: + Returns: + bool: True if success """ - Create media pool item. + resolve_project = get_current_resolve_project() + media_pool = resolve_project.GetMediaPool() + return media_pool.DeleteClips([media_pool_item]) + + +def create_media_pool_item(fpath: str, + root: object = None) -> object: + """ Create media pool item. Args: - files (list[str]): list of absolute paths to files + fpath (str): absolute path to a file root (resolve.Folder)[optional]: root folder / bin object Returns: object: resolve.MediaPoolItem """ # get all variables - media_pool = get_current_project().GetMediaPool() + media_storage = get_media_storage() + resolve_project = get_current_resolve_project() + media_pool = resolve_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(filepath, root_bin) + existing_mpi = get_media_pool_item(fpath, root_bin) if existing_mpi: return existing_mpi - # add all data in folder to media pool - media_pool_items = media_pool.ImportMedia(files) + dirname, file = os.path.split(fpath) + _name, ext = os.path.splitext(file) + + # add all data in folder to media-pool + media_pool_items = media_storage.AddItemListToMediaPool( + os.path.normpath(dirname)) + + if not media_pool_items: + return False - return media_pool_items.pop() if media_pool_items else 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() def get_media_pool_item(filepath, root: object = None) -> object: @@ -237,7 +246,8 @@ def get_media_pool_item(filepath, root: object = None) -> object: Returns: object: resolve.MediaPoolItem """ - media_pool = get_current_project().GetMediaPool() + resolve_project = get_current_resolve_project() + media_pool = resolve_project.GetMediaPool() root = root or media_pool.GetRootFolder() fname = os.path.basename(filepath) @@ -270,8 +280,8 @@ def create_timeline_item( object: resolve.TimelineItem """ # get all variables - project = get_current_project() - media_pool = project.GetMediaPool() + resolve_project = get_current_resolve_project() + media_pool = resolve_project.GetMediaPool() _clip_property = media_pool_item.GetClipProperty clip_name = _clip_property("File Name") timeline = timeline or get_current_timeline() @@ -348,15 +358,17 @@ def get_timeline_item(media_pool_item: object, def get_video_track_names() -> list: - tracks = list() - track_type = "video" timeline = get_current_timeline() + if not timeline: + return [] + + track_type = "video" # get all tracks count filtered by track type selected_track_count = timeline.GetTrackCount(track_type) # loop all tracks and get items - track_index: int + tracks = [] for track_index in range(1, (int(selected_track_count) + 1)): track_name = timeline.GetTrackName("video", track_index) tracks.append(track_name) @@ -373,7 +385,7 @@ def get_current_timeline_items( """ track_type = track_type or "video" selecting_color = selecting_color or "Chocolate" - project = get_current_project() + resolve_project = get_current_resolve_project() # get timeline anyhow timeline = ( @@ -391,7 +403,7 @@ def get_current_timeline_items( for track_index in range(1, (int(selected_track_count) + 1)): _track_name = timeline.GetTrackName(track_type, track_index) - # filter out all unmathed track names + # filter out all unmatched track names if track_name and _track_name not in track_name: continue @@ -400,7 +412,7 @@ def get_current_timeline_items( _clips[track_index] = timeline_items _data = { - "project": project, + "project": resolve_project, "timeline": timeline, "track": { "name": _track_name, @@ -420,7 +432,7 @@ def get_current_timeline_items( return selected_clips -def get_pype_timeline_item_by_name(name: str) -> object: +def get_timeline_item_by_name(name: str) -> object: """Get timeline item by name. Args: @@ -431,7 +443,7 @@ def get_pype_timeline_item_by_name(name: str) -> object: """ for _ti_data in get_current_timeline_items(): _ti_clip = _ti_data["clip"]["item"] - tag_data = get_timeline_item_pype_tag(_ti_clip) + tag_data = get_timeline_item_ayon_tag(_ti_clip) tag_name = tag_data.get("namespace") if not tag_name: continue @@ -440,20 +452,24 @@ def get_pype_timeline_item_by_name(name: str) -> object: return None -def get_timeline_item_pype_tag(timeline_item): +# alias for backward compatibility +get_pype_timeline_item_by_name = get_timeline_item_by_name + + +def get_timeline_item_ayon_tag(timeline_item): """ - Get openpype track item tag created by creator or loader plugin. + Get ayon track item tag created by creator or loader plugin. Attributes: trackItem (resolve.TimelineItem): resolve object Returns: - dict: openpype tag data + dict: ayon tag data """ return_tag = None - if self.pype_marker_workflow: - return_tag = get_pype_marker(timeline_item) + if constants.ayon_marker_workflow: + return_tag = get_ayon_marker(timeline_item) else: media_pool_item = timeline_item.GetMediaPoolItem() @@ -463,15 +479,18 @@ def get_timeline_item_pype_tag(timeline_item): return None for key, data in _tags.items(): # return only correct tag defined by global name - if key in self.pype_tag_name: + if key in constants.ayon_tag_name: return_tag = json.loads(data) return return_tag +# alias for backward compatibility +get_timeline_item_pype_tag = get_timeline_item_ayon_tag + -def set_timeline_item_pype_tag(timeline_item, data=None): +def set_timeline_item_ayon_tag(timeline_item, data=None): """ - Set openpype track item tag to input timeline_item. + Set ayon track item tag to input timeline_item. Attributes: trackItem (resolve.TimelineItem): resolve api object @@ -479,33 +498,36 @@ def set_timeline_item_pype_tag(timeline_item, data=None): Returns: dict: json loaded data """ - data = data or dict() + data = data or {} - # get available openpype tag if any - tag_data = get_timeline_item_pype_tag(timeline_item) + # get available ayon tag if any + tag_data = get_timeline_item_ayon_tag(timeline_item) - if self.pype_marker_workflow: + if constants.ayon_marker_workflow: # delete tag as it is not updatable if tag_data: - delete_pype_marker(timeline_item) + delete_ayon_marker(timeline_item) tag_data.update(data) - set_pype_marker(timeline_item, tag_data) + set_ayon_marker(timeline_item, tag_data) else: if tag_data: media_pool_item = timeline_item.GetMediaPoolItem() # it not tag then create one tag_data.update(data) media_pool_item.SetMetadata( - self.pype_tag_name, json.dumps(tag_data)) + constants.ayon_tag_name, json.dumps(tag_data)) else: tag_data = data - # if openpype tag available then update with input data + # if ayon tag available then update with input data # add it to the input track item - timeline_item.SetMetadata(self.pype_tag_name, json.dumps(tag_data)) + timeline_item.SetMetadata(constants.ayon_tag_name, json.dumps(tag_data)) return tag_data +# alias for backward compatibility +set_timeline_item_pype_tag = set_timeline_item_ayon_tag + def imprint(timeline_item, data=None): """ @@ -515,18 +537,18 @@ def imprint(timeline_item, data=None): Arguments: timeline_item (hiero.core.TrackItem): hiero track item object - data (dict): Any data which needs to be imprinted + data (dict): Any data which needst to be imprinted Examples: data = { - 'folderPath': 'sq020sh0280', - 'productType': 'render', - 'productName': 'productMain' + 'asset': 'sq020sh0280', + 'family': 'render', + 'subset': 'subsetMain' } """ data = data or {} - set_timeline_item_pype_tag(timeline_item, data) + set_timeline_item_ayon_tag(timeline_item, data) # add publish attribute set_publish_attribute(timeline_item, True) @@ -539,10 +561,10 @@ def set_publish_attribute(timeline_item, value): tag (hiero.core.Tag): a tag object value (bool): True or False """ - tag_data = get_timeline_item_pype_tag(timeline_item) + tag_data = get_timeline_item_ayon_tag(timeline_item) tag_data["publish"] = value # set data to the publish attribute - set_timeline_item_pype_tag(timeline_item, tag_data) + set_timeline_item_ayon_tag(timeline_item, tag_data) def get_publish_attribute(timeline_item): @@ -552,21 +574,21 @@ def get_publish_attribute(timeline_item): tag (hiero.core.Tag): a tag object value (bool): True or False """ - tag_data = get_timeline_item_pype_tag(timeline_item) + tag_data = get_timeline_item_ayon_tag(timeline_item) return tag_data["publish"] -def set_pype_marker(timeline_item, tag_data): +def set_ayon_marker(timeline_item, tag_data): source_start = timeline_item.GetLeftOffset() item_duration = timeline_item.GetDuration() frame = int(source_start + (item_duration / 2)) # marker attributes frameId = (frame / 10) * 10 - color = self.pype_marker_color - name = self.pype_marker_name + color = constants.ayon_marker_color + name = constants.ayon_marker_name note = json.dumps(tag_data) - duration = (self.pype_marker_duration / 10) * 10 + duration = (constants.ayon_marker_duration / 10) * 10 timeline_item.AddMarker( frameId, @@ -577,22 +599,26 @@ def set_pype_marker(timeline_item, tag_data): ) -def get_pype_marker(timeline_item): +def get_ayon_marker(timeline_item): timeline_item_markers = timeline_item.GetMarkers() - 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 + 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}") + if ( + name == constants.ayon_marker_name + and color == constants.ayon_marker_color + ): + constants.temp_marker_frame = marker_frame return json.loads(note) - return dict() + return {} -def delete_pype_marker(timeline_item): - timeline_item.DeleteMarkerAtFrame(self.temp_marker_frame) - self.temp_marker_frame = None +def delete_ayon_marker(timeline_item): + timeline_item.DeleteMarkerAtFrame(constants.temp_marker_frame) + constants.temp_marker_frame = None def create_compound_clip(clip_data, name, folder): @@ -609,14 +635,14 @@ def create_compound_clip(clip_data, name, folder): resolve.MediaPoolItem: media pool item with compound clip timeline(cct) """ # get basic objects form data - project = clip_data["project"] + resolve_project = clip_data["project"] timeline = clip_data["timeline"] clip = clip_data["clip"] # get details of objects clip_item = clip["item"] - mp = project.GetMediaPool() + mp = resolve_project.GetMediaPool() # get clip attributes clip_attributes = get_clip_attributes(clip_item) @@ -652,7 +678,7 @@ def create_compound_clip(clip_data, name, folder): if c.GetName() in name), None) if cct: - print(f"Compound clip exists: {cct}") + print(f"_ cct exists: {cct}") else: # Create empty timeline in current folder and give name: cct = mp.CreateEmptyTimeline(name) @@ -661,7 +687,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"Compound clip created: {cct}") + print(f"_ cct created: {cct}") with maintain_current_timeline(cct, tl_origin): # Add input clip to the current timeline: @@ -671,10 +697,10 @@ def create_compound_clip(clip_data, name, folder): "endFrame": mp_last_frame }]) - # Add collected metadata and attributes to the comound clip: - if mp_item.GetMetadata(self.pype_tag_name): - clip_attributes[self.pype_tag_name] = mp_item.GetMetadata( - self.pype_tag_name)[self.pype_tag_name] + # Add collected metadata and attributes to the compound clip: + if mp_item.GetMetadata(constants.ayon_tag_name): + clip_attributes[constants.ayon_tag_name] = mp_item.GetMetadata( + constants.ayon_tag_name)[constants.ayon_tag_name] # stringify clip_attributes = json.dumps(clip_attributes) @@ -684,7 +710,7 @@ def create_compound_clip(clip_data, name, folder): cct.SetMetadata(k, v) # add metadata to cct - cct.SetMetadata(self.pype_tag_name, clip_attributes) + cct.SetMetadata(constants.ayon_tag_name, clip_attributes) # reset start timecode of the compound clip cct.SetClipProperty("Start TC", _mp_props("Start TC")) @@ -753,7 +779,7 @@ def _validate_tc(x): def get_pype_clip_metadata(clip): """ - Get openpype metadata created by creator plugin + Get ayon metadata created by creator plugin Attributes: clip (resolve.TimelineItem): resolve's object @@ -764,7 +790,7 @@ def get_pype_clip_metadata(clip): mp_item = clip.GetMediaPoolItem() metadata = mp_item.GetMetadata() - return metadata.get(self.pype_tag_name) + return metadata.get(constants.ayon_tag_name) def get_clip_attributes(clip): @@ -809,21 +835,21 @@ def set_project_manager_to_folder_name(folder_name): """ # initialize project manager - get_project_manager() + project_manager = get_project_manager() set_folder = False # go back to root folder - if self.project_manager.GotoRootFolder(): + if project_manager.GotoRootFolder(): log.info(f"Testing existing folder: {folder_name}") folders = _convert_resolve_list_type( - self.project_manager.GetFoldersInCurrentFolder()) + project_manager.GetFoldersInCurrentFolder()) log.info(f"Testing existing folders: {folders}") # get me first available folder object # with the same name as in `folder_name` else return False if next((f for f in folders if f in folder_name), False): log.info(f"Found existing folder: {folder_name}") - set_folder = self.project_manager.OpenFolder(folder_name) + set_folder = project_manager.OpenFolder(folder_name) if set_folder: return True @@ -831,11 +857,11 @@ def set_project_manager_to_folder_name(folder_name): # if folder by name is not existent then create one # go back to root folder log.info(f"Folder `{folder_name}` not found and will be created") - if self.project_manager.GotoRootFolder(): + if project_manager.GotoRootFolder(): try: # create folder by given name - self.project_manager.CreateFolder(folder_name) - self.project_manager.OpenFolder(folder_name) + project_manager.CreateFolder(folder_name) + project_manager.OpenFolder(folder_name) return True except NameError as e: log.error((f"Folder with name `{folder_name}` cannot be created!" @@ -856,13 +882,13 @@ def _convert_resolve_list_type(resolve_list): def create_otio_time_range_from_timeline_item_data(timeline_item_data): timeline_item = timeline_item_data["clip"]["item"] - project = timeline_item_data["project"] + resolve_project = timeline_item_data["project"] timeline = timeline_item_data["timeline"] timeline_start = timeline.GetStartFrame() frame_start = int(timeline_item.GetStart() - timeline_start) frame_duration = int(timeline_item.GetDuration()) - fps = project.GetSetting("timelineFrameRate") + fps = resolve_project.GetSetting("timelineFrameRate") return otio_export.create_otio_time_range( frame_start, frame_duration, fps) @@ -899,7 +925,7 @@ def get_otio_clip_instance_data(otio_timeline, timeline_item_data): # add pypedata marker to otio_clip metadata for marker in otio_clip.markers: - if self.pype_marker_name in marker.name: + if constants.ayon_marker_name in marker.name: otio_clip.metadata.update(marker.metadata) return {"otioClip": otio_clip} diff --git a/client/ayon_core/hosts/resolve/api/menu.py b/client/ayon_core/hosts/resolve/api/menu.py index ce0ca386e4..62b5b43b23 100644 --- a/client/ayon_core/hosts/resolve/api/menu.py +++ b/client/ayon_core/hosts/resolve/api/menu.py @@ -125,14 +125,14 @@ def on_set_resolution_clicked(self): print("Clicked Set Resolution") -def launch_pype_menu(): +def launch_ayon_menu(): app = QtWidgets.QApplication(sys.argv) - pype_menu = AyonMenu() + ayon_menu = AyonMenu() stylesheet = load_stylesheet() - pype_menu.setStyleSheet(stylesheet) + ayon_menu.setStyleSheet(stylesheet) - pype_menu.show() + ayon_menu.show() sys.exit(app.exec_()) diff --git a/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py b/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py index 4f6cc6609c..7dd9872120 100644 --- a/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py +++ b/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py @@ -296,7 +296,7 @@ def collect_instances(self): timeline_item = timeline_item_data["clip"]["item"] # get openpype tag data - tag_data = lib.get_timeline_item_pype_tag(timeline_item) + tag_data = lib.get_timeline_item_ayon_tag(timeline_item) if not tag_data: continue diff --git a/client/ayon_core/hosts/resolve/startup.py b/client/ayon_core/hosts/resolve/startup.py index b3c1a024d9..3ad0a6bf7b 100644 --- a/client/ayon_core/hosts/resolve/startup.py +++ b/client/ayon_core/hosts/resolve/startup.py @@ -35,7 +35,7 @@ def ensure_installed_host(): def launch_menu(): print("Launching Resolve AYON menu..") ensure_installed_host() - ayon_core.hosts.resolve.api.launch_pype_menu() + ayon_core.hosts.resolve.api.launch_ayon_menu() def open_workfile(path): diff --git a/client/ayon_core/hosts/resolve/utility_scripts/AYON__Menu.py b/client/ayon_core/hosts/resolve/utility_scripts/AYON__Menu.py index 08cefb9d61..b10b477beb 100644 --- a/client/ayon_core/hosts/resolve/utility_scripts/AYON__Menu.py +++ b/client/ayon_core/hosts/resolve/utility_scripts/AYON__Menu.py @@ -8,13 +8,13 @@ def main(env): - from ayon_core.hosts.resolve.api import ResolveHost, launch_pype_menu + from ayon_core.hosts.resolve.api import ResolveHost, launch_ayon_menu # activate resolve from openpype host = ResolveHost() install_host(host) - launch_pype_menu() + launch_ayon_menu() if __name__ == "__main__": From 18ccc00433242774bec04aa33346d019f57957e0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Mar 2024 17:08:36 +0100 Subject: [PATCH 09/21] Update Resolve API functions, containerization methods, and marker handling. Adjust naming conventions for Ayon compatibility. --- .../ayon_core/hosts/resolve/README.markdown | 6 ++-- client/ayon_core/hosts/resolve/api/lib.py | 20 ++++++------- .../ayon_core/hosts/resolve/api/pipeline.py | 28 ++++++------------- .../hosts/resolve/api/todo-rendering.py | 2 +- .../hosts/resolve/hooks/pre_resolve_setup.py | 4 +-- .../plugins/create/create_shot_clip.py | 2 -- .../hosts/resolve/plugins/load/load_clip.py | 2 +- .../plugins/publish/precollect_instances.py | 2 +- .../utility_scripts/ayon_startup.scriptlib | 4 +-- 9 files changed, 29 insertions(+), 41 deletions(-) diff --git a/client/ayon_core/hosts/resolve/README.markdown b/client/ayon_core/hosts/resolve/README.markdown index a8bb071e7e..b16a654538 100644 --- a/client/ayon_core/hosts/resolve/README.markdown +++ b/client/ayon_core/hosts/resolve/README.markdown @@ -10,7 +10,7 @@ ![image](https://user-images.githubusercontent.com/40640033/102792588-ffcb1c80-43a8-11eb-9c6b-bf2114ed578e.png) with installed CMake in PATH. - make sure Resolve Fusion (Fusion Tab/menu/Fusion/Fusion Settings) is set to Python 3.6 ![image](https://user-images.githubusercontent.com/40640033/102631545-280b0f00-414e-11eb-89fc-98ac268d209d.png) -- Open OpenPype **Tray/Admin/Studio settings** > `applications/resolve/environment` and add Python3 path to `RESOLVE_PYTHON3_HOME` platform related. +- Open Ayon **Tray/Admin/Studio settings** > `applications/resolve/environment` and add Python3 path to `RESOLVE_PYTHON3_HOME` platform related. ## Editorial setup @@ -18,9 +18,9 @@ This is how it looks on my testing project timeline ![image](https://user-images.githubusercontent.com/40640033/102637638-96ec6600-4156-11eb-9656-6e8e3ce4baf8.png) Notice I had renamed tracks to `main` (holding metadata markers) and `review` used for generating review data with ffmpeg confersion to jpg sequence. -1. you need to start OpenPype menu from Resolve/EditTab/Menu/Workspace/Scripts/Comp/**__OpenPype_Menu__** +1. you need to start Ayon menu from Resolve/EditTab/Menu/Workspace/Scripts/Comp/**__Ayon_Menu__** 2. then select any clips in `main` track and change their color to `Chocolate` -3. in OpenPype Menu select `Create` +3. in Ayon Menu select `Create` 4. in Creator select `Create Publishable Clip [New]` (temporary name) 5. set `Rename clips` to True, Master Track to `main` and Use review track to `review` as in picture ![image](https://user-images.githubusercontent.com/40640033/102643773-0d419600-4160-11eb-919e-9c2be0aecab8.png) diff --git a/client/ayon_core/hosts/resolve/api/lib.py b/client/ayon_core/hosts/resolve/api/lib.py index 42dc9500bd..fe7c110ed7 100644 --- a/client/ayon_core/hosts/resolve/api/lib.py +++ b/client/ayon_core/hosts/resolve/api/lib.py @@ -521,23 +521,25 @@ def set_timeline_item_ayon_tag(timeline_item, data=None): tag_data = data # if ayon tag available then update with input data # add it to the input track item - timeline_item.SetMetadata(constants.ayon_tag_name, json.dumps(tag_data)) + timeline_item.SetMetadata( + constants.ayon_tag_name, json.dumps(tag_data)) return tag_data + # alias for backward compatibility set_timeline_item_pype_tag = set_timeline_item_ayon_tag def imprint(timeline_item, data=None): """ - Adding `Avalon data` into a hiero track item tag. + Adding `Ayon data` into a timeline item track item tag. Also including publish attribute into tag. Arguments: - timeline_item (hiero.core.TrackItem): hiero track item object - data (dict): Any data which needst to be imprinted + timeline_item (resolve.TimelineItem): resolve's object + data (dict): Any data which needs to be imprinted Examples: data = { @@ -555,11 +557,10 @@ def imprint(timeline_item, data=None): def set_publish_attribute(timeline_item, value): - """ Set Publish attribute in input Tag object + """ Set Publish attribute to marker on timeline item Attribute: - tag (hiero.core.Tag): a tag object - value (bool): True or False + timeline_item (resolve.TimelineItem): resolve's object """ tag_data = get_timeline_item_ayon_tag(timeline_item) tag_data["publish"] = value @@ -568,11 +569,10 @@ def set_publish_attribute(timeline_item, value): def get_publish_attribute(timeline_item): - """ Get Publish attribute from input Tag object + """ Get Publish attribute from marker on timeline item Attribute: - tag (hiero.core.Tag): a tag object - value (bool): True or False + timeline_item (resolve.TimelineItem): resolve's object """ tag_data = get_timeline_item_ayon_tag(timeline_item) return tag_data["publish"] diff --git a/client/ayon_core/hosts/resolve/api/pipeline.py b/client/ayon_core/hosts/resolve/api/pipeline.py index bc034639cb..9665ca7d64 100644 --- a/client/ayon_core/hosts/resolve/api/pipeline.py +++ b/client/ayon_core/hosts/resolve/api/pipeline.py @@ -95,19 +95,9 @@ def get_containers(self): return ls() def get_context_data(self): - # data = cmds.fileInfo("OpenPypeContext", query=True) - # if not data: - # return {} - # - # data = data[0] # Maya seems to return a list - # decoded = base64.b64decode(data).decode("utf-8") - # return json.loads(decoded) return {} def update_context_data(self, data, changes): - # json_str = json.dumps(data) - # encoded = base64.b64encode(json_str.encode("utf-8")) - # return cmds.fileInfo("OpenPypeContext", encoded) pass @@ -117,20 +107,20 @@ def containerise(timeline_item, context, loader=None, data=None): - """Bundle Hiero's object into an assembly and imprint it with metadata + """Bundle Resolve's object into an assembly and imprint it with metadata - Containerisation enables a tracking of version, author and origin + Containerization enables a tracking of version, author and origin for loaded assets. Arguments: - timeline_item (hiero.core.TrackItem): object to imprint as container + timeline_item (resolve.TimelineItem): The object to containerise name (str): Name of resulting assembly namespace (str): Namespace under which to host container context (dict): Asset information loader (str, optional): Name of node used to produce this container. Returns: - timeline_item (hiero.core.TrackItem): containerised object + timeline_item (resolve.TimelineItem): containerized object """ @@ -173,10 +163,10 @@ def ls(): def parse_container(timeline_item, validate=True): - """Return container data from timeline_item's openpype tag. + """Return container data from timeline_item's marker data. Args: - timeline_item (hiero.core.TrackItem): A containerised track item. + timeline_item (resolve.TimelineItem): A containerized track item. validate (bool)[optional]: validating with avalon scheme Returns: @@ -210,11 +200,11 @@ def parse_container(timeline_item, validate=True): def update_container(timeline_item, data=None): - """Update container data to input timeline_item's openpype tag. + """Update container data to input timeline_item's ayon marker data. Args: - timeline_item (hiero.core.TrackItem): A containerised track item. - data (dict)[optional]: dictionery with data to be updated + timeline_item (resolve.TimelineItem): A containerized track item. + data (dict)[optional]: dictionary with data to be updated Returns: bool: True if container was updated correctly diff --git a/client/ayon_core/hosts/resolve/api/todo-rendering.py b/client/ayon_core/hosts/resolve/api/todo-rendering.py index 5238d76dec..265f922069 100644 --- a/client/ayon_core/hosts/resolve/api/todo-rendering.py +++ b/client/ayon_core/hosts/resolve/api/todo-rendering.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# TODO: convert this script to be usable with OpenPype +# TODO: convert this script to be usable with Ayon """ Example DaVinci Resolve script: Load a still from DRX file, apply the still to all clips in all timelines. diff --git a/client/ayon_core/hosts/resolve/hooks/pre_resolve_setup.py b/client/ayon_core/hosts/resolve/hooks/pre_resolve_setup.py index c14fd75b2f..43a0291df6 100644 --- a/client/ayon_core/hosts/resolve/hooks/pre_resolve_setup.py +++ b/client/ayon_core/hosts/resolve/hooks/pre_resolve_setup.py @@ -18,12 +18,12 @@ class PreLaunchResolveSetup(PreLaunchHook): It adds $RESOLVE_SCRIPT_API/Modules to PYTHONPATH. Additionally it sets up the Python home for Python 3 based on the - RESOLVE_PYTHON3_HOME in the environment (usually defined in OpenPype's + RESOLVE_PYTHON3_HOME in the environment (usually defined in Ayon's Application environment for Resolve by the admin). For this it sets PYTHONHOME and PATH variables. It also defines: - - `RESOLVE_UTILITY_SCRIPTS_DIR`: Destination directory for OpenPype + - `RESOLVE_UTILITY_SCRIPTS_DIR`: Destination directory for Ayon Fusion scripts to be copied to for Resolve to pick them up. - `AYON_LOG_NO_COLORS` to True to ensure OP doesn't try to use logging with terminal colors as it fails in Resolve. diff --git a/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py b/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py index 7dd9872120..6a280fc141 100644 --- a/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py +++ b/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py @@ -30,8 +30,6 @@ def header_label(text): {_clip_}: name of used clip {_track_}: name of parent track layer {_sequence_}: name of parent sequence (timeline)""" - # gui_name = "OpenPype publish attributes creator" - # gui_info = "Define sequential rename and fill hierarchy data." gui_tracks = get_video_track_names() # Project settings might be applied to this creator via diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_clip.py b/client/ayon_core/hosts/resolve/plugins/load/load_clip.py index d00af41c50..d87b5d4588 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_clip.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_clip.py @@ -153,7 +153,7 @@ def remove(self, container): 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 + # so all we can do is just remove the ayon marker to 'untag' it if lib.get_ayon_marker(timeline_item): # Note: We must call `get_ayon_marker` because # `delete_ayon_marker` uses a global variable set by diff --git a/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py b/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py index a01d3f1c32..039fd27af8 100644 --- a/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py +++ b/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py @@ -33,7 +33,7 @@ def process(self, context): data = {} timeline_item = timeline_item_data["clip"]["item"] - # get pype tag data + # get ayon tag data tag_data = get_timeline_item_ayon_tag(timeline_item) self.log.debug(f"__ tag_data: {pformat(tag_data)}") diff --git a/client/ayon_core/hosts/resolve/utility_scripts/ayon_startup.scriptlib b/client/ayon_core/hosts/resolve/utility_scripts/ayon_startup.scriptlib index 22253390a3..ecc75946b5 100644 --- a/client/ayon_core/hosts/resolve/utility_scripts/ayon_startup.scriptlib +++ b/client/ayon_core/hosts/resolve/utility_scripts/ayon_startup.scriptlib @@ -1,4 +1,4 @@ --- Run OpenPype's Python launch script for resolve +-- Run Ayon's Python launch script for resolve function file_exists(name) local f = io.open(name, "r") return f ~= nil and io.close(f) @@ -12,7 +12,7 @@ if ayon_startup_script ~= nil then if file_exists(script) then -- We must use RunScript to ensure it runs in a separate -- process to Resolve itself to avoid a deadlock for - -- certain imports of OpenPype libraries or Qt + -- certain imports of Ayon libraries or Qt print("Running launch script: " .. script) fusion:RunScript(script) else From cabf6bed4ba27fc597981d219c9e0041e01a3968 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Mar 2024 17:17:57 +0100 Subject: [PATCH 10/21] Remove deprecated functions and use constants for clip color in Resolve plugins. Adjust imports accordingly. --- client/ayon_core/hosts/resolve/api/__init__.py | 2 -- client/ayon_core/hosts/resolve/api/plugin.py | 4 ++-- .../hosts/resolve/plugins/create/create_shot_clip.py | 6 +++--- .../hosts/resolve/plugins/publish/precollect_instances.py | 4 +++- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/hosts/resolve/api/__init__.py b/client/ayon_core/hosts/resolve/api/__init__.py index 711d0c4a71..50df9aea2d 100644 --- a/client/ayon_core/hosts/resolve/api/__init__.py +++ b/client/ayon_core/hosts/resolve/api/__init__.py @@ -15,7 +15,6 @@ from .lib import ( maintain_current_timeline, - publish_clip_color, get_project_manager, get_current_resolve_project, get_current_project, # backward compatibility @@ -93,7 +92,6 @@ # lib "maintain_current_timeline", - "publish_clip_color", "get_project_manager", "get_current_resolve_project", "get_current_project", # backward compatibility diff --git a/client/ayon_core/hosts/resolve/api/plugin.py b/client/ayon_core/hosts/resolve/api/plugin.py index d6edc5f1a4..64f489f525 100644 --- a/client/ayon_core/hosts/resolve/api/plugin.py +++ b/client/ayon_core/hosts/resolve/api/plugin.py @@ -12,7 +12,7 @@ Creator as NewCreator ) -from . import lib +from . import lib, constants class ClipLoader: @@ -436,7 +436,7 @@ def convert(self): else: self.tag_data["asset"] = self.ti_name - if not lib.ayon_marker_workflow: + if not constants.ayon_marker_workflow: # create compound clip workflow lib.create_compound_clip( self.timeline_item_data, diff --git a/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py b/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py index 6a280fc141..b2e2efa81e 100644 --- a/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py +++ b/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py @@ -1,6 +1,6 @@ import copy -from ayon_core.hosts.resolve.api import plugin, lib +from ayon_core.hosts.resolve.api import plugin, lib, constants from ayon_core.hosts.resolve.api.lib import ( get_video_track_names, create_bin, @@ -262,7 +262,7 @@ def create(self, subset_name, instance_data, pre_create_data): # from `PublishableClip.convert` continue - track_item.SetClipColor(lib.publish_clip_color) + track_item.SetClipColor(constants.publish_clip_color) instance_data = copy.deepcopy(instance_data) # TODO: here we need to replicate Traypublisher Editorial workflow @@ -287,7 +287,7 @@ def create(self, subset_name, instance_data, pre_create_data): def collect_instances(self): """Collect all created instances from current timeline.""" selected_timeline_items = lib.get_current_timeline_items( - filter=True, selecting_color=lib.publish_clip_color) + filter=True, selecting_color=constants.publish_clip_color) instances = [] for timeline_item_data in selected_timeline_items: diff --git a/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py b/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py index 039fd27af8..4bfeab767f 100644 --- a/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py +++ b/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py @@ -3,10 +3,12 @@ import pyblish from ayon_core.pipeline import AYON_INSTANCE_ID, AVALON_INSTANCE_ID +from ayon_core.hosts.resolve.api.constants import ( + publish_clip_color +) from ayon_core.hosts.resolve.api.lib import ( get_current_timeline_items, get_timeline_item_ayon_tag, - publish_clip_color, get_publish_attribute, get_otio_clip_instance_data, ) From 551743842d05a82270d9847ffc037c3b1ec89216 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Mar 2024 09:53:57 +0100 Subject: [PATCH 11/21] fixing bits after renaming and moving into constants --- .../ayon_core/hosts/resolve/plugins/create/create_shot_clip.py | 2 +- .../hosts/resolve/plugins/publish/collect_current_project.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py b/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py index b2e2efa81e..c9b553273d 100644 --- a/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py +++ b/client/ayon_core/hosts/resolve/plugins/create/create_shot_clip.py @@ -330,6 +330,6 @@ def remove_instances(self, instances): # removing instance by marker color print(f"Removing instance: {track_item.GetName()}") - track_item.DeleteMarkersByColor(lib.pype_marker_color) + track_item.DeleteMarkersByColor(constants.ayon_marker_color) self._remove_instance_from_context(instance) diff --git a/client/ayon_core/hosts/resolve/plugins/publish/collect_current_project.py b/client/ayon_core/hosts/resolve/plugins/publish/collect_current_project.py index 27605593ce..dbac3d0635 100644 --- a/client/ayon_core/hosts/resolve/plugins/publish/collect_current_project.py +++ b/client/ayon_core/hosts/resolve/plugins/publish/collect_current_project.py @@ -11,7 +11,7 @@ class CollectResolveProject(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.499 def process(self, context): - resolve_project = rapi.get_current_project() + resolve_project = rapi.get_current_resolve_project() timeline = resolve_project.GetCurrentTimeline() fps = timeline.GetSetting("timelineFrameRate") From 0c84f6f8d9e752a8b942a835354f9a2cbff126fd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Mar 2024 09:54:25 +0100 Subject: [PATCH 12/21] adding function for getting native otio file --- client/ayon_core/hosts/resolve/api/lib.py | 43 +++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/client/ayon_core/hosts/resolve/api/lib.py b/client/ayon_core/hosts/resolve/api/lib.py index fe7c110ed7..fdcb1b8640 100644 --- a/client/ayon_core/hosts/resolve/api/lib.py +++ b/client/ayon_core/hosts/resolve/api/lib.py @@ -2,6 +2,7 @@ import re import os import contextlib +import tempfile from opentimelineio import opentime from ayon_core.lib import Logger @@ -9,6 +10,8 @@ is_overlapping_otio_ranges, frames_to_timecode ) +from ayon_core.pipeline.tempdir import create_custom_tempdir + from . import constants from ..otio import davinci_export as otio_export @@ -932,6 +935,46 @@ def get_otio_clip_instance_data(otio_timeline, timeline_item_data): return None +def get_timeline_otio_filepath(project_name, anatomy=None, timeline=None): + """Get timeline otio filepath. + + Args: + project_name (str): ayon project name + anatomy (ayon_core.pipeline.Anatomy)[optional]: Anatomy object + timeline (resolve.Timeline)[optional]: resolve's object + + Returns: + str: temporary otio filepath + """ + from . import bmdvr + resolve_project = get_current_resolve_project() + timeline = resolve_project.GetCurrentTimeline() + timeline_name = timeline.GetName() + + # get custom staging dir + custom_temp_dir = create_custom_tempdir(project_name, anatomy) + staging_dir = os.path.normpath( + tempfile.mkdtemp( + prefix="resolve_otio_tmp_", + dir=custom_temp_dir + ) + ) + filename = os.path.join(staging_dir, f"{timeline_name}.otio") + + # Native otio export is available from Resolve 18.5 + # [major, minor, patch, build, suffix] + resolve_version = bmdvr.GetVersion() + if resolve_version[0] < 18 or resolve_version[1] < 5: + # if it is lower then use ayon's otio exporter + otio_timeline = otio_export.create_otio_timeline( + resolve_project, timeline=timeline) + otio_export.write_to_file(otio_timeline, filename) + + timeline.Export(filename, bmdvr.EXPORT_OTIO) + + return filename + + def get_reformated_path(path, padded=False, first=False): """ Return fixed python expression path From 2a64d723310110f53aaecbc991d1bf72fc447313 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Mar 2024 09:55:13 +0100 Subject: [PATCH 13/21] adding HostContext class for dealing with context in creators --- .../ayon_core/hosts/resolve/api/pipeline.py | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/client/ayon_core/hosts/resolve/api/pipeline.py b/client/ayon_core/hosts/resolve/api/pipeline.py index 9665ca7d64..e320dcfe40 100644 --- a/client/ayon_core/hosts/resolve/api/pipeline.py +++ b/client/ayon_core/hosts/resolve/api/pipeline.py @@ -3,6 +3,9 @@ """ import os import contextlib +import atexit +import tempfile +import json from collections import OrderedDict from pyblish import api as pyblish @@ -260,3 +263,138 @@ def on_pyblish_instance_toggled(instance, old_value, new_value): # Whether instances should be passthrough based on new value timeline_item = instance.data["item"] set_publish_attribute(timeline_item, new_value) + + +class HostContext: + _context_json_path = None + + @staticmethod + def _on_exit(): + if ( + HostContext._context_json_path + and os.path.exists(HostContext._context_json_path) + ): + os.remove(HostContext._context_json_path) + + @classmethod + def get_context_json_path(cls): + if cls._context_json_path is None: + output_file = tempfile.NamedTemporaryFile( + mode="w", prefix="resolve_", suffix=".json" + ) + output_file.close() + cls._context_json_path = output_file.name + atexit.register(HostContext._on_exit) + print(cls._context_json_path) + return cls._context_json_path + + @classmethod + def _get_data(cls, group=None): + json_path = cls.get_context_json_path() + data = {} + if not os.path.exists(json_path): + with open(json_path, "w") as json_stream: + json.dump(data, json_stream) + else: + with open(json_path, "r") as json_stream: + content = json_stream.read() + if content: + data = json.loads(content) + if group is None: + return data + return data.get(group) + + @classmethod + def _save_data(cls, group, new_data): + json_path = cls.get_context_json_path() + data = cls._get_data() + data[group] = new_data + with open(json_path, "w") as json_stream: + json.dump(data, json_stream) + + @classmethod + def add_instance(cls, instance): + instances = cls.get_instances() + instances.append(instance) + cls.save_instances(instances) + + @classmethod + def get_instances(cls): + return cls._get_data("instances") or [] + + @classmethod + def save_instances(cls, instances): + cls._save_data("instances", instances) + + @classmethod + def get_context_data(cls): + return cls._get_data("context") or {} + + @classmethod + def save_context_data(cls, data): + cls._save_data("context", data) + + @classmethod + def get_project_name(cls): + return cls._get_data("project_name") + + @classmethod + def set_project_name(cls, project_name): + cls._save_data("project_name", project_name) + + @classmethod + def get_data_to_store(cls): + return { + "project_name": cls.get_project_name(), + "instances": cls.get_instances(), + "context": cls.get_context_data(), + } + + +def list_instances(): + return HostContext.get_instances() + + +def update_instances(update_list): + updated_instances = {} + for instance, _changes in update_list: + updated_instances[instance.id] = instance.data_to_store() + + instances = HostContext.get_instances() + for instance_data in instances: + instance_id = instance_data["instance_id"] + if instance_id in updated_instances: + new_instance_data = updated_instances[instance_id] + old_keys = set(instance_data.keys()) + new_keys = set(new_instance_data.keys()) + instance_data.update(new_instance_data) + for key in (old_keys - new_keys): + instance_data.pop(key) + + HostContext.save_instances(instances) + + +def remove_instances(instances): + if not isinstance(instances, (tuple, list)): + instances = [instances] + + current_instances = HostContext.get_instances() + for instance in instances: + instance_id = instance.data["instance_id"] + found_idx = None + for idx, _instance in enumerate(current_instances): + if instance_id == _instance["instance_id"]: + found_idx = idx + break + + if found_idx is not None: + current_instances.pop(found_idx) + HostContext.save_instances(current_instances) + + +def get_context_data(): + return HostContext.get_context_data() + + +def update_context_data(data, changes): + HostContext.save_context_data(data) \ No newline at end of file From 0d90f57a785a06b437ad0f22aec104cb44a1dc2b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Mar 2024 09:56:10 +0100 Subject: [PATCH 14/21] adding abstracted plugins for dealing with new publisher --- client/ayon_core/hosts/resolve/api/plugin.py | 92 ++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/client/ayon_core/hosts/resolve/api/plugin.py b/client/ayon_core/hosts/resolve/api/plugin.py index 64f489f525..8655243642 100644 --- a/client/ayon_core/hosts/resolve/api/plugin.py +++ b/client/ayon_core/hosts/resolve/api/plugin.py @@ -12,9 +12,26 @@ Creator as NewCreator ) +from ayon_core.pipeline.create import ( + Creator, + HiddenCreator, + CreatedInstance, + cache_and_get_instances, +) + +from .pipeline import ( + list_instances, + update_instances, + remove_instances, + HostContext, +) + from . import lib, constants +SHARED_DATA_KEY = "ayon.resolve.instances" + + class ClipLoader: active_bin = None @@ -653,3 +670,78 @@ def _create_parents(self): # alias for backward compatibility PublishClip = PublishableClip # noqa + + +class HiddenResolvePublishCreator(HiddenCreator): + host_name = "resolve" + settings_category = "resolve" + + def collect_instances(self): + instances_by_identifier = cache_and_get_instances( + self, SHARED_DATA_KEY, list_instances + ) + for instance_data in instances_by_identifier[self.identifier]: + instance = CreatedInstance.from_existing(instance_data, self) + self._add_instance_to_context(instance) + + def update_instances(self, update_list): + update_instances(update_list) + + def remove_instances(self, instances): + remove_instances(instances) + for instance in instances: + self._remove_instance_from_context(instance) + + def _store_new_instance(self, new_instance): + """Resolve publisher specific method to store instance. + + Instance is stored into "workfile" of Resolve and also add it + to CreateContext. + + Args: + new_instance (CreatedInstance): Instance that should be stored. + """ + + # Host implementation of storing metadata about instance + HostContext.add_instance(new_instance.data_to_store()) + # Add instance to current context + self._add_instance_to_context(new_instance) + + +class ResolvePublishCreator(Creator): + create_allow_context_change = True + host_name = "resolve" + settings_category = "resolve" + + def collect_instances(self): + instances_by_identifier = cache_and_get_instances( + self, SHARED_DATA_KEY, list_instances + ) + for instance_data in instances_by_identifier[self.identifier]: + instance = CreatedInstance.from_existing(instance_data, self) + self._add_instance_to_context(instance) + + def update_instances(self, update_list): + update_instances(update_list) + + def remove_instances(self, instances): + remove_instances(instances) + for instance in instances: + self._remove_instance_from_context(instance) + + def _store_new_instance(self, new_instance): + """Resolve publisher specific method to store instance. + + Instance is stored into "workfile" of Resolve and also add it + to CreateContext. + + Args: + new_instance (CreatedInstance): Instance that should be stored. + """ + + # Host implementation of storing metadata about instance + HostContext.add_instance(new_instance.data_to_store()) + new_instance.mark_as_stored() + + # Add instance to current context + self._add_instance_to_context(new_instance) From d03c2bf0fbc42bcbd86b13f9a802188ec6fd5ecf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Mar 2024 09:57:06 +0100 Subject: [PATCH 15/21] create otio timeline can optionally add own timeline --- client/ayon_core/hosts/resolve/otio/davinci_export.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/resolve/otio/davinci_export.py b/client/ayon_core/hosts/resolve/otio/davinci_export.py index ab1a03dddc..416f63654d 100644 --- a/client/ayon_core/hosts/resolve/otio/davinci_export.py +++ b/client/ayon_core/hosts/resolve/otio/davinci_export.py @@ -256,10 +256,10 @@ def add_otio_metadata(otio_item, media_pool_item, **kwargs): otio_item.metadata.update({key: value}) -def create_otio_timeline(resolve_project): +def create_otio_timeline(resolve_project, timeline=None): # get current timeline - timeline = resolve_project.GetCurrentTimeline() + timeline = timeline or resolve_project.GetCurrentTimeline() self.project_fps = timeline.GetSetting("timelineFrameRate") # convert timeline to otio From 6e1851ff18fb6597be9c9e4ea2a38e29ee3de74d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 12 Jun 2024 15:12:13 +0200 Subject: [PATCH 16/21] Refactor menu stylesheet loading, update plugin creation logic - Removed the function for loading stylesheets in the menu. - Refactored plugin creation logic to use new API methods and data structures. --- .../resolve/client/ayon_resolve/api/menu.py | 11 --- .../resolve/client/ayon_resolve/api/plugin.py | 2 - .../plugins/create/create_workfile.py | 90 +++++++++---------- 3 files changed, 44 insertions(+), 59 deletions(-) diff --git a/server_addon/resolve/client/ayon_resolve/api/menu.py b/server_addon/resolve/client/ayon_resolve/api/menu.py index fbe91811db..6778119091 100644 --- a/server_addon/resolve/client/ayon_resolve/api/menu.py +++ b/server_addon/resolve/client/ayon_resolve/api/menu.py @@ -11,17 +11,6 @@ MENU_LABEL = os.environ["AYON_MENU_LABEL"] -def load_stylesheet(): - path = os.path.join(os.path.dirname(__file__), "menu_style.qss") - if not os.path.exists(path): - print("Unable to load stylesheet, file not found in resources") - return "" - - with open(path, "r") as file_stream: - stylesheet = file_stream.read() - return stylesheet - - class Spacer(QtWidgets.QWidget): def __init__(self, height, *args, **kwargs): super(Spacer, self).__init__(*args, **kwargs) diff --git a/server_addon/resolve/client/ayon_resolve/api/plugin.py b/server_addon/resolve/client/ayon_resolve/api/plugin.py index ba6ac9719e..55c9c45ff0 100644 --- a/server_addon/resolve/client/ayon_resolve/api/plugin.py +++ b/server_addon/resolve/client/ayon_resolve/api/plugin.py @@ -3,8 +3,6 @@ import qargparse -from ayon_core.pipeline.context_tools import get_current_project_asset - from ayon_core.lib import BoolDef from ayon_core.pipeline import ( diff --git a/server_addon/resolve/client/ayon_resolve/plugins/create/create_workfile.py b/server_addon/resolve/client/ayon_resolve/plugins/create/create_workfile.py index 227a2ca500..2a8183da7b 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/create/create_workfile.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/create/create_workfile.py @@ -1,67 +1,65 @@ # -*- coding: utf-8 -*- """Creator plugin for creating workfiles.""" -from ayon_core.pipeline import CreatedInstance, AutoCreator -from ayon_core.client import get_asset_by_name +import ayon_api +from ayon_core.pipeline import ( + AutoCreator, + CreatedInstance, +) class CreateWorkfile(AutoCreator): """Workfile auto-creator.""" + settings_category = "resolve" + identifier = "io.ayon.creators.resolve.workfile" label = "Workfile" - family = "workfile" - icon = "fa5.file" + product_type = "workfile" default_variant = "Main" - def create(self): + def collect_instances(self): variant = self.default_variant - current_instance = next( - ( - instance for instance in self.create_context.instances - if instance.creator_identifier == self.identifier - ), None) - - project_name = self.project_name - asset_name = self.create_context.get_current_asset_name() + project_name = self.create_context.get_current_project_name() + folder_path = self.create_context.get_current_folder_path() task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name - if current_instance is None: - asset_doc = get_asset_by_name(project_name, asset_name) - subset_name = self.get_subset_name( - variant, task_name, asset_doc, project_name, host_name - ) - data = { - "asset": asset_name, - "task": task_name, - "variant": variant, - } - data.update( - self.get_dynamic_data( - variant, task_name, asset_doc, - project_name, host_name, current_instance) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) + product_name = self.get_product_name( + project_name, + folder_entity, + task_entity, + self.default_variant, + host_name, + ) + data = { + "folderPath": folder_path, + "task": task_name, + "variant": variant, + } + data.update( + self.get_dynamic_data( + variant, + task_name, + folder_entity, + project_name, + host_name, + False, ) - self.log.info("Auto-creating workfile instance...") - current_instance = CreatedInstance( - self.family, subset_name, data, self - ) - self._add_instance_to_context(current_instance) - elif ( - current_instance["asset"] != asset_name - or current_instance["task"] != task_name - ): - # Update instance context if is not the same - asset_doc = get_asset_by_name(project_name, asset_name) - subset_name = self.get_subset_name( - variant, task_name, asset_doc, project_name, host_name - ) - current_instance["asset"] = asset_name - current_instance["task"] = task_name - current_instance["subset"] = subset_name + ) + self.log.info("Auto-creating workfile instance...") + current_instance = CreatedInstance( + self.product_type, product_name, data, self) + self._add_instance_to_context(current_instance) - def collect_instances(self): - # TODO: Implement + def create(self, options=None): + # no need to create if it is created + # in `collect_instances` pass def update_instances(self, update_list): From cdc55edcb1f032030bfeae4bb9b54f73655b75cd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 13 Jun 2024 12:15:50 +0200 Subject: [PATCH 17/21] Update API documentation and keyframe mode information. Remove outdated content related to previous versions. --- .../ayon_resolve/RESOLVE_API_v19.0B-build20.txt | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/server_addon/resolve/client/ayon_resolve/RESOLVE_API_v19.0B-build20.txt b/server_addon/resolve/client/ayon_resolve/RESOLVE_API_v19.0B-build20.txt index 7deb0743f6..d9207c9137 100644 --- a/server_addon/resolve/client/ayon_resolve/RESOLVE_API_v19.0B-build20.txt +++ b/server_addon/resolve/client/ayon_resolve/RESOLVE_API_v19.0B-build20.txt @@ -1,8 +1,5 @@ -<<<<<<<< HEAD:server_addon/resolve/client/ayon_resolve/RESOLVE_API_v18.6.5-build7.txt -Updated as of 18 December 2023 -======== + Last Updated: 1 April 2024 ->>>>>>>> develop:server_addon/resolve/client/ayon_resolve/RESOLVE_API_v19.0B-build20.txt ---------------------------- In this package, you will find a brief introduction to the Scripting API for DaVinci Resolve Studio. Apart from this README.txt file, this package contains folders containing the basic import modules for scripting access (DaVinciResolve.py) and some representative examples. @@ -106,11 +103,8 @@ Resolve ExportRenderPreset(presetName, exportPath) --> Bool # Export a preset to a given path (string) if presetName(string) exists. ImportBurnInPreset(presetPath) --> Bool # Import a data burn in preset from a given presetPath (string) ExportBurnInPreset(presetName, exportPath) --> Bool # Export a data burn in preset to a given path (string) if presetName (string) exists. -<<<<<<<< HEAD:server_addon/resolve/client/ayon_resolve/RESOLVE_API_v18.6.5-build7.txt -======== GetKeyframeMode() --> keyframeMode # Returns the currently set keyframe mode (int). Refer to section 'Keyframe Mode information' below for details. SetKeyframeMode(keyframeMode) --> Bool # Returns True when 'keyframeMode'(enum) is successfully set. Refer to section 'Keyframe Mode information' below for details. ->>>>>>>> develop:server_addon/resolve/client/ayon_resolve/RESOLVE_API_v19.0B-build20.txt ProjectManager ArchiveProject(projectName, @@ -370,10 +364,7 @@ Timeline # Returns True on success, False otherwise. DetectSceneCuts() --> Bool # Detects and makes scene cuts along the timeline. Returns True if successful, False otherwise. ConvertTimelineToStereo() --> Bool # Converts timeline to stereo. Returns True if successful; False otherwise. -<<<<<<<< HEAD:server_addon/resolve/client/ayon_resolve/RESOLVE_API_v18.6.5-build7.txt -======== GetNodeGraph() --> Graph # Returns the timeline's node graph object. ->>>>>>>> develop:server_addon/resolve/client/ayon_resolve/RESOLVE_API_v19.0B-build20.txt TimelineItem GetName() --> string # Returns the item name. @@ -489,8 +480,7 @@ Beside primitive data types, Resolve's Python API mainly uses list and dict data As Lua does not support list and dict data structures, the Lua API implements "list" as a table with indices, e.g. { [1] = listValue1, [2] = listValue2, ... }. Similarly the Lua API implements "dict" as a table with the dictionary key as first element, e.g. { [dictKey1] = dictValue1, [dictKey2] = dictValue2, ... }. -<<<<<<<< HEAD:server_addon/resolve/client/ayon_resolve/RESOLVE_API_v18.6.5-build7.txt -======== + Keyframe Mode information ------------------------- This section covers additional notes for the functions Resolve.GetKeyframeMode() and Resolve.SetKeyframeMode(keyframeMode). @@ -502,7 +492,6 @@ This section covers additional notes for the functions Resolve.GetKeyframeMode() Integer values returned by Resolve.GetKeyframeMode() will correspond to the enums above. ->>>>>>>> develop:server_addon/resolve/client/ayon_resolve/RESOLVE_API_v19.0B-build20.txt Cloud Projects Settings -------------------------------------- This section covers additional notes for the functions "ProjectManager:CreateCloudProject," "ProjectManager:ImportCloudProject," and "ProjectManager:RestoreCloudProject" From e1dea1cd4eeebc435d271e098f5c261b22789b4a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 13 Jun 2024 12:25:42 +0200 Subject: [PATCH 18/21] Refactor otio file handling functions, add temp dir logic. - Refactored function names and descriptions for clarity - Added new function to export timeline otio files - Improved temporary directory handling for otio files --- .../resolve/client/ayon_resolve/api/lib.py | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/server_addon/resolve/client/ayon_resolve/api/lib.py b/server_addon/resolve/client/ayon_resolve/api/lib.py index 0b279a2f46..ef5e7ee248 100644 --- a/server_addon/resolve/client/ayon_resolve/api/lib.py +++ b/server_addon/resolve/client/ayon_resolve/api/lib.py @@ -946,8 +946,8 @@ def get_otio_clip_instance_data(otio_timeline, timeline_item_data): return None -def get_timeline_otio_filepath(project_name, anatomy=None, timeline=None): - """Get timeline otio filepath. +def get_otio_temp_dir(project_name, anatomy=None, timeline=None) -> str: + """Get otio temporary directory. Args: project_name (str): ayon project name @@ -957,33 +957,48 @@ def get_timeline_otio_filepath(project_name, anatomy=None, timeline=None): Returns: str: temporary otio filepath """ - from . import bmdvr resolve_project = get_current_resolve_project() - timeline = resolve_project.GetCurrentTimeline() + timeline = timeline or resolve_project.GetCurrentTimeline() timeline_name = timeline.GetName() # get custom staging dir custom_temp_dir = create_custom_tempdir(project_name, anatomy) staging_dir = os.path.normpath( - tempfile.mkdtemp( - prefix="resolve_otio_tmp_", - dir=custom_temp_dir - ) + tempfile.mkdtemp(prefix="resolve_otio_tmp_", dir=custom_temp_dir) + ) + return os.path.join( + staging_dir, f"{timeline_name}.otio" ) - filename = os.path.join(staging_dir, f"{timeline_name}.otio") - # Native otio export is available from Resolve 18.5 - # [major, minor, patch, build, suffix] - resolve_version = bmdvr.GetVersion() - if resolve_version[0] < 18 or resolve_version[1] < 5: - # if it is lower then use ayon's otio exporter - otio_timeline = otio_export.create_otio_timeline( - resolve_project, timeline=timeline) - otio_export.write_to_file(otio_timeline, filename) - timeline.Export(filename, bmdvr.EXPORT_OTIO) +def export_timeline_otio(timeline, filepath): + """Get timeline otio filepath. + + Only supported from Resolve 19.5 + + Example: + # Native otio export is available from Resolve 18.5 + # [major, minor, patch, build, suffix] + resolve_version = bmdvr.GetVersion() + if resolve_version[0] < 18 or resolve_version[1] < 5: + # if it is lower then use ayon's otio exporter + otio_timeline = otio_export.create_otio_timeline( + resolve_project, timeline=timeline) + otio_export.write_to_file(otio_timeline, filepath) + else: + # use native otio export + export_timeline_otio(timeline, filepath) + + Args: + timeline (resolve.Timeline): resolve's object + filepath (str): otio file path + + Returns: + str: temporary otio filepath + """ + from . import bmdvr - return filename + timeline.Export(filepath, bmdvr.EXPORT_OTIO) def get_reformated_path(path, padded=False, first=False): From e4a11ef85d8dae1e47f9ddab5e4dcc94fea61c52 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 13 Jun 2024 12:29:58 +0200 Subject: [PATCH 19/21] Refactor get_otio_temp_dir to handle missing timeline. Adjusted get_otio_temp_dir to set timeline if not provided, throwing error if none found. --- server_addon/resolve/client/ayon_resolve/api/lib.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server_addon/resolve/client/ayon_resolve/api/lib.py b/server_addon/resolve/client/ayon_resolve/api/lib.py index ef5e7ee248..72c567d9e3 100644 --- a/server_addon/resolve/client/ayon_resolve/api/lib.py +++ b/server_addon/resolve/client/ayon_resolve/api/lib.py @@ -958,7 +958,12 @@ def get_otio_temp_dir(project_name, anatomy=None, timeline=None) -> str: str: temporary otio filepath """ resolve_project = get_current_resolve_project() - timeline = timeline or resolve_project.GetCurrentTimeline() + + if timeline is None: + timeline = resolve_project.GetCurrentTimeline() + if not timeline: + raise RuntimeError("No current timeline") + timeline_name = timeline.GetName() # get custom staging dir From 3d1f0d7520ff231d4369dc385b77d1b3b8ddd4d3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 13 Jun 2024 12:35:11 +0200 Subject: [PATCH 20/21] reverting latest commit changes partly --- server_addon/resolve/client/ayon_resolve/api/lib.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server_addon/resolve/client/ayon_resolve/api/lib.py b/server_addon/resolve/client/ayon_resolve/api/lib.py index 626c42dffc..dea09c9291 100644 --- a/server_addon/resolve/client/ayon_resolve/api/lib.py +++ b/server_addon/resolve/client/ayon_resolve/api/lib.py @@ -289,11 +289,13 @@ def create_timeline_item( object: resolve.TimelineItem """ # get all variables - project = get_current_project() - media_pool = project.GetMediaPool() + resolve_project = get_current_resolve_project() + media_pool = resolve_project.GetMediaPool() clip_name = media_pool_item.GetClipProperty("File Name") timeline = timeline or get_current_timeline() + timeline = timeline or get_current_timeline() + # timing variables if all([timeline_in, source_start, source_end]): fps = timeline.GetSetting("timelineFrameRate") From d105d133a01ea0bd205717b3c098a2587d9e354b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 14 Jun 2024 12:33:00 +0200 Subject: [PATCH 21/21] Update server_addon/resolve/client/ayon_resolve/api/lib.py Co-authored-by: Roy Nieterau --- server_addon/resolve/client/ayon_resolve/api/lib.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/server_addon/resolve/client/ayon_resolve/api/lib.py b/server_addon/resolve/client/ayon_resolve/api/lib.py index dea09c9291..d9ff07b6a8 100644 --- a/server_addon/resolve/client/ayon_resolve/api/lib.py +++ b/server_addon/resolve/client/ayon_resolve/api/lib.py @@ -294,8 +294,6 @@ def create_timeline_item( clip_name = media_pool_item.GetClipProperty("File Name") timeline = timeline or get_current_timeline() - timeline = timeline or get_current_timeline() - # timing variables if all([timeline_in, source_start, source_end]): fps = timeline.GetSetting("timelineFrameRate")