mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
1045 lines
37 KiB
Python
1045 lines
37 KiB
Python
import os
|
|
import re
|
|
import shutil
|
|
from copy import deepcopy
|
|
from xml.etree import ElementTree as ET
|
|
|
|
import qargparse
|
|
from qtpy import QtCore, QtWidgets
|
|
|
|
from openpype import style
|
|
from openpype.lib import Logger, StringTemplate
|
|
from openpype.pipeline import LegacyCreator, LoaderPlugin
|
|
from openpype.settings import get_current_project_settings
|
|
|
|
from . import constants
|
|
from . import lib as flib
|
|
from . import pipeline as fpipeline
|
|
|
|
log = Logger.get_logger(__name__)
|
|
|
|
|
|
class CreatorWidget(QtWidgets.QDialog):
|
|
|
|
# output items
|
|
items = dict()
|
|
_results_back = None
|
|
|
|
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 "Pype 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)
|
|
|
|
self.setStyleSheet(style.load_stylesheet())
|
|
|
|
@classmethod
|
|
def set_results_back(cls, value):
|
|
cls._results_back = value
|
|
|
|
@classmethod
|
|
def get_results_back(cls):
|
|
return cls._results_back
|
|
|
|
def _on_ok_clicked(self):
|
|
log.debug("ok is clicked: {}".format(self.items))
|
|
results_back = self._values(self.items)
|
|
self.set_results_back(results_back)
|
|
self.close()
|
|
|
|
def _on_cancel_clicked(self):
|
|
self.set_results_back(None)
|
|
self.close()
|
|
|
|
def showEvent(self, event):
|
|
self.set_results_back(None)
|
|
super(CreatorWidget, self).showEvent(event)
|
|
|
|
def _values(self, data, new_data=None):
|
|
new_data = new_data or dict()
|
|
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._values(v["value"])
|
|
if v["type"] == "section":
|
|
new_data.pop(k)
|
|
new_data = self._values(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_name, text, **kwargs):
|
|
# get type attribute from qwidgets
|
|
attr = getattr(QtWidgets, type_name)
|
|
|
|
# convert label text to normal capitalized text with spaces
|
|
label_text = self.camel_case_split(text)
|
|
|
|
# assign the new text to lable 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)
|
|
func_attr(val)
|
|
|
|
# add to layout
|
|
layout.addRow(label, item)
|
|
|
|
return item
|
|
|
|
def populate_widgets(self, data, content_layout=None):
|
|
"""
|
|
Populate widget from input dict.
|
|
|
|
Each plugin has its own set of widget rows defined in dictionary
|
|
each row values should have following keys: `type`, `target`,
|
|
`label`, `order`, `value` and optionally also `toolTip`.
|
|
|
|
Args:
|
|
data (dict): widget rows or organized groups defined
|
|
by types `dict` or `section`
|
|
content_layout (QtWidgets.QFormLayout)[optional]: used when nesting
|
|
|
|
Returns:
|
|
dict: redefined data dict updated with created widgets
|
|
|
|
"""
|
|
|
|
content_layout = content_layout or self.content_layout[-1]
|
|
# fix order of process by defined order value
|
|
ordered_keys = list(data.keys())
|
|
for k, v in data.items():
|
|
try:
|
|
# try removing a key from index which should
|
|
# be filled with new
|
|
ordered_keys.pop(v["order"])
|
|
except IndexError:
|
|
pass
|
|
# add key into correct order
|
|
ordered_keys.insert(v["order"], k)
|
|
|
|
# process ordered
|
|
for k in ordered_keys:
|
|
v = data[k]
|
|
tool_tip = v.get("toolTip", "")
|
|
if v["type"] == "dict":
|
|
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":
|
|
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"],
|
|
setValue=v["value"], setMinimum=0,
|
|
setMaximum=100000, setToolTip=tool_tip)
|
|
return data
|
|
|
|
|
|
class Spacer(QtWidgets.QWidget):
|
|
def __init__(self, height, *args, **kwargs):
|
|
super(self.__class__, self).__init__(*args, **kwargs)
|
|
|
|
self.setFixedHeight(height)
|
|
|
|
real_spacer = QtWidgets.QWidget(self)
|
|
real_spacer.setObjectName("Spacer")
|
|
real_spacer.setFixedHeight(height)
|
|
|
|
layout = QtWidgets.QVBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.addWidget(real_spacer)
|
|
|
|
self.setLayout(layout)
|
|
|
|
|
|
class Creator(LegacyCreator):
|
|
"""Creator class wrapper
|
|
"""
|
|
clip_color = constants.COLOR_MAP["purple"]
|
|
rename_index = None
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(Creator, self).__init__(*args, **kwargs)
|
|
self.presets = get_current_project_settings()[
|
|
"flame"]["create"].get(self.__class__.__name__, {})
|
|
|
|
# adding basic current context flame objects
|
|
self.project = flib.get_current_project()
|
|
self.sequence = flib.get_current_sequence(flib.CTX.selection)
|
|
|
|
if (self.options or {}).get("useSelection"):
|
|
self.selected = flib.get_sequence_segments(self.sequence, True)
|
|
else:
|
|
self.selected = flib.get_sequence_segments(self.sequence)
|
|
|
|
def create_widget(self, *args, **kwargs):
|
|
widget = CreatorWidget(*args, **kwargs)
|
|
widget.exec_()
|
|
return widget.get_results_back()
|
|
|
|
|
|
class PublishableClip:
|
|
"""
|
|
Convert a segment to publishable instance
|
|
|
|
Args:
|
|
segment (flame.PySegment): flame api object
|
|
kwargs (optional): additional data needed for rename=True (presets)
|
|
|
|
Returns:
|
|
flame.PySegment: flame api object
|
|
"""
|
|
vertical_clip_match = {}
|
|
marker_data = {}
|
|
types = {
|
|
"shot": "shot",
|
|
"folder": "folder",
|
|
"episode": "episode",
|
|
"sequence": "sequence",
|
|
"track": "sequence",
|
|
}
|
|
|
|
# parents search patern
|
|
parents_search_patern = r"\{([a-z]*?)\}"
|
|
|
|
# default templates for non-ui use
|
|
rename_default = False
|
|
hierarchy_default = "{_folder_}/{_sequence_}/{_track_}"
|
|
clip_name_default = "shot_{_trackIndex_:0>3}_{_clipIndex_:0>4}"
|
|
subset_name_default = "[ track name ]"
|
|
review_track_default = "[ none ]"
|
|
subset_family_default = "plate"
|
|
count_from_default = 10
|
|
count_steps_default = 10
|
|
vertical_sync_default = False
|
|
driving_layer_default = ""
|
|
index_from_segment_default = False
|
|
use_shot_name_default = False
|
|
include_handles_default = False
|
|
retimed_handles_default = True
|
|
retimed_framerange_default = True
|
|
|
|
def __init__(self, segment, **kwargs):
|
|
self.rename_index = kwargs["rename_index"]
|
|
self.family = kwargs["family"]
|
|
self.log = kwargs["log"]
|
|
|
|
# get main parent objects
|
|
self.current_segment = segment
|
|
sequence_name = flib.get_current_sequence([segment]).name.get_value()
|
|
self.sequence_name = str(sequence_name).replace(" ", "_")
|
|
|
|
self.clip_data = flib.get_segment_attributes(segment)
|
|
# segment (clip) main attributes
|
|
self.cs_name = self.clip_data["segment_name"]
|
|
self.cs_index = int(self.clip_data["segment"])
|
|
self.shot_name = self.clip_data["shot_name"]
|
|
|
|
# get track name and index
|
|
self.track_index = int(self.clip_data["track"])
|
|
track_name = self.clip_data["track_name"]
|
|
self.track_name = str(track_name).replace(" ", "_").replace(
|
|
"*", "noname{}".format(self.track_index))
|
|
|
|
# adding tag.family into tag
|
|
if kwargs.get("avalon"):
|
|
self.marker_data.update(kwargs["avalon"])
|
|
|
|
# add publish attribute to marker data
|
|
self.marker_data.update({"publish": True})
|
|
|
|
# adding ui inputs if any
|
|
self.ui_inputs = kwargs.get("ui_inputs", {})
|
|
|
|
self.log.info("Inside of plugin: {}".format(
|
|
self.marker_data
|
|
))
|
|
# populate default data before we get other attributes
|
|
self._populate_segment_default_data()
|
|
|
|
# use all populated default data to create all important attributes
|
|
self._populate_attributes()
|
|
|
|
# create parents with correct types
|
|
self._create_parents()
|
|
|
|
def convert(self):
|
|
|
|
# solve segment data and add them to marker data
|
|
self._convert_to_marker_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):
|
|
return
|
|
|
|
# deal with clip name
|
|
new_name = self.marker_data.pop("newClipName")
|
|
|
|
if self.rename and not self.use_shot_name:
|
|
# rename segment
|
|
self.current_segment.name = str(new_name)
|
|
self.marker_data["asset"] = str(new_name)
|
|
elif self.use_shot_name:
|
|
self.marker_data["asset"] = self.shot_name
|
|
self.marker_data["hierarchyData"]["shot"] = self.shot_name
|
|
else:
|
|
self.marker_data["asset"] = self.cs_name
|
|
self.marker_data["hierarchyData"]["shot"] = self.cs_name
|
|
|
|
if self.marker_data["heroTrack"] and self.review_layer:
|
|
self.marker_data["reviewTrack"] = self.review_layer
|
|
else:
|
|
self.marker_data["reviewTrack"] = None
|
|
|
|
# create pype tag on track_item and add data
|
|
fpipeline.imprint(self.current_segment, self.marker_data)
|
|
|
|
return self.current_segment
|
|
|
|
def _populate_segment_default_data(self):
|
|
""" Populate default formating data from segment. """
|
|
|
|
self.current_segment_default_data = {
|
|
"_folder_": "shots",
|
|
"_sequence_": self.sequence_name,
|
|
"_track_": self.track_name,
|
|
"_clip_": self.cs_name,
|
|
"_trackIndex_": self.track_index,
|
|
"_clipIndex_": self.cs_index
|
|
}
|
|
|
|
def _populate_attributes(self):
|
|
""" Populate main object attributes. """
|
|
# segment frame range and parent track name for vertical sync check
|
|
self.clip_in = int(self.clip_data["record_in"])
|
|
self.clip_out = int(self.clip_data["record_out"])
|
|
|
|
# define ui inputs if non gui mode was used
|
|
self.shot_num = self.cs_index
|
|
self.log.debug(
|
|
"____ self.shot_num: {}".format(self.shot_num))
|
|
|
|
# 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.use_shot_name = self.ui_inputs.get(
|
|
"useShotName", {}).get("value") or self.use_shot_name_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.current_segment_default_data.copy()
|
|
self.index_from_segment = self.ui_inputs.get(
|
|
"segmentIndex", {}).get("value") or self.index_from_segment_default
|
|
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.subset_name = self.ui_inputs.get(
|
|
"subsetName", {}).get("value") or self.subset_name_default
|
|
self.subset_family = self.ui_inputs.get(
|
|
"subsetFamily", {}).get("value") or self.subset_family_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
|
|
self.audio = self.ui_inputs.get(
|
|
"audio", {}).get("value") or False
|
|
self.include_handles = self.ui_inputs.get(
|
|
"includeHandles", {}).get("value") or self.include_handles_default
|
|
self.retimed_handles = (
|
|
self.ui_inputs.get("retimedHandles", {}).get("value")
|
|
or self.retimed_handles_default
|
|
)
|
|
self.retimed_framerange = (
|
|
self.ui_inputs.get("retimedFramerange", {}).get("value")
|
|
or self.retimed_framerange_default
|
|
)
|
|
|
|
# build subset name from layer name
|
|
if self.subset_name == "[ track name ]":
|
|
self.subset_name = self.track_name
|
|
|
|
# create subset for publishing
|
|
self.subset = self.subset_family + self.subset_name.capitalize()
|
|
|
|
def _replace_hash_to_expression(self, name, text):
|
|
""" Replace hash with number in correct padding. """
|
|
_spl = text.split("#")
|
|
_len = (len(_spl) - 1)
|
|
_repl = "{{{0}:0>{1}}}".format(name, _len)
|
|
return text.replace(("#" * _len), _repl)
|
|
|
|
def _convert_to_marker_data(self):
|
|
""" Convert internal data to marker data.
|
|
|
|
Populating the marker data into internal variable self.marker_data
|
|
"""
|
|
# define vertical sync attributes
|
|
hero_track = True
|
|
self.review_layer = ""
|
|
if self.vertical_sync and self.track_name not in self.driving_layer:
|
|
# if it is not then define vertical sync as None
|
|
hero_track = False
|
|
|
|
# increasing steps by index of rename iteration
|
|
if not self.index_from_segment:
|
|
self.count_steps *= self.rename_index
|
|
|
|
hierarchy_formating_data = {}
|
|
hierarchy_data = deepcopy(self.hierarchy_data)
|
|
_data = self.current_segment_default_data.copy()
|
|
if self.ui_inputs:
|
|
# adding tag metadata from ui
|
|
for _k, _v in self.ui_inputs.items():
|
|
if _v["target"] == "tag":
|
|
self.marker_data[_k] = _v["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 defalut
|
|
self.review_layer = self.review_track
|
|
|
|
# shot num calculate
|
|
if self.index_from_segment:
|
|
# use clip index from timeline
|
|
self.shot_num = self.count_steps * self.cs_index
|
|
else:
|
|
if self.rename_index == 0:
|
|
self.shot_num = self.count_from
|
|
else:
|
|
self.shot_num = self.count_from + self.count_steps
|
|
|
|
# clip name sequence number
|
|
_data.update({"shot": self.shot_num})
|
|
|
|
# solve # in test to pythonic expression
|
|
for _k, _v in hierarchy_data.items():
|
|
if "#" not in _v["value"]:
|
|
continue
|
|
hierarchy_data[
|
|
_k]["value"] = self._replace_hash_to_expression(
|
|
_k, _v["value"])
|
|
|
|
# fill up pythonic expresisons in hierarchy data
|
|
for k, _v in hierarchy_data.items():
|
|
hierarchy_formating_data[k] = _v["value"].format(**_data)
|
|
else:
|
|
# if no gui mode then just pass default data
|
|
hierarchy_formating_data = hierarchy_data
|
|
|
|
tag_hierarchy_data = self._solve_tag_hierarchy_data(
|
|
hierarchy_formating_data
|
|
)
|
|
|
|
tag_hierarchy_data.update({"heroTrack": True})
|
|
if hero_track and self.vertical_sync:
|
|
self.vertical_clip_match.update({
|
|
(self.clip_in, self.clip_out): tag_hierarchy_data
|
|
})
|
|
|
|
if not hero_track and self.vertical_sync:
|
|
# driving layer is set as negative match
|
|
for (_in, _out), hero_data in self.vertical_clip_match.items():
|
|
"""
|
|
Since only one instance of hero clip is expected in
|
|
`self.vertical_clip_match`, this will loop only once
|
|
until none hero clip will be matched with hero clip.
|
|
|
|
`tag_hierarchy_data` will be set only once for every
|
|
clip which is not hero clip.
|
|
"""
|
|
_hero_data = deepcopy(hero_data)
|
|
_hero_data.update({"heroTrack": False})
|
|
if _in <= self.clip_in and _out >= self.clip_out:
|
|
data_subset = hero_data["subset"]
|
|
# add track index in case duplicity of names in hero data
|
|
if self.subset in data_subset:
|
|
_hero_data["subset"] = self.subset + str(
|
|
self.track_index)
|
|
# in case track name and subset name is the same then add
|
|
if self.subset_name == self.track_name:
|
|
_hero_data["subset"] = self.subset
|
|
# assing data to return hierarchy data to tag
|
|
tag_hierarchy_data = _hero_data
|
|
break
|
|
|
|
# add data to return data dict
|
|
self.marker_data.update(tag_hierarchy_data)
|
|
|
|
def _solve_tag_hierarchy_data(self, hierarchy_formating_data):
|
|
""" Solve marker data from hierarchy data and templates. """
|
|
# fill up clip name and hierarchy keys
|
|
hierarchy_filled = self.hierarchy.format(**hierarchy_formating_data)
|
|
clip_name_filled = self.clip_name.format(**hierarchy_formating_data)
|
|
|
|
# remove shot from hierarchy data: is not needed anymore
|
|
hierarchy_formating_data.pop("shot")
|
|
|
|
return {
|
|
"newClipName": clip_name_filled,
|
|
"hierarchy": hierarchy_filled,
|
|
"parents": self.parents,
|
|
"hierarchyData": hierarchy_formating_data,
|
|
"subset": self.subset,
|
|
"family": self.subset_family,
|
|
"families": [self.family]
|
|
}
|
|
|
|
def _convert_to_entity(self, type, template):
|
|
""" Converting input key to key with type. """
|
|
# convert to entity type
|
|
entity_type = self.types.get(type, None)
|
|
|
|
assert entity_type, "Missing entity type for `{}`".format(
|
|
type
|
|
)
|
|
|
|
# first collect formating data to use for formating template
|
|
formating_data = {}
|
|
for _k, _v in self.hierarchy_data.items():
|
|
value = _v["value"].format(
|
|
**self.current_segment_default_data)
|
|
formating_data[_k] = value
|
|
|
|
return {
|
|
"entity_type": entity_type,
|
|
"entity_name": template.format(
|
|
**formating_data
|
|
)
|
|
}
|
|
|
|
def _create_parents(self):
|
|
""" Create parents and return it in list. """
|
|
self.parents = []
|
|
|
|
patern = re.compile(self.parents_search_patern)
|
|
|
|
par_split = [(patern.findall(t).pop(), t)
|
|
for t in self.hierarchy.split("/")]
|
|
|
|
for type, template in par_split:
|
|
parent = self._convert_to_entity(type, template)
|
|
self.parents.append(parent)
|
|
|
|
|
|
# Publishing plugin functions
|
|
|
|
# Loader plugin functions
|
|
class ClipLoader(LoaderPlugin):
|
|
"""A basic clip loader for Flame
|
|
|
|
This will implement the basic behavior for a loader to inherit from that
|
|
will containerize the reference and will implement the `remove` and
|
|
`update` logic.
|
|
|
|
"""
|
|
log = log
|
|
|
|
options = [
|
|
qargparse.Boolean(
|
|
"handles",
|
|
label="Set handles",
|
|
default=0,
|
|
help="Also set handles to clip as In/Out marks"
|
|
)
|
|
]
|
|
|
|
_mapping = None
|
|
|
|
def get_colorspace(self, context):
|
|
"""Get colorspace name
|
|
|
|
Look either to version data or representation data.
|
|
|
|
Args:
|
|
context (dict): version context data
|
|
|
|
Returns:
|
|
str: colorspace name or None
|
|
"""
|
|
version = context['version']
|
|
version_data = version.get("data", {})
|
|
colorspace = version_data.get(
|
|
"colorspace", None
|
|
)
|
|
|
|
if (
|
|
not colorspace
|
|
or colorspace == "Unknown"
|
|
):
|
|
colorspace = context["representation"]["data"].get(
|
|
"colorspace", None)
|
|
|
|
return colorspace
|
|
|
|
@classmethod
|
|
def get_native_colorspace(cls, input_colorspace):
|
|
"""Return native colorspace name.
|
|
|
|
Args:
|
|
input_colorspace (str | None): colorspace name
|
|
|
|
Returns:
|
|
str: native colorspace name defined in mapping or None
|
|
"""
|
|
if not cls._mapping:
|
|
settings = get_current_project_settings()["flame"]
|
|
mapping = settings["imageio"]["profilesMapping"]["inputs"]
|
|
cls._mapping = {
|
|
input["ocioName"]: input["flameName"]
|
|
for input in mapping
|
|
}
|
|
|
|
return cls._mapping.get(input_colorspace)
|
|
|
|
|
|
class OpenClipSolver(flib.MediaInfoFile):
|
|
create_new_clip = False
|
|
|
|
log = log
|
|
|
|
def __init__(self, openclip_file_path, feed_data, logger=None):
|
|
self.out_file = openclip_file_path
|
|
|
|
# replace log if any
|
|
if logger:
|
|
self.log = logger
|
|
|
|
# new feed variables:
|
|
feed_path = feed_data.pop("path")
|
|
|
|
# initialize parent class
|
|
super(OpenClipSolver, self).__init__(
|
|
feed_path,
|
|
logger=logger
|
|
)
|
|
|
|
# get other metadata
|
|
self.feed_version_name = feed_data["version"]
|
|
self.feed_colorspace = feed_data.get("colorspace")
|
|
self.log.debug("feed_version_name: {}".format(self.feed_version_name))
|
|
|
|
# layer rename variables
|
|
self.layer_rename_template = feed_data["layer_rename_template"]
|
|
self.layer_rename_patterns = feed_data["layer_rename_patterns"]
|
|
self.context_data = feed_data["context_data"]
|
|
|
|
# derivate other feed variables
|
|
self.feed_basename = os.path.basename(feed_path)
|
|
self.feed_dir = os.path.dirname(feed_path)
|
|
self.feed_ext = os.path.splitext(self.feed_basename)[1][1:].lower()
|
|
self.log.debug("feed_ext: {}".format(self.feed_ext))
|
|
self.log.debug("out_file: {}".format(self.out_file))
|
|
if not self._is_valid_tmp_file(self.out_file):
|
|
self.create_new_clip = True
|
|
|
|
def _is_valid_tmp_file(self, file):
|
|
# check if file exists
|
|
if os.path.isfile(file):
|
|
# test also if file is not empty
|
|
with open(file) as f:
|
|
lines = f.readlines()
|
|
|
|
if len(lines) > 2:
|
|
return True
|
|
|
|
# file is probably corrupted
|
|
os.remove(file)
|
|
return False
|
|
|
|
def make(self):
|
|
|
|
if self.create_new_clip:
|
|
# New openClip
|
|
self._create_new_open_clip()
|
|
else:
|
|
self._update_open_clip()
|
|
|
|
def _clear_handler(self, xml_object):
|
|
for handler in xml_object.findall("./handler"):
|
|
self.log.info("Handler found")
|
|
xml_object.remove(handler)
|
|
|
|
def _create_new_open_clip(self):
|
|
self.log.info("Building new openClip")
|
|
|
|
for tmp_xml_track in self.clip_data.iter("track"):
|
|
# solve track (layer) name
|
|
self._rename_track_name(tmp_xml_track)
|
|
|
|
tmp_xml_feeds = tmp_xml_track.find('feeds')
|
|
tmp_xml_feeds.set('currentVersion', self.feed_version_name)
|
|
|
|
for tmp_feed in tmp_xml_track.iter("feed"):
|
|
tmp_feed.set('vuid', self.feed_version_name)
|
|
|
|
# add colorspace if any is set
|
|
if self.feed_colorspace:
|
|
self._add_colorspace(tmp_feed, self.feed_colorspace)
|
|
|
|
self._clear_handler(tmp_feed)
|
|
|
|
tmp_xml_versions_obj = self.clip_data.find('versions')
|
|
tmp_xml_versions_obj.set('currentVersion', self.feed_version_name)
|
|
for xml_new_version in tmp_xml_versions_obj:
|
|
xml_new_version.set('uid', self.feed_version_name)
|
|
xml_new_version.set('type', 'version')
|
|
|
|
self._clear_handler(self.clip_data)
|
|
self.log.info("Adding feed version: {}".format(self.feed_basename))
|
|
|
|
self.write_clip_data_to_file(self.out_file, self.clip_data)
|
|
|
|
def _get_xml_track_obj_by_uid(self, xml_data, uid):
|
|
# loop all tracks of input xml data
|
|
for xml_track in xml_data.iter("track"):
|
|
track_uid = xml_track.get("uid")
|
|
self.log.debug(
|
|
">> track_uid:uid: {}:{}".format(track_uid, uid))
|
|
|
|
# get matching uids
|
|
if uid == track_uid:
|
|
return xml_track
|
|
|
|
def _rename_track_name(self, xml_track_data):
|
|
layer_uid = xml_track_data.get("uid")
|
|
name_obj = xml_track_data.find("name")
|
|
layer_name = name_obj.text
|
|
|
|
if (
|
|
self.layer_rename_patterns
|
|
and not any(
|
|
re.search(lp_.lower(), layer_name.lower())
|
|
for lp_ in self.layer_rename_patterns
|
|
)
|
|
):
|
|
return
|
|
|
|
formating_data = self._update_formating_data(
|
|
layerName=layer_name,
|
|
layerUID=layer_uid
|
|
)
|
|
name_obj.text = StringTemplate(
|
|
self.layer_rename_template
|
|
).format(formating_data)
|
|
|
|
def _update_formating_data(self, **kwargs):
|
|
""" Updating formating data for layer rename
|
|
|
|
Attributes:
|
|
key=value (optional): will be included to formating data
|
|
as {key: value}
|
|
Returns:
|
|
dict: anatomy context data for formating
|
|
"""
|
|
self.log.debug(">> self.clip_data: {}".format(self.clip_data))
|
|
clip_name_obj = self.clip_data.find("name")
|
|
data = {
|
|
"originalBasename": clip_name_obj.text
|
|
}
|
|
# include version context data
|
|
data.update(self.context_data)
|
|
# include input kwargs data
|
|
data.update(kwargs)
|
|
return data
|
|
|
|
def _update_open_clip(self):
|
|
self.log.info("Updating openClip ..")
|
|
|
|
out_xml = ET.parse(self.out_file)
|
|
out_xml = out_xml.getroot()
|
|
|
|
self.log.debug(">> out_xml: {}".format(out_xml))
|
|
# loop tmp tracks
|
|
updated_any = False
|
|
for tmp_xml_track in self.clip_data.iter("track"):
|
|
# solve track (layer) name
|
|
self._rename_track_name(tmp_xml_track)
|
|
|
|
# get tmp track uid
|
|
tmp_track_uid = tmp_xml_track.get("uid")
|
|
self.log.debug(">> tmp_track_uid: {}".format(tmp_track_uid))
|
|
|
|
# get out data track by uid
|
|
out_track_element = self._get_xml_track_obj_by_uid(
|
|
out_xml, tmp_track_uid)
|
|
self.log.debug(
|
|
">> out_track_element: {}".format(out_track_element))
|
|
|
|
# loop tmp feeds
|
|
for tmp_xml_feed in tmp_xml_track.iter("feed"):
|
|
new_path_obj = tmp_xml_feed.find(
|
|
"spans/span/path")
|
|
new_path = new_path_obj.text
|
|
|
|
# check if feed path already exists in track's feeds
|
|
if (
|
|
out_track_element is not None
|
|
and self._feed_exists(out_track_element, new_path)
|
|
):
|
|
continue
|
|
|
|
# rename versions on feeds
|
|
tmp_xml_feed.set('vuid', self.feed_version_name)
|
|
self._clear_handler(tmp_xml_feed)
|
|
|
|
# update fps from MediaInfoFile class
|
|
if self.fps is not None:
|
|
tmp_feed_fps_obj = tmp_xml_feed.find(
|
|
"startTimecode/rate")
|
|
tmp_feed_fps_obj.text = str(self.fps)
|
|
|
|
# update start_frame from MediaInfoFile class
|
|
if self.start_frame is not None:
|
|
tmp_feed_nb_ticks_obj = tmp_xml_feed.find(
|
|
"startTimecode/nbTicks")
|
|
tmp_feed_nb_ticks_obj.text = str(self.start_frame)
|
|
|
|
# update drop_mode from MediaInfoFile class
|
|
if self.drop_mode is not None:
|
|
tmp_feed_drop_mode_obj = tmp_xml_feed.find(
|
|
"startTimecode/dropMode")
|
|
tmp_feed_drop_mode_obj.text = str(self.drop_mode)
|
|
|
|
# add colorspace if any is set
|
|
if self.feed_colorspace is not None:
|
|
self._add_colorspace(tmp_xml_feed, self.feed_colorspace)
|
|
|
|
# then append/update feed to correct track in output
|
|
if out_track_element:
|
|
self.log.debug("updating track element ..")
|
|
# update already present track
|
|
out_feeds = out_track_element.find('feeds')
|
|
out_feeds.set('currentVersion', self.feed_version_name)
|
|
out_feeds.append(tmp_xml_feed)
|
|
|
|
self.log.info(
|
|
"Appending new feed: {}".format(
|
|
self.feed_version_name))
|
|
else:
|
|
self.log.debug("adding new track element ..")
|
|
# create new track as it doesnt exists yet
|
|
# set current version to feeds on tmp
|
|
tmp_xml_feeds = tmp_xml_track.find('feeds')
|
|
tmp_xml_feeds.set('currentVersion', self.feed_version_name)
|
|
out_tracks = out_xml.find("tracks")
|
|
out_tracks.append(tmp_xml_track)
|
|
|
|
updated_any = True
|
|
|
|
if updated_any:
|
|
# Append vUID to versions
|
|
out_xml_versions_obj = out_xml.find('versions')
|
|
out_xml_versions_obj.set(
|
|
'currentVersion', self.feed_version_name)
|
|
new_version_obj = ET.Element(
|
|
"version", {"type": "version", "uid": self.feed_version_name})
|
|
out_xml_versions_obj.insert(0, new_version_obj)
|
|
|
|
self._clear_handler(out_xml)
|
|
|
|
# fist create backup
|
|
self._create_openclip_backup_file(self.out_file)
|
|
|
|
self.log.info("Adding feed version: {}".format(
|
|
self.feed_version_name))
|
|
|
|
self.write_clip_data_to_file(self.out_file, out_xml)
|
|
|
|
self.log.debug("OpenClip Updated: {}".format(self.out_file))
|
|
|
|
def _feed_exists(self, xml_data, path):
|
|
# loop all available feed paths and check if
|
|
# the path is not already in file
|
|
for src_path in xml_data.iter('path'):
|
|
if path == src_path.text:
|
|
self.log.warning(
|
|
"Not appending file as it already is in .clip file")
|
|
return True
|
|
|
|
def _create_openclip_backup_file(self, file):
|
|
bck_file = "{}.bak".format(file)
|
|
# if backup does not exist
|
|
if not os.path.isfile(bck_file):
|
|
shutil.copy2(file, bck_file)
|
|
else:
|
|
# in case it exists and is already multiplied
|
|
created = False
|
|
for _i in range(1, 99):
|
|
bck_file = "{name}.bak.{idx:0>2}".format(
|
|
name=file,
|
|
idx=_i)
|
|
# create numbered backup file
|
|
if not os.path.isfile(bck_file):
|
|
shutil.copy2(file, bck_file)
|
|
created = True
|
|
break
|
|
# in case numbered does not exists
|
|
if not created:
|
|
bck_file = "{}.bak.last".format(file)
|
|
shutil.copy2(file, bck_file)
|
|
|
|
def _add_colorspace(self, feed_obj, profile_name):
|
|
feed_storage_obj = feed_obj.find("storageFormat")
|
|
feed_clr_obj = feed_storage_obj.find("colourSpace")
|
|
if feed_clr_obj is not None:
|
|
feed_clr_obj = ET.Element(
|
|
"colourSpace", {"type": "string"})
|
|
feed_clr_obj.text = profile_name
|
|
feed_storage_obj.append(feed_clr_obj)
|