flame: fixing Task creation and adding notes

This commit is contained in:
Jakub Jezek 2021-10-30 23:44:52 +02:00
parent 70928f410c
commit a4b2bf0951
No known key found for this signature in database
GPG key ID: D8548FBF690B100A

View file

@ -1,356 +1,583 @@
import sys
import collections
import six
import pyblish.api
from avalon import io
from __future__ import print_function
from PySide2 import QtWidgets, QtCore
from pprint import pformat
from contextlib import contextmanager
# Copy of constant `openpype_modules.ftrack.lib.avalon_sync.CUST_ATTR_AUTO_SYNC`
CUST_ATTR_AUTO_SYNC = "avalon_auto_sync"
CUST_ATTR_GROUP = "openpype"
# Constants
WORKFILE_START_FRAME = 1001
HIERARCHY_TEMPLATE = "shots[Folder]/{sequence}[Sequence]"
CREATE_TASK_TYPE = "Compositing"
# Copy of `get_pype_attr` from openpype_modules.ftrack.lib
# TODO import from openpype's ftrack module when possible to not break Python 2
def get_pype_attr(session, split_hierarchical=True):
custom_attributes = []
hier_custom_attributes = []
# TODO remove deprecated "avalon" group from query
cust_attrs_query = (
"select id, entity_type, object_type_id, is_hierarchical, default"
" from CustomAttributeConfiguration"
# Kept `pype` for Backwards Compatiblity
" where group.name in (\"pype\", \"{}\")"
).format(CUST_ATTR_GROUP)
all_avalon_attr = session.query(cust_attrs_query).all()
for cust_attr in all_avalon_attr:
if split_hierarchical and cust_attr["is_hierarchical"]:
hier_custom_attributes.append(cust_attr)
continue
@contextmanager
def maintained_ftrack_session():
import ftrack_api
import os
custom_attributes.append(cust_attr)
def validate_credentials(url, user, api):
first_validation = True
if not user:
print('- Ftrack Username is not set')
first_validation = False
if not api:
print('- Ftrack API key is not set')
first_validation = False
if not first_validation:
return False
if split_hierarchical:
# return tuple
return custom_attributes, hier_custom_attributes
return custom_attributes
class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
"""
Create entities in ftrack based on collected data from premiere
Example of entry data:
{
"ProjectXS": {
"entity_type": "Project",
"custom_attributes": {
"fps": 24,...
},
"tasks": [
"Compositing",
"Lighting",... *task must exist as task type in project schema*
],
"childs": {
"sq01": {
"entity_type": "Sequence",
...
}
try:
session = ftrack_api.Session(
server_url=url,
api_user=user,
api_key=api
)
session.close()
except Exception as _e:
print(
"Can't log into Ftrack with used credentials: {}".format(
_e)
)
ftrack_cred = {
'Ftrack server': str(url),
'Username': str(user),
'API key': str(api),
}
}
}
item_lens = [len(key) + 1 for key in ftrack_cred]
justify_len = max(*item_lens)
for key, value in ftrack_cred.items():
print('{} {}'.format((key + ':').ljust(
justify_len, ' '), value))
return False
print(
'Credentials Username: "{}", API key: "{}" are valid.'.format(
user, api)
)
return True
# fill your own credentials
url = os.getenv("FTRACK_SERVER")
user = os.getenv("FTRACK_API_USER")
api = os.getenv("FTRACK_API_KEY")
try:
assert validate_credentials(url, user, api), (
"Ftrack credentials failed")
# open ftrack session
session = ftrack_api.Session(
server_url=url,
api_user=user,
api_key=api
)
yield session
except Exception as _E:
print(
"ERROR: {}".format(_E))
finally:
# close the session
session.close()
class FlameLabel(QtWidgets.QLabel):
"""
Custom Qt Flame Label Widget
For different label looks set label_type as: 'normal', 'background', or 'outline'
To use:
label = FlameLabel('Label Name', 'normal', window)
"""
order = pyblish.api.IntegratorOrder - 0.04
label = 'Integrate Hierarchy To Ftrack'
families = ["shot"]
hosts = ["hiero", "resolve", "standalonepublisher"]
optional = False
def __init__(self, label_name, label_type, parent_window, *args, **kwargs):
super(FlameLabel, self).__init__(*args, **kwargs)
def process(self, context):
self.context = context
if "hierarchyContext" not in self.context.data:
return
self.setText(label_name)
self.setParent(parent_window)
self.setMinimumSize(130, 28)
self.setMaximumHeight(28)
self.setFocusPolicy(QtCore.Qt.NoFocus)
hierarchy_context = self.context.data["hierarchyContext"]
# Set label stylesheet based on label_type
self.session = self.context.data["ftrackSession"]
project_name = self.context.data["projectEntity"]["name"]
query = 'Project where full_name is "{}"'.format(project_name)
project = self.session.query(query).one()
auto_sync_state = project[
"custom_attributes"][CUST_ATTR_AUTO_SYNC]
if label_type == 'normal':
self.setStyleSheet('QLabel {color: #9a9a9a; border-bottom: 1px inset #282828; font: 14px "Discreet"}'
'QLabel:disabled {color: #6a6a6a}')
elif label_type == 'background':
self.setAlignment(QtCore.Qt.AlignCenter)
self.setStyleSheet(
'color: #9a9a9a; background-color: #393939; font: 14px "Discreet"')
elif label_type == 'outline':
self.setAlignment(QtCore.Qt.AlignCenter)
self.setStyleSheet(
'color: #9a9a9a; background-color: #212121; border: 1px solid #404040; font: 14px "Discreet"')
if not io.Session:
io.install()
self.ft_project = None
class FlameLineEdit(QtWidgets.QLineEdit):
"""
Custom Qt Flame Line Edit Widget
input_data = hierarchy_context
Main window should include this: window.setFocusPolicy(QtCore.Qt.StrongFocus)
# disable termporarily ftrack project's autosyncing
if auto_sync_state:
self.auto_sync_off(project)
To use:
try:
# import ftrack hierarchy
self.import_to_ftrack(input_data)
except Exception:
raise
finally:
if auto_sync_state:
self.auto_sync_on(project)
line_edit = FlameLineEdit('Some text here', window)
"""
def import_to_ftrack(self, input_data, parent=None):
# Prequery hiearchical custom attributes
hier_custom_attributes = get_pype_attr(self.session)[1]
hier_attr_by_key = {
attr["key"]: attr
for attr in hier_custom_attributes
}
# Get ftrack api module (as they are different per python version)
ftrack_api = self.context.data["ftrackPythonModule"]
def __init__(self, text, parent_window, *args, **kwargs):
super(FlameLineEdit, self).__init__(*args, **kwargs)
for entity_name in input_data:
entity_data = input_data[entity_name]
entity_type = entity_data['entity_type']
self.log.debug(entity_data)
self.log.debug(entity_type)
self.setText(text)
self.setParent(parent_window)
self.setMinimumHeight(28)
self.setMinimumWidth(110)
self.setStyleSheet('QLineEdit {color: #9a9a9a; background-color: #373e47; selection-color: #262626; selection-background-color: #b8b1a7; font: 14px "Discreet"}'
'QLineEdit:focus {background-color: #474e58}'
'QLineEdit:disabled {color: #6a6a6a; background-color: #373737}')
if entity_type.lower() == 'project':
query = 'Project where full_name is "{}"'.format(entity_name)
entity = self.session.query(query).one()
self.ft_project = entity
self.task_types = self.get_all_task_types(entity)
elif self.ft_project is None or parent is None:
raise AssertionError(
"Collected items are not in right order!"
)
class FlameTreeWidget(QtWidgets.QTreeWidget):
"""
Custom Qt Flame Tree Widget
# try to find if entity already exists
else:
query = (
'TypedContext where name is "{0}" and '
'project_id is "{1}"'
).format(entity_name, self.ft_project["id"])
try:
entity = self.session.query(query).one()
except Exception:
entity = None
To use:
# Create entity if not exists
if entity is None:
entity = self.create_entity(
name=entity_name,
type=entity_type,
parent=parent
)
# self.log.info('entity: {}'.format(dict(entity)))
# CUSTOM ATTRIBUTES
custom_attributes = entity_data.get('custom_attributes', [])
instances = [
i for i in self.context if i.data['asset'] in entity['name']
]
for key in custom_attributes:
hier_attr = hier_attr_by_key.get(key)
# Use simple method if key is not hierarchical
if not hier_attr:
assert (key in entity['custom_attributes']), (
'Missing custom attribute key: `{0}` in attrs: '
'`{1}`'.format(key, entity['custom_attributes'].keys())
)
tree_headers = ['Header1', 'Header2', 'Header3', 'Header4']
tree = FlameTreeWidget(tree_headers, window)
"""
entity['custom_attributes'][key] = custom_attributes[key]
def __init__(self, tree_headers, parent_window, *args, **kwargs):
super(FlameTreeWidget, self).__init__(*args, **kwargs)
else:
# Use ftrack operations method to set hiearchical
# attribute value.
# - this is because there may be non hiearchical custom
# attributes with different properties
entity_key = collections.OrderedDict()
entity_key["configuration_id"] = hier_attr["id"]
entity_key["entity_id"] = entity["id"]
self.session.recorded_operations.push(
ftrack_api.operation.UpdateEntityOperation(
"ContextCustomAttributeValue",
entity_key,
"value",
ftrack_api.symbol.NOT_SET,
custom_attributes[key]
self.setMinimumWidth(1000)
self.setMinimumHeight(300)
self.setSortingEnabled(True)
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.setAlternatingRowColors(True)
self.setFocusPolicy(QtCore.Qt.NoFocus)
self.setStyleSheet('QTreeWidget {color: #9a9a9a; background-color: #2a2a2a; alternate-background-color: #2d2d2d; font: 14px "Discreet"}'
'QTreeWidget::item:selected {color: #d9d9d9; background-color: #474747; border: 1px solid #111111}'
'QHeaderView {color: #9a9a9a; background-color: #393939; font: 14px "Discreet"}'
'QTreeWidget::item:selected {selection-background-color: #111111}'
'QMenu {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}'
'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}')
self.verticalScrollBar().setStyleSheet('color: #818181')
self.horizontalScrollBar().setStyleSheet('color: #818181')
self.setHeaderLabels(tree_headers)
class FlameButton(QtWidgets.QPushButton):
"""
Custom Qt Flame Button Widget
To use:
button = FlameButton('Button Name', do_this_when_pressed, window)
"""
def __init__(self, button_name, do_when_pressed, parent_window, *args, **kwargs):
super(FlameButton, self).__init__(*args, **kwargs)
self.setText(button_name)
self.setParent(parent_window)
self.setMinimumSize(QtCore.QSize(110, 28))
self.setMaximumSize(QtCore.QSize(110, 28))
self.setFocusPolicy(QtCore.Qt.NoFocus)
self.clicked.connect(do_when_pressed)
self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: #424142; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}'
'QPushButton:pressed {color: #d9d9d9; background-color: #4f4f4f; border-top: 1px inset #666666; font: italic}'
'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}')
def main_window(selection):
def timecode_to_frames(timecode, framerate):
def _seconds(value):
if isinstance(value, str):
_zip_ft = zip((3600, 60, 1, 1 / framerate), value.split(':'))
return sum(f * float(t) for f, t in _zip_ft)
elif isinstance(value, (int, float)):
return value / framerate
return 0
def _frames(seconds):
return seconds * framerate
def timecode_to_frames(_timecode, start=None):
return _frames(_seconds(_timecode) - _seconds(start))
if '+' in timecode:
timecode = timecode.replace('+', ':')
elif '#' in timecode:
timecode = timecode.replace('#', ':')
frames = int(round(timecode_to_frames(timecode, start='00:00:00:00')))
return frames
def timeline_info(selection):
import flame
# identificar as informacoes dos segmentos na timeline
for sequence in selection:
frame_rate = float(str(sequence.frame_rate)[:-4])
for ver in sequence.versions:
for tracks in ver.tracks:
for segment in tracks.segments:
print(segment.type)
# get clip frame duration
record_duration = str(segment.record_duration)[1:-1]
clip_duration = timecode_to_frames(
record_duration, frame_rate)
# populate shot source metadata
shot_description = ""
for attr in ["tape_name", "source_name", "head",
"tail", "file_path"]:
if not hasattr(segment, attr):
continue
_value = getattr(segment, attr)
_label = attr.replace("_", " ").capitalize()
row = "{}: {}\n".format(_label, _value)
shot_description += row
# Add timeline segment to tree
QtWidgets.QTreeWidgetItem(tree, [
str(sequence.name)[1:-1], # seq
str(segment.name)[1:-1], # shot
CREATE_TASK_TYPE, # task type
str(WORKFILE_START_FRAME), # start frame
str(clip_duration), # clip duration
"0:0", # handles
shot_description, # shot description
str(segment.comment)[1:-1] # task description
]).setFlags(
QtCore.Qt.ItemIsEditable
| QtCore.Qt.ItemIsEnabled
| QtCore.Qt.ItemIsSelectable
)
)
for instance in instances:
instance.data['ftrackEntity'] = entity
# Select top item in tree
tree.setCurrentItem(tree.topLevelItem(0))
def select_all():
tree.selectAll()
def send_to_ftrack():
import flame
import six
import sys
import re
def create_ftrack_entity(session, type, name, parent=None):
parent = parent or f_project
entity = session.create(type, {
'name': name,
'parent': parent
})
try:
session.commit()
except Exception:
tp, value, tb = sys.exc_info()
session.rollback()
session._configure_locations()
six.reraise(tp, value, tb)
return entity
def get_ftrack_entity(session, type, name, parent):
query = '{} where name is "{}" and project_id is "{}"'.format(
type, name, f_project["id"])
try:
entity = session.query(query).one()
except Exception:
entity = None
# if entity doesnt exist then create one
if not entity:
entity = create_ftrack_entity(
session,
type,
name,
parent
)
return entity
def generate_parents_from_template(template):
parents = []
t_split = template.split("/")
replace_patern = re.compile(r"(\[.*\])")
type_patern = re.compile(r"\[(.*)\]")
for t_s in t_split:
match_type = type_patern.findall(t_s)
if not match_type:
raise Exception((
"Missing correct type flag in : {}"
"/n Example: name[Type]").format(
t_s)
)
new_name = re.sub(replace_patern, "", t_s)
f_type = match_type.pop()
parents.append((new_name, f_type))
return parents
def get_all_task_types():
tasks = {}
proj_template = f_project['project_schema']
temp_task_types = proj_template['_task_type_schema']['types']
for type in temp_task_types:
if type['name'] not in tasks:
tasks[type['name']] = type
return tasks
def create_task(task_type, parent):
existing_task = [
child for child in parent['children']
if child.entity_type.lower() == 'task'
if child['name'].lower() in task_type.lower()
]
if existing_task:
return existing_task
task = session.create('Task', {
"name": task_type.lower(),
"parent": parent
})
task_types = get_all_task_types()
task["type"] = task_types[task_type]
return task
# start procedure
with maintained_ftrack_session() as session:
print("Ftrack session is: {}".format(session))
# get project name from flame current project
project_name = flame.project.current_project.name
# get project from ftrack -
# ftrack project name has to be the same as flame project!
query = 'Project where full_name is "{}"'.format(project_name)
f_project = session.query(query).one()
print("Ftrack project is: {}".format(f_project))
# Get all selected items from treewidget
for item in tree.selectedItems():
# solve handle start and end
handles = item.text(5)
if ":" in handles:
_s, _e = handles.split(":")
handle_start = int(_s)
handle_end = int(_e)
else:
handle_start = int(handles)
handle_end = int(handles)
# frame ranges
frame_start = int(item.text(3))
frame_duration = int(item.text(4))
frame_end = frame_start + frame_duration
# description
shot_description = item.text(6)
task_description = item.text(7)
# other
task_type = item.text(2)
shot_name = item.text(1)
sequence_name = item.text(0)
# populate full shot info
shot_attributes = {
"sequence": sequence_name,
"shot": shot_name,
"task": task_type
}
# format hierarchy template
hierarchy_text = hierarchy_template.text()
hierarchy_text = hierarchy_text.format(**shot_attributes)
print(hierarchy_text)
# solve parents
parents = generate_parents_from_template(hierarchy_text)
print(parents)
# obtain shot parents entities
_parent = None
for _name, _type in parents:
p_entity = get_ftrack_entity(
session,
_type,
_name,
_parent
)
print(p_entity)
_parent = p_entity
# obtain shot ftrack entity
f_s_entity = get_ftrack_entity(
session,
"Shot",
item.text(1),
_parent
)
print("Shot entity is: {}".format(f_s_entity))
# create custom attributtes
custom_attrs = {
"frameStart": frame_start,
"frameEnd": frame_end,
"handleStart": handle_start,
"handleEnd": handle_end
}
# update custom attributes on shot entity
for key in custom_attrs:
f_s_entity['custom_attributes'][key] = custom_attrs[key]
task_entity = create_task(task_type, f_s_entity)
# Create notes.
user = session.query(
"User where username is \"{}\"".format(session.api_user)
).first()
f_s_entity.create_note(shot_description, author=user)
if task_description:
task_entity.create_note(task_description, user)
try:
self.session.commit()
session.commit()
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
self.session._configure_locations()
session.rollback()
session._configure_locations()
six.reraise(tp, value, tb)
# TASKS
tasks = entity_data.get('tasks', [])
existing_tasks = []
tasks_to_create = []
for child in entity['children']:
if child.entity_type.lower() == 'task':
existing_tasks.append(child['name'].lower())
# existing_tasks.append(child['type']['name'])
# creating ui
window = QtWidgets.QWidget()
window.setMinimumSize(1500, 600)
window.setWindowTitle('Sequence Shots to Ftrack')
window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
window.setAttribute(QtCore.Qt.WA_DeleteOnClose)
window.setStyleSheet('background-color: #313131')
for task_name in tasks:
task_type = tasks[task_name]["type"]
if task_name.lower() in existing_tasks:
print("Task {} already exists".format(task_name))
continue
tasks_to_create.append((task_name, task_type))
# Center window in linux
resolution = QtWidgets.QDesktopWidget().screenGeometry()
window.move((resolution.width() / 2) - (window.frameSize().width() / 2),
(resolution.height() / 2) - (window.frameSize().height() / 2))
for task_name, task_type in tasks_to_create:
self.create_task(
name=task_name,
task_type=task_type,
parent=entity
)
try:
self.session.commit()
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
self.session._configure_locations()
six.reraise(tp, value, tb)
# TreeWidget
columns = {
"Sequence name": {
"columnWidth": 100,
"order": 0
},
"Shot name": {
"columnWidth": 100,
"order": 1
},
"Task type": {
"columnWidth": 100,
"order": 2
},
"Start frame": {
"columnWidth": 100,
"order": 3
},
"Clip duration": {
"columnWidth": 100,
"order": 4
},
"Handles": {
"columnWidth": 100,
"order": 5
},
"Shot description": {
"columnWidth": 300,
"order": 6
},
"Task description": {
"columnWidth": 300,
"order": 7
},
}
ordered_column_labels = columns.keys()
for _name, _value in columns.items():
ordered_column_labels.pop(_value["order"])
ordered_column_labels.insert(_value["order"], _name)
# Incoming links.
self.create_links(entity_data, entity)
try:
self.session.commit()
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
self.session._configure_locations()
six.reraise(tp, value, tb)
print(ordered_column_labels)
# Create notes.
user = self.session.query(
"User where username is \"{}\"".format(self.session.api_user)
).first()
if user:
for comment in entity_data.get("comments", []):
entity.create_note(comment, user)
else:
self.log.warning(
"Was not able to query current User {}".format(
self.session.api_user
)
)
try:
self.session.commit()
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
self.session._configure_locations()
six.reraise(tp, value, tb)
tree = FlameTreeWidget(ordered_column_labels, window)
# Import children.
if 'childs' in entity_data:
self.import_to_ftrack(
entity_data['childs'], entity)
# Allow multiple items in tree to be selected
tree.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
def create_links(self, entity_data, entity):
# Clear existing links.
for link in entity.get("incoming_links", []):
self.session.delete(link)
try:
self.session.commit()
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
self.session._configure_locations()
six.reraise(tp, value, tb)
# Set tree column width
for _name, _val in columns.items():
tree.setColumnWidth(
_val["order"],
_val["columnWidth"]
)
# Create new links.
for input in entity_data.get("inputs", []):
input_id = io.find_one({"_id": input})["data"]["ftrackId"]
assetbuild = self.session.get("AssetBuild", input_id)
self.log.debug(
"Creating link from {0} to {1}".format(
assetbuild["name"], entity["name"]
)
)
self.session.create(
"TypedContextLink", {"from": assetbuild, "to": entity}
)
# Prevent weird characters when shrinking tree columns
tree.setTextElideMode(QtCore.Qt.ElideNone)
def get_all_task_types(self, project):
tasks = {}
proj_template = project['project_schema']
temp_task_types = proj_template['_task_type_schema']['types']
# input fields
hierarchy_label = FlameLabel(
'Parents template', 'normal', window)
hierarchy_template = FlameLineEdit(HIERARCHY_TEMPLATE, window)
for type in temp_task_types:
if type['name'] not in tasks:
tasks[type['name']] = type
## Button
select_all_btn = FlameButton('Select All', select_all, window)
ftrack_send_btn = FlameButton('Send to Ftrack', send_to_ftrack, window)
return tasks
## Window Layout
gridbox = QtWidgets.QGridLayout()
gridbox.setMargin(20)
gridbox.setHorizontalSpacing(20)
gridbox.addWidget(hierarchy_label, 0, 0)
gridbox.addWidget(hierarchy_template, 0, 1, 1, 4)
gridbox.addWidget(tree, 1, 0, 5, 5)
gridbox.addWidget(select_all_btn, 6, 3)
gridbox.addWidget(ftrack_send_btn, 6, 4)
def create_task(self, name, task_type, parent):
task = self.session.create('Task', {
'name': name,
'parent': parent
})
# TODO not secured!!! - check if task_type exists
self.log.info(task_type)
self.log.info(self.task_types)
task['type'] = self.task_types[task_type]
window.setLayout(gridbox)
window.show()
try:
self.session.commit()
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
self.session._configure_locations()
six.reraise(tp, value, tb)
timeline_info(selection)
return task
return window
def create_entity(self, name, type, parent):
entity = self.session.create(type, {
'name': name,
'parent': parent
})
try:
self.session.commit()
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
self.session._configure_locations()
six.reraise(tp, value, tb)
return entity
def scope_sequence(selection):
import flame
return any(isinstance(item, flame.PySequence) for item in selection)
def auto_sync_off(self, project):
project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = False
self.log.info("Ftrack autosync swithed off")
def get_media_panel_custom_ui_actions():
return [
{
"name": "OpenPype: Ftrack",
"actions": [
{
"name": "Create Shots",
"isVisible": scope_sequence,
"execute": main_window
}
]
}
try:
self.session.commit()
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
self.session._configure_locations()
six.reraise(tp, value, tb)
def auto_sync_on(self, project):
project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = True
self.log.info("Ftrack autosync swithed on")
try:
self.session.commit()
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
self.session._configure_locations()
six.reraise(tp, value, tb)
]