diff --git a/server_addon/resolve/client/ayon_resolve/README.markdown b/server_addon/resolve/client/ayon_resolve/README.markdown index 064e791f65..b16a654538 100644 --- a/server_addon/resolve/client/ayon_resolve/README.markdown +++ b/server_addon/resolve/client/ayon_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 AYON 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/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 a2f3fa6f73..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,3 +1,4 @@ + Last Updated: 1 April 2024 ---------------------------- 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 @@ -479,6 +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, ... }. + Keyframe Mode information ------------------------- This section covers additional notes for the functions Resolve.GetKeyframeMode() and Resolve.SetKeyframeMode(keyframeMode). diff --git a/server_addon/resolve/client/ayon_resolve/api/__init__.py b/server_addon/resolve/client/ayon_resolve/api/__init__.py index 3359430ef5..50df9aea2d 100644 --- a/server_addon/resolve/client/ayon_resolve/api/__init__.py +++ b/server_addon/resolve/client/ayon_resolve/api/__init__.py @@ -11,15 +11,13 @@ 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, @@ -30,9 +28,12 @@ get_timeline_item, 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_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, + set_timeline_item_pype_tag, # backward compatibility imprint, set_publish_attribute, get_publish_attribute, @@ -49,8 +50,10 @@ from .plugin import ( ClipLoader, TimelineItemLoader, - Creator, - PublishClip + ResolveCreator, + Creator, # backward compatibility + PublishableClip, + PublishClip, # backward compatibility ) from .workio import ( @@ -64,13 +67,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,17 +86,15 @@ "containerise", "update_container", "maintained_selection", - "remove_instance", - "list_instances", # utils "get_resolve_module", # lib "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", @@ -99,9 +105,12 @@ "get_timeline_item", "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_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", + "set_timeline_item_pype_tag", # backward compatibility "imprint", "set_publish_attribute", "get_publish_attribute", @@ -118,8 +127,10 @@ # plugin "ClipLoader", "TimelineItemLoader", - "Creator", - "PublishClip", + "ResolveCreator", + "Creator", # backward compatibility + "PublishableClip", + "PublishClip", # backward compatibility # workio "open_file", diff --git a/server_addon/resolve/client/ayon_resolve/api/constants.py b/server_addon/resolve/client/ayon_resolve/api/constants.py new file mode 100644 index 0000000000..4b809e8786 --- /dev/null +++ b/server_addon/resolve/client/ayon_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/server_addon/resolve/client/ayon_resolve/api/lib.py b/server_addon/resolve/client/ayon_resolve/api/lib.py index 829c72b80a..d9ff07b6a8 100644 --- a/server_addon/resolve/client/ayon_resolve/api/lib.py +++ b/server_addon/resolve/client/ayon_resolve/api/lib.py @@ -1,8 +1,8 @@ -import sys import json import re import os import contextlib +import tempfile from opentimelineio import opentime from ayon_core.lib import Logger @@ -10,34 +10,13 @@ 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 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 +38,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_project(): - """Get current project object. +def get_current_resolve_project(): + """Get current resolve 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 +101,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 +119,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 +134,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 @@ -165,7 +162,7 @@ def create_bin(name: str, 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 @@ -193,43 +190,58 @@ def create_bin(name: str, 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 + + # 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 media_pool_items.pop() if media_pool_items else False + # return only first found + return media_pool_item.pop() def get_media_pool_item(filepath, root: object = None) -> object: @@ -243,7 +255,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) @@ -276,8 +289,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_name = media_pool_item.GetClipProperty("File Name") timeline = timeline or get_current_timeline() @@ -365,15 +378,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) @@ -390,7 +405,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 = ( @@ -408,7 +423,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 @@ -417,7 +432,7 @@ def get_current_timeline_items( _clips[track_index] = timeline_items _data = { - "project": project, + "project": resolve_project, "timeline": timeline, "track": { "name": _track_name, @@ -437,7 +452,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: @@ -448,7 +463,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 @@ -457,20 +472,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() @@ -480,15 +499,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 @@ -496,94 +518,97 @@ 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): """ - 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 + timeline_item (resolve.TimelineItem): resolve's object data (dict): Any data which needs 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) 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_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): - """ 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_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, @@ -594,22 +619,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): @@ -626,14 +655,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) @@ -669,7 +698,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) @@ -678,7 +707,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: @@ -688,10 +717,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) @@ -701,7 +730,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")) @@ -775,7 +804,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 @@ -786,7 +815,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): @@ -831,21 +860,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 @@ -853,11 +882,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!" @@ -878,13 +907,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) @@ -921,13 +950,73 @@ 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} return None +def get_otio_temp_dir(project_name, anatomy=None, timeline=None) -> str: + """Get otio temporary directory. + + 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 + """ + resolve_project = get_current_resolve_project() + + if timeline is None: + timeline = resolve_project.GetCurrentTimeline() + if not timeline: + raise RuntimeError("No current timeline") + + 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) + ) + return os.path.join( + staging_dir, f"{timeline_name}.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 + + timeline.Export(filepath, bmdvr.EXPORT_OTIO) + + def get_reformated_path(path, padded=False, first=False): """ Return fixed python expression path diff --git a/server_addon/resolve/client/ayon_resolve/api/menu.py b/server_addon/resolve/client/ayon_resolve/api/menu.py index fc2c15ad6d..6778119091 100644 --- a/server_addon/resolve/client/ayon_resolve/api/menu.py +++ b/server_addon/resolve/client/ayon_resolve/api/menu.py @@ -5,22 +5,12 @@ 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) @@ -44,6 +34,10 @@ def __init__(self, *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 @@ -57,15 +51,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 # ) @@ -78,32 +68,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) @@ -111,12 +89,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() @@ -136,11 +115,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") @@ -150,10 +129,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() @@ -167,8 +142,6 @@ 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_ayon_menu(): diff --git a/server_addon/resolve/client/ayon_resolve/api/menu_style.qss b/server_addon/resolve/client/ayon_resolve/api/menu_style.qss deleted file mode 100644 index ad8932d881..0000000000 --- a/server_addon/resolve/client/ayon_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; -} - -#AYONMenu { - 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/server_addon/resolve/client/ayon_resolve/api/pipeline.py b/server_addon/resolve/client/ayon_resolve/api/pipeline.py index 05d2c9bcd1..6ae7c3468f 100644 --- a/server_addon/resolve/client/ayon_resolve/api/pipeline.py +++ b/server_addon/resolve/client/ayon_resolve/api/pipeline.py @@ -4,6 +4,9 @@ import os import json import contextlib +import atexit +import tempfile +import json from collections import OrderedDict from pyblish import api as pyblish @@ -19,7 +22,8 @@ from ayon_core.host import ( HostBase, IWorkfileHost, - ILoadHost + ILoadHost, + IPublishHost ) from . import lib @@ -45,7 +49,7 @@ AVALON_CONTAINERS = ":AVALON_CONTAINERS" -class ResolveHost(HostBase, IWorkfileHost, ILoadHost): +class ResolveHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "resolve" def install(self): @@ -97,6 +101,12 @@ def get_workfile_extensions(self): def get_containers(self): return ls() + def get_context_data(self): + return {} + + def update_context_data(self, data, changes): + pass + def containerise(timeline_item, name, @@ -104,20 +114,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 """ @@ -133,7 +143,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 @@ -180,10 +190,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: @@ -191,7 +201,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) @@ -217,19 +227,19 @@ 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 """ - 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: @@ -238,7 +248,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 @@ -277,49 +287,136 @@ def on_pyblish_instance_toggled(instance, old_value, new_value): set_publish_attribute(timeline_item, new_value) -def remove_instance(instance): - """Remove instance marker from track item.""" - instance_id = instance.get("uuid") +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(), + } - 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"] +def list_instances(): + return HostContext.get_instances() - # 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 +def update_instances(update_list): + updated_instances = {} + for instance, _changes in update_list: + updated_instances[instance.id] = instance.data_to_store() - # removing instance by marker color - print(f"Removing instance: {found_ti.GetName()}") - found_ti.DeleteMarkersByColor(lib.pype_marker_color) + 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 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] +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) + - # get openpype tag data - tag_data = lib.get_timeline_item_pype_tag(timeline_item) +def get_context_data(): + return HostContext.get_context_data() - 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 +def update_context_data(data, changes): + HostContext.save_context_data(data) \ No newline at end of file diff --git a/server_addon/resolve/client/ayon_resolve/api/plugin.py b/server_addon/resolve/client/ayon_resolve/api/plugin.py index 0b339cdf7c..55c9c45ff0 100644 --- a/server_addon/resolve/client/ayon_resolve/api/plugin.py +++ b/server_addon/resolve/client/ayon_resolve/api/plugin.py @@ -1,290 +1,33 @@ 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.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. +from ayon_core.pipeline.create import ( + Creator, + HiddenCreator, + CreatedInstance, + cache_and_get_instances, +) - 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`. +from .pipeline import ( + list_instances, + update_instances, + remove_instances, + HostContext, +) - Args: - data (dict): widget rows or organized groups defined - by types `dict` or `section` - content_layout (QtWidgets.QFormLayout)[optional]: used when nesting +from . import lib, constants - 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) +SHARED_DATA_KEY = "ayon.resolve.instances" class ClipLoader: @@ -555,45 +298,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", @@ -609,7 +354,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 @@ -617,9 +362,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 @@ -636,14 +411,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() @@ -655,37 +427,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 constants.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 @@ -719,39 +491,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. """ @@ -762,37 +530,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 @@ -803,16 +572,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 @@ -831,19 +600,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) @@ -852,8 +619,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}) @@ -900,11 +667,80 @@ def _create_parents(self): parent = self._convert_to_entity(key) self.parents.append(parent) +# 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() -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 + # Add instance to current context + self._add_instance_to_context(new_instance) diff --git a/server_addon/resolve/client/ayon_resolve/api/todo-rendering.py b/server_addon/resolve/client/ayon_resolve/api/todo-rendering.py index 5238d76dec..265f922069 100644 --- a/server_addon/resolve/client/ayon_resolve/api/todo-rendering.py +++ b/server_addon/resolve/client/ayon_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/server_addon/resolve/client/ayon_resolve/api/workio.py b/server_addon/resolve/client/ayon_resolve/api/workio.py index b6c2f63432..fc61d9bb10 100644 --- a/server_addon/resolve/client/ayon_resolve/api/workio.py +++ b/server_addon/resolve/client/ayon_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 diff --git a/server_addon/resolve/client/ayon_resolve/hooks/pre_resolve_setup.py b/server_addon/resolve/client/ayon_resolve/hooks/pre_resolve_setup.py index ffd34d7b8d..ad1b96161d 100644 --- a/server_addon/resolve/client/ayon_resolve/hooks/pre_resolve_setup.py +++ b/server_addon/resolve/client/ayon_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/server_addon/resolve/client/ayon_resolve/otio/davinci_export.py b/server_addon/resolve/client/ayon_resolve/otio/davinci_export.py index 5f11c81fc5..416f63654d 100644 --- a/server_addon/resolve/client/ayon_resolve/otio/davinci_export.py +++ b/server_addon/resolve/client/ayon_resolve/otio/davinci_export.py @@ -256,11 +256,11 @@ 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 - self.project_fps = resolve_project.GetSetting("timelineFrameRate") - timeline = resolve_project.GetCurrentTimeline() + timeline = timeline or resolve_project.GetCurrentTimeline() + self.project_fps = timeline.GetSetting("timelineFrameRate") # convert timeline to otio otio_timeline = _create_otio_timeline( diff --git a/server_addon/resolve/client/ayon_resolve/plugins/create/create_shot_clip.py b/server_addon/resolve/client/ayon_resolve/plugins/create/create_shot_clip.py index da98c8de7d..75e3fd3d37 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/create/create_shot_clip.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/create/create_shot_clip.py @@ -1,245 +1,234 @@ # from pprint import pformat -from ayon_resolve.api import plugin, lib +from ayon_resolve.api import plugin, lib, constants from ayon_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 resolution 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_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 pairs 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 +237,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() - track_item.SetClipColor(lib.publish_clip_color) + 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(constants.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=constants.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_ayon_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(constants.ayon_marker_color) + + self._remove_instance_from_context(instance) 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 new file mode 100644 index 0000000000..2a8183da7b --- /dev/null +++ b/server_addon/resolve/client/ayon_resolve/plugins/create/create_workfile.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating workfiles.""" +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" + product_type = "workfile" + + default_variant = "Main" + + def collect_instances(self): + + variant = self.default_variant + 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 + + 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.product_type, product_name, data, self) + self._add_instance_to_context(current_instance) + + def create(self, options=None): + # no need to create if it is created + # in `collect_instances` + 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 diff --git a/server_addon/resolve/client/ayon_resolve/plugins/load/load_clip.py b/server_addon/resolve/client/ayon_resolve/plugins/load/load_clip.py index 7e3a5a254e..bd1847c95a 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/load/load_clip.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/load/load_clip.py @@ -154,13 +154,13 @@ 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 - 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 + # 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 + # `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/server_addon/resolve/client/ayon_resolve/plugins/publish/collect_current_project.py b/server_addon/resolve/client/ayon_resolve/plugins/publish/collect_current_project.py new file mode 100644 index 0000000000..dbac3d0635 --- /dev/null +++ b/server_addon/resolve/client/ayon_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_resolve_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/server_addon/resolve/client/ayon_resolve/plugins/publish/extract_workfile.py b/server_addon/resolve/client/ayon_resolve/plugins/publish/extract_workfile.py index 77d14ccdc5..1a9477b720 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/publish/extract_workfile.py +++ b/server_addon/resolve/client/ayon_resolve/plugins/publish/extract_workfile.py @@ -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/server_addon/resolve/client/ayon_resolve/plugins/publish/precollect_instances.py b/server_addon/resolve/client/ayon_resolve/plugins/publish/precollect_instances.py index e2b6e7ba37..f3307eb3ee 100644 --- a/server_addon/resolve/client/ayon_resolve/plugins/publish/precollect_instances.py +++ b/server_addon/resolve/client/ayon_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_resolve.api.constants import ( + publish_clip_color +) from ayon_resolve.api.lib import ( get_current_timeline_items, - get_timeline_item_pype_tag, - publish_clip_color, + get_timeline_item_ayon_tag, get_publish_attribute, get_otio_clip_instance_data, ) @@ -33,8 +35,8 @@ def process(self, context): data = {} timeline_item = timeline_item_data["clip"]["item"] - # get pype tag data - tag_data = get_timeline_item_pype_tag(timeline_item) + # get ayon tag data + 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/server_addon/resolve/client/ayon_resolve/plugins/publish/precollect_workfile.py b/server_addon/resolve/client/ayon_resolve/plugins/publish/precollect_workfile.py deleted file mode 100644 index a388d4bc59..0000000000 --- a/server_addon/resolve/client/ayon_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_folder_path - -from ayon_resolve import api as rapi -from ayon_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_folder_path = get_current_folder_path() - folder_name = current_folder_path.split("/")[-1] - - product_name = "workfileMain" - project = rapi.get_current_project() - fps = project.GetSetting("timelineFrameRate") - video_tracks = rapi.get_video_track_names() - - # adding otio timeline to context - otio_timeline = davinci_export.create_otio_timeline(project) - - instance_data = { - "name": "{}_{}".format(folder_name, product_name), - "label": "{} {}".format(current_folder_path, product_name), - "item": project, - "folderPath": current_folder_path, - "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": project, - "otioTimeline": otio_timeline, - "videoTracks": video_tracks, - "currentFile": 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))) diff --git a/server_addon/resolve/client/ayon_resolve/utility_scripts/ayon_startup.scriptlib b/server_addon/resolve/client/ayon_resolve/utility_scripts/ayon_startup.scriptlib index 22253390a3..ecc75946b5 100644 --- a/server_addon/resolve/client/ayon_resolve/utility_scripts/ayon_startup.scriptlib +++ b/server_addon/resolve/client/ayon_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 diff --git a/server_addon/resolve/client/ayon_resolve/utility_scripts/develop/OTIO_export.py b/server_addon/resolve/client/ayon_resolve/utility_scripts/develop/OTIO_export.py index 4572d1354d..27147e5500 100644 --- a/server_addon/resolve/client/ayon_resolve/utility_scripts/develop/OTIO_export.py +++ b/server_addon/resolve/client/ayon_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")