mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
464 lines
13 KiB
Python
464 lines
13 KiB
Python
import sys
|
|
import json
|
|
from opentimelineio import opentime
|
|
from pprint import pformat
|
|
|
|
from pype.api import Logger
|
|
|
|
log = Logger().get_logger(__name__, "resolve")
|
|
|
|
self = sys.modules[__name__]
|
|
self.pm = None
|
|
self.rename_index = 0
|
|
self.rename_add = 0
|
|
self.pype_metadata_key = "VFX Notes"
|
|
|
|
|
|
def get_project_manager():
|
|
from . import bmdvr
|
|
if not self.pm:
|
|
self.pm = bmdvr.GetProjectManager()
|
|
return self.pm
|
|
|
|
|
|
def get_current_project():
|
|
# initialize project manager
|
|
get_project_manager()
|
|
|
|
return self.pm.GetCurrentProject()
|
|
|
|
|
|
def get_current_sequence():
|
|
# get current project
|
|
project = get_current_project()
|
|
|
|
return project.GetCurrentTimeline()
|
|
|
|
|
|
def get_current_track_items(
|
|
filter=False,
|
|
track_type=None,
|
|
selecting_color=None):
|
|
""" Gets all available current timeline track items
|
|
"""
|
|
track_type = track_type or "video"
|
|
selecting_color = selecting_color or "Chocolate"
|
|
project = get_current_project()
|
|
sequence = get_current_sequence()
|
|
selected_clips = list()
|
|
|
|
# get all tracks count filtered by track type
|
|
selected_track_count = sequence.GetTrackCount(track_type)
|
|
|
|
# loop all tracks and get items
|
|
_clips = dict()
|
|
for track_index in range(1, (int(selected_track_count) + 1)):
|
|
track_name = sequence.GetTrackName(track_type, track_index)
|
|
track_track_items = sequence.GetItemListInTrack(
|
|
track_type, track_index)
|
|
_clips[track_index] = track_track_items
|
|
|
|
_data = {
|
|
"project": project,
|
|
"sequence": sequence,
|
|
"track": {
|
|
"name": track_name,
|
|
"index": track_index,
|
|
"type": track_type}
|
|
}
|
|
# get track item object and its color
|
|
for clip_index, ti in enumerate(_clips[track_index]):
|
|
data = _data.copy()
|
|
data["clip"] = {
|
|
"item": ti,
|
|
"index": clip_index
|
|
}
|
|
ti_color = ti.GetClipColor()
|
|
if filter is True:
|
|
if selecting_color in ti_color:
|
|
selected_clips.append(data)
|
|
# ti.ClearClipColor()
|
|
else:
|
|
selected_clips.append(data)
|
|
|
|
return selected_clips
|
|
|
|
|
|
def create_current_sequence_media_bin(sequence):
|
|
seq_name = sequence.GetName()
|
|
media_pool = get_current_project().GetMediaPool()
|
|
root_folder = media_pool.GetRootFolder()
|
|
sub_folders = root_folder.GetSubFolderList()
|
|
testing_names = list()
|
|
|
|
print(f"_ sub_folders: {sub_folders}")
|
|
for subfolder in sub_folders:
|
|
subf_name = subfolder.GetName()
|
|
if seq_name in subf_name:
|
|
testing_names.append(subfolder)
|
|
else:
|
|
testing_names.append(False)
|
|
|
|
matching = next((f for f in testing_names if f is not False), None)
|
|
|
|
if not matching:
|
|
new_folder = media_pool.AddSubFolder(root_folder, seq_name)
|
|
media_pool.SetCurrentFolder(new_folder)
|
|
else:
|
|
media_pool.SetCurrentFolder(matching)
|
|
|
|
return media_pool.GetCurrentFolder()
|
|
|
|
|
|
def get_name_with_data(clip_data, presets):
|
|
"""
|
|
Take hierarchy data from presets and build name with parents data
|
|
|
|
Args:
|
|
clip_data (dict): clip data from `get_current_track_items()`
|
|
presets (dict): data from create plugin
|
|
|
|
Returns:
|
|
list: name, data
|
|
|
|
"""
|
|
def _replace_hash_to_expression(name, text):
|
|
_spl = text.split("#")
|
|
_len = (len(_spl) - 1)
|
|
_repl = f"{{{name}:0>{_len}}}"
|
|
new_text = text.replace(("#" * _len), _repl)
|
|
return new_text
|
|
|
|
# presets data
|
|
clip_name = presets["clipName"]
|
|
hierarchy = presets["hierarchy"]
|
|
hierarchy_data = presets["hierarchyData"].copy()
|
|
count_from = presets["countFrom"]
|
|
steps = presets["steps"]
|
|
|
|
# reset rename_add
|
|
if self.rename_add < count_from:
|
|
self.rename_add = count_from
|
|
|
|
# shot num calculate
|
|
if self.rename_index == 0:
|
|
shot_num = self.rename_add
|
|
else:
|
|
shot_num = self.rename_add + steps
|
|
|
|
print(f"shot_num: {shot_num}")
|
|
|
|
# clip data
|
|
_data = {
|
|
"sequence": clip_data["sequence"].GetName(),
|
|
"track": clip_data["track"]["name"].replace(" ", "_"),
|
|
"shot": shot_num
|
|
}
|
|
|
|
# solve # in test to pythonic explression
|
|
for k, v in hierarchy_data.items():
|
|
if "#" not in v:
|
|
continue
|
|
hierarchy_data[k] = _replace_hash_to_expression(k, v)
|
|
|
|
# fill up pythonic expresisons
|
|
for k, v in hierarchy_data.items():
|
|
hierarchy_data[k] = v.format(**_data)
|
|
|
|
# fill up clip name and hierarchy keys
|
|
hierarchy = hierarchy.format(**hierarchy_data)
|
|
clip_name = clip_name.format(**hierarchy_data)
|
|
|
|
self.rename_add = shot_num
|
|
print(f"shot_num: {shot_num}")
|
|
|
|
return (clip_name, {
|
|
"hierarchy": hierarchy,
|
|
"hierarchyData": hierarchy_data
|
|
})
|
|
|
|
|
|
def create_compound_clip(clip_data, folder, rename=False, **kwargs):
|
|
"""
|
|
Convert timeline object into nested timeline object
|
|
|
|
Args:
|
|
clip_data (dict): timeline item object packed into dict
|
|
with project, timeline (sequence)
|
|
folder (resolve.MediaPool.Folder): media pool folder object,
|
|
rename (bool)[optional]: renaming in sequence or not
|
|
kwargs (optional): additional data needed for rename=True (presets)
|
|
|
|
Returns:
|
|
resolve.MediaPoolItem: media pool item with compound clip timeline(cct)
|
|
"""
|
|
# get basic objects form data
|
|
project = clip_data["project"]
|
|
sequence = clip_data["sequence"]
|
|
clip = clip_data["clip"]
|
|
|
|
# get details of objects
|
|
clip_item = clip["item"]
|
|
track = clip_data["track"]
|
|
|
|
mp = project.GetMediaPool()
|
|
|
|
# get clip attributes
|
|
clip_attributes = get_clip_attributes(clip_item)
|
|
print(f"_ clip_attributes: {pformat(clip_attributes)}")
|
|
|
|
if rename:
|
|
presets = kwargs.get("presets")
|
|
if presets:
|
|
name, data = get_name_with_data(clip_data, presets)
|
|
# add hirarchy data to clip attributes
|
|
clip_attributes.update(data)
|
|
else:
|
|
name = "{:0>3}_{:0>4}".format(
|
|
int(track["index"]), int(clip["index"]))
|
|
else:
|
|
# build name
|
|
clip_name_split = clip_item.GetName().split(".")
|
|
name = "_".join([
|
|
track["name"],
|
|
str(track["index"]),
|
|
clip_name_split[0],
|
|
str(clip["index"])]
|
|
)
|
|
|
|
# get metadata
|
|
mp_item = clip_item.GetMediaPoolItem()
|
|
mp_props = mp_item.GetClipProperty()
|
|
|
|
mp_first_frame = int(mp_props["Start"])
|
|
mp_last_frame = int(mp_props["End"])
|
|
|
|
# initialize basic source timing for otio
|
|
ci_l_offset = clip_item.GetLeftOffset()
|
|
ci_duration = clip_item.GetDuration()
|
|
rate = float(mp_props["FPS"])
|
|
|
|
# source rational times
|
|
mp_in_rc = opentime.RationalTime((ci_l_offset), rate)
|
|
mp_out_rc = opentime.RationalTime((ci_l_offset + ci_duration - 1), rate)
|
|
|
|
# get frame in and out for clip swaping
|
|
in_frame = opentime.to_frames(mp_in_rc)
|
|
out_frame = opentime.to_frames(mp_out_rc)
|
|
|
|
# keep original sequence
|
|
sq_origin = sequence
|
|
|
|
# Set current folder to input media_pool_folder:
|
|
mp.SetCurrentFolder(folder)
|
|
|
|
# check if clip doesnt exist already:
|
|
clips = folder.GetClipList()
|
|
cct = next((c for c in clips
|
|
if c.GetName() in name), None)
|
|
|
|
if cct:
|
|
print(f"_ cct exists: {cct}")
|
|
else:
|
|
# Create empty timeline in current folder and give name:
|
|
cct = mp.CreateEmptyTimeline(name)
|
|
|
|
# check if clip doesnt exist already:
|
|
clips = folder.GetClipList()
|
|
cct = next((c for c in clips
|
|
if c.GetName() in name), None)
|
|
print(f"_ cct created: {cct}")
|
|
|
|
# Set current timeline to created timeline:
|
|
project.SetCurrentTimeline(cct)
|
|
|
|
# Add input clip to the current timeline:
|
|
mp.AppendToTimeline([{
|
|
"mediaPoolItem": mp_item,
|
|
"startFrame": mp_first_frame,
|
|
"endFrame": mp_last_frame
|
|
}])
|
|
|
|
# Set current timeline to the working timeline:
|
|
project.SetCurrentTimeline(sq_origin)
|
|
|
|
# Add collected metadata and attributes to the comound clip:
|
|
if mp_item.GetMetadata(self.pype_metadata_key):
|
|
clip_attributes[self.pype_metadata_key] = mp_item.GetMetadata(
|
|
self.pype_metadata_key)[self.pype_metadata_key]
|
|
|
|
# stringify
|
|
clip_attributes = json.dumps(clip_attributes)
|
|
|
|
# add attributes to metadata
|
|
for k, v in mp_item.GetMetadata().items():
|
|
cct.SetMetadata(k, v)
|
|
|
|
# add metadata to cct
|
|
cct.SetMetadata(self.pype_metadata_key, clip_attributes)
|
|
|
|
# reset start timecode of the compound clip
|
|
cct.SetClipProperty("Start TC", mp_props["Start TC"])
|
|
|
|
# swap clips on timeline
|
|
swap_clips(clip_item, cct, name, in_frame, out_frame)
|
|
|
|
cct.SetClipColor("Pink")
|
|
return cct
|
|
|
|
|
|
def swap_clips(from_clip, to_clip, to_clip_name, to_in_frame, to_out_frame):
|
|
"""
|
|
Swaping clips on timeline in timelineItem
|
|
|
|
It will add take and activate it to the frame range which is inputted
|
|
|
|
Args:
|
|
from_clip (resolve.mediaPoolItem)
|
|
to_clip (resolve.mediaPoolItem)
|
|
to_clip_name (str): name of to_clip
|
|
to_in_frame (float): cut in frame, usually `GetLeftOffset()`
|
|
to_out_frame (float): cut out frame, usually left offset plus duration
|
|
|
|
Returns:
|
|
bool: True if successfully replaced
|
|
|
|
"""
|
|
# add clip item as take to timeline
|
|
take = from_clip.AddTake(
|
|
to_clip,
|
|
float(to_in_frame),
|
|
float(to_out_frame)
|
|
)
|
|
|
|
if not take:
|
|
return False
|
|
|
|
for take_index in range(1, (int(from_clip.GetTakesCount()) + 1)):
|
|
take_item = from_clip.GetTakeByIndex(take_index)
|
|
take_mp_item = take_item["mediaPoolItem"]
|
|
if to_clip_name in take_mp_item.GetName():
|
|
from_clip.SelectTakeByIndex(take_index)
|
|
from_clip.FinalizeTake()
|
|
return True
|
|
return False
|
|
|
|
|
|
def validate_tc(x):
|
|
# Validate and reformat timecode string
|
|
|
|
if len(x) != 11:
|
|
print('Invalid timecode. Try again.')
|
|
|
|
c = ':'
|
|
colonized = x[:2] + c + x[3:5] + c + x[6:8] + c + x[9:]
|
|
|
|
if colonized.replace(':', '').isdigit():
|
|
print(f"_ colonized: {colonized}")
|
|
return colonized
|
|
else:
|
|
print('Invalid timecode. Try again.')
|
|
|
|
|
|
def get_pype_clip_metadata(clip):
|
|
"""
|
|
Get pype metadata created by creator plugin
|
|
|
|
Attributes:
|
|
clip (resolve.TimelineItem): resolve's object
|
|
|
|
Returns:
|
|
dict: hierarchy, orig clip attributes
|
|
"""
|
|
mp_item = clip.GetMediaPoolItem()
|
|
metadata = mp_item.GetMetadata()
|
|
|
|
return metadata.get(self.pype_metadata_key)
|
|
|
|
|
|
def get_clip_attributes(clip):
|
|
"""
|
|
Collect basic atrributes from resolve timeline item
|
|
|
|
Args:
|
|
clip (resolve.TimelineItem): timeline item object
|
|
|
|
Returns:
|
|
dict: all collected attributres as key: values
|
|
"""
|
|
mp_item = clip.GetMediaPoolItem()
|
|
|
|
data = {
|
|
"clipIn": clip.GetStart(),
|
|
"clipOut": clip.GetEnd(),
|
|
"clipLeftOffset": clip.GetLeftOffset(),
|
|
"clipRightOffset": clip.GetRightOffset(),
|
|
"clipMarkers": clip.GetMarkers(),
|
|
"clipFlags": clip.GetFlagList(),
|
|
"sourceId": mp_item.GetMediaId(),
|
|
"sourceProperties": mp_item.GetClipProperty()
|
|
}
|
|
return data
|
|
|
|
|
|
def set_project_manager_to_folder_name(folder_name):
|
|
"""
|
|
Sets context of Project manager to given folder by name.
|
|
|
|
Searching for folder by given name from root folder to nested.
|
|
If no existing folder by name it will create one in root folder.
|
|
|
|
Args:
|
|
folder_name (str): name of searched folder
|
|
|
|
Returns:
|
|
bool: True if success
|
|
|
|
Raises:
|
|
Exception: Cannot create folder in root
|
|
|
|
"""
|
|
# initialize project manager
|
|
get_project_manager()
|
|
|
|
set_folder = False
|
|
|
|
# go back to root folder
|
|
if self.pm.GotoRootFolder():
|
|
log.info(f"Testing existing folder: {folder_name}")
|
|
folders = convert_resolve_list_type(
|
|
self.pm.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.pm.OpenFolder(folder_name)
|
|
|
|
if set_folder:
|
|
return True
|
|
|
|
# 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.pm.GotoRootFolder():
|
|
try:
|
|
# create folder by given name
|
|
self.pm.CreateFolder(folder_name)
|
|
self.pm.OpenFolder(folder_name)
|
|
return True
|
|
except NameError as e:
|
|
log.error((f"Folder with name `{folder_name}` cannot be created!"
|
|
f"Error: {e}"))
|
|
return False
|
|
|
|
|
|
def convert_resolve_list_type(resolve_list):
|
|
""" Resolve is using indexed dictionary as list type.
|
|
`{1.0: 'vaule'}`
|
|
This will convert it to normal list class
|
|
"""
|
|
assert isinstance(resolve_list, dict), (
|
|
"Input argument should be dict() type")
|
|
|
|
return [resolve_list[i] for i in sorted(resolve_list.keys())]
|