Merge pull request #2381 from pypeclub/feature/OP-1915_flame-ftrack-direct-link

This commit is contained in:
Jakub Ježek 2021-12-07 17:11:38 +01:00 committed by GitHub
commit c0c3fceae3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 1524 additions and 4 deletions

View file

@ -2,6 +2,7 @@ import os
import json
import tempfile
import contextlib
import socket
from openpype.lib import (
PreLaunchHook, get_openpype_username)
from openpype.hosts import flame as opflame
@ -32,6 +33,7 @@ class FlamePrelaunch(PreLaunchHook):
"""Hook entry method."""
project_doc = self.data["project_doc"]
user_name = get_openpype_username()
hostname = socket.gethostname() # not returning wiretap host name
self.log.debug("Collected user \"{}\"".format(user_name))
self.log.info(pformat(project_doc))
@ -53,11 +55,12 @@ class FlamePrelaunch(PreLaunchHook):
"FieldDominance": "PROGRESSIVE"
}
data_to_script = {
# from settings
"host_name": "localhost",
"volume_name": "stonefs",
"group_name": "staff",
"host_name": os.getenv("FLAME_WIRETAP_HOSTNAME") or hostname,
"volume_name": os.getenv("FLAME_WIRETAP_VOLUME"),
"group_name": os.getenv("FLAME_WIRETAP_GROUP"),
"color_policy": "ACES 1.1",
# from project

View file

@ -0,0 +1,58 @@
<?xml version="1.0"?>
<preset version="9">
<type>sequence</type>
<comment>Creates a 8-bit Jpeg file per segment. </comment>
<sequence>
<fileType>NONE</fileType>
<namePattern></namePattern>
<composition>&lt;name&gt;</composition>
<includeVideo>True</includeVideo>
<exportVideo>True</exportVideo>
<videoMedia>
<mediaFileType>image</mediaFileType>
<commit>FX</commit>
<flatten>NoChange</flatten>
<exportHandles>False</exportHandles>
<nbHandles>10</nbHandles>
</videoMedia>
<includeAudio>True</includeAudio>
<exportAudio>False</exportAudio>
<audioMedia>
<mediaFileType>audio</mediaFileType>
<commit>FX</commit>
<flatten>FlattenTracks</flatten>
<exportHandles>True</exportHandles>
<nbHandles>10</nbHandles>
</audioMedia>
</sequence>
<video>
<fileType>Jpeg</fileType>
<codec>923688</codec>
<codecProfile></codecProfile>
<namePattern>&lt;segment name&gt;</namePattern>
<compressionQuality>100</compressionQuality>
<transferCharacteristic>2</transferCharacteristic>
<colorimetricSpecification>4</colorimetricSpecification>
<includeAlpha>False</includeAlpha>
<overwriteWithVersions>False</overwriteWithVersions>
<posterFrame>True</posterFrame>
<useFrameAsPoster>1</useFrameAsPoster>
<resize>
<resizeType>fit</resizeType>
<resizeFilter>lanczos</resizeFilter>
<width>1920</width>
<height>1080</height>
<bitsPerChannel>8</bitsPerChannel>
<numChannels>3</numChannels>
<floatingPoint>False</floatingPoint>
<bigEndian>True</bigEndian>
<pixelRatio>1</pixelRatio>
<scanFormat>P</scanFormat>
</resize>
</video>
<name>
<framePadding>4</framePadding>
<startFrame>1</startFrame>
<frameIndex>2</frameIndex>
</name>
</preset>

View file

@ -0,0 +1,72 @@
<?xml version="1.0"?>
<preset version="10">
<type>sequence</type>
<comment>Create MOV H264 files per segment with thumbnail</comment>
<sequence>
<fileType>NONE</fileType>
<namePattern></namePattern>
<composition>&lt;name&gt;</composition>
<includeVideo>True</includeVideo>
<exportVideo>True</exportVideo>
<videoMedia>
<mediaFileType>movie</mediaFileType>
<commit>FX</commit>
<flatten>FlattenTracks</flatten>
<exportHandles>True</exportHandles>
<nbHandles>5</nbHandles>
</videoMedia>
<includeAudio>True</includeAudio>
<exportAudio>False</exportAudio>
<audioMedia>
<mediaFileType>audio</mediaFileType>
<commit>Original</commit>
<flatten>NoChange</flatten>
<exportHandles>True</exportHandles>
<nbHandles>5</nbHandles>
</audioMedia>
</sequence>
<movie>
<fileType>QuickTime</fileType>
<namePattern>&lt;segment name&gt;</namePattern>
<yuvHeadroom>0</yuvHeadroom>
<yuvColourSpace>PCS_709</yuvColourSpace>
<operationalPattern>None</operationalPattern>
<companyName>Autodesk</companyName>
<productName>Flame</productName>
<versionName>2021</versionName>
</movie>
<video>
<fileType>QuickTime</fileType>
<codec>33622016</codec>
<codecProfile>
<rootPath>/opt/Autodesk/mediaconverter/</rootPath>
<targetVersion>2021</targetVersion>
<pathSuffix>/profiles/.33622016/HDTV_720p_8Mbits.cdxprof</pathSuffix>
</codecProfile>
<namePattern>&lt;segment name&gt;_&lt;video codec&gt;</namePattern>
<compressionQuality>50</compressionQuality>
<transferCharacteristic>2</transferCharacteristic>
<colorimetricSpecification>4</colorimetricSpecification>
<includeAlpha>False</includeAlpha>
<overwriteWithVersions>False</overwriteWithVersions>
<posterFrame>False</posterFrame>
<useFrameAsPoster>1</useFrameAsPoster>
<resize>
<resizeType>fit</resizeType>
<resizeFilter>gaussian</resizeFilter>
<width>1920</width>
<height>1080</height>
<bitsPerChannel>8</bitsPerChannel>
<numChannels>3</numChannels>
<floatingPoint>False</floatingPoint>
<bigEndian>True</bigEndian>
<pixelRatio>1</pixelRatio>
<scanFormat>P</scanFormat>
</resize>
</video>
<name>
<framePadding>4</framePadding>
<startFrame>1</startFrame>
<frameIndex>2</frameIndex>
</name>
</preset>

View file

@ -0,0 +1,162 @@
import os
import io
import ConfigParser as CP
from xml.etree import ElementTree as ET
from contextlib import contextmanager
PLUGIN_DIR = os.path.dirname(os.path.dirname(__file__))
EXPORT_PRESETS_DIR = os.path.join(PLUGIN_DIR, "export_preset")
CONFIG_DIR = os.path.join(os.path.expanduser(
"~/.openpype"), "openpype_flame_to_ftrack")
@contextmanager
def make_temp_dir():
import tempfile
try:
dirpath = tempfile.mkdtemp()
yield dirpath
except IOError as _error:
raise IOError("Not able to create temp dir file: {}".format(_error))
finally:
pass
@contextmanager
def get_config(section=None):
cfg_file_path = os.path.join(CONFIG_DIR, "settings.ini")
# create config dir
if not os.path.exists(CONFIG_DIR):
print("making dirs at: `{}`".format(CONFIG_DIR))
os.makedirs(CONFIG_DIR, mode=0o777)
# write default data to settings.ini
if not os.path.exists(cfg_file_path):
default_cfg = cfg_default()
config = CP.RawConfigParser()
config.readfp(io.BytesIO(default_cfg))
with open(cfg_file_path, 'wb') as cfg_file:
config.write(cfg_file)
try:
config = CP.RawConfigParser()
config.read(cfg_file_path)
if section:
_cfg_data = {
k: v
for s in config.sections()
for k, v in config.items(s)
if s == section
}
else:
_cfg_data = {s: dict(config.items(s)) for s in config.sections()}
yield _cfg_data
except IOError as _error:
raise IOError('Not able to read settings.ini file: {}'.format(_error))
finally:
pass
def set_config(cfg_data, section=None):
cfg_file_path = os.path.join(CONFIG_DIR, "settings.ini")
config = CP.RawConfigParser()
config.read(cfg_file_path)
try:
if not section:
for section in cfg_data:
for key, value in cfg_data[section].items():
config.set(section, key, value)
else:
for key, value in cfg_data.items():
config.set(section, key, value)
with open(cfg_file_path, 'wb') as cfg_file:
config.write(cfg_file)
except IOError as _error:
raise IOError('Not able to write settings.ini file: {}'.format(_error))
def cfg_default():
return """
[main]
workfile_start_frame = 1001
shot_handles = 0
shot_name_template = {sequence}_{shot}
hierarchy_template = shots[Folder]/{sequence}[Sequence]
create_task_type = Compositing
"""
def configure_preset(file_path, data):
split_fp = os.path.splitext(file_path)
new_file_path = split_fp[0] + "_tmp" + split_fp[-1]
with open(file_path, "r") as datafile:
tree = ET.parse(datafile)
for key, value in data.items():
for element in tree.findall(".//{}".format(key)):
print(element)
element.text = str(value)
tree.write(new_file_path)
return new_file_path
def export_thumbnail(sequence, tempdir_path, data):
import flame
export_preset = os.path.join(
EXPORT_PRESETS_DIR,
"openpype_seg_thumbnails_jpg.xml"
)
new_path = configure_preset(export_preset, data)
poster_frame_exporter = flame.PyExporter()
poster_frame_exporter.foreground = True
poster_frame_exporter.export(sequence, new_path, tempdir_path)
def export_video(sequence, tempdir_path, data):
import flame
export_preset = os.path.join(
EXPORT_PRESETS_DIR,
"openpype_seg_video_h264.xml"
)
new_path = configure_preset(export_preset, data)
poster_frame_exporter = flame.PyExporter()
poster_frame_exporter.foreground = True
poster_frame_exporter.export(sequence, new_path, tempdir_path)
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 tc_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(tc_to_frames(timecode, start='00:00:00:00')))
return frames

View file

@ -0,0 +1,448 @@
import os
import sys
import six
import re
import json
import app_utils
# Fill following constants or set them via environment variable
FTRACK_MODULE_PATH = None
FTRACK_API_KEY = None
FTRACK_API_USER = None
FTRACK_SERVER = None
def import_ftrack_api():
try:
import ftrack_api
return ftrack_api
except ImportError:
import sys
ftrk_m_p = FTRACK_MODULE_PATH or os.getenv("FTRACK_MODULE_PATH")
sys.path.append(ftrk_m_p)
import ftrack_api
return ftrack_api
def get_ftrack_session():
import os
ftrack_api = import_ftrack_api()
# fill your own credentials
url = FTRACK_SERVER or os.getenv("FTRACK_SERVER") or ""
user = FTRACK_API_USER or os.getenv("FTRACK_API_USER") or ""
api = FTRACK_API_KEY or os.getenv("FTRACK_API_KEY") or ""
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
try:
return ftrack_api.Session(
server_url=url,
api_user=user,
api_key=api
)
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
def get_project_task_types(project_entity):
tasks = {}
proj_template = project_entity['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
class FtrackComponentCreator:
default_location = "ftrack.server"
ftrack_locations = {}
thumbnails = []
videos = []
temp_dir = None
def __init__(self, session):
self.session = session
self._get_ftrack_location()
def generate_temp_data(self, selection, change_preset_data):
with app_utils.make_temp_dir() as tempdir_path:
for seq in selection:
app_utils.export_thumbnail(
seq, tempdir_path, change_preset_data)
app_utils.export_video(seq, tempdir_path, change_preset_data)
return tempdir_path
def collect_generated_data(self, tempdir_path):
temp_files = os.listdir(tempdir_path)
self.thumbnails = [f for f in temp_files if "jpg" in f]
self.videos = [f for f in temp_files if "mov" in f]
self.temp_dir = tempdir_path
def get_thumb_path(self, shot_name):
# get component files
thumb_f = next((f for f in self.thumbnails if shot_name in f), None)
return os.path.join(self.temp_dir, thumb_f)
def get_video_path(self, shot_name):
# get component files
video_f = next((f for f in self.videos if shot_name in f), None)
return os.path.join(self.temp_dir, video_f)
def close(self):
self.ftrack_locations = {}
self.session = None
def create_comonent(self, shot_entity, data, assetversion_entity=None):
self.shot_entity = shot_entity
location = self._get_ftrack_location()
file_path = data["file_path"]
# get extension
file = os.path.basename(file_path)
_n, ext = os.path.splitext(file)
name = "ftrackreview-mp4" if "mov" in ext else "thumbnail"
component_data = {
"name": name,
"file_path": file_path,
"file_type": ext,
"location": location
}
if name == "ftrackreview-mp4":
duration = data["duration"]
handles = data["handles"]
fps = data["fps"]
component_data["metadata"] = {
'ftr_meta': json.dumps({
'frameIn': int(0),
'frameOut': int(duration + (handles * 2)),
'frameRate': float(fps)
})
}
if not assetversion_entity:
# get assettype entity from session
assettype_entity = self._get_assettype({"short": "reference"})
# get or create asset entity from session
asset_entity = self._get_asset({
"name": "plateReference",
"type": assettype_entity,
"parent": self.shot_entity
})
# get or create assetversion entity from session
assetversion_entity = self._get_assetversion({
"version": 0,
"asset": asset_entity
})
# get or create component entity
self._set_component(component_data, {
"name": name,
"version": assetversion_entity,
})
return assetversion_entity
def _overwrite_members(self, entity, data):
origin_location = self._get_ftrack_location("ftrack.origin")
location = data.pop("location")
self._remove_component_from_location(entity, location)
entity["file_type"] = data["file_type"]
try:
origin_location.add_component(
entity, data["file_path"]
)
# Add components to location.
location.add_component(
entity, origin_location, recursive=True)
except Exception as __e:
print("Error: {}".format(__e))
self._remove_component_from_location(entity, origin_location)
origin_location.add_component(
entity, data["file_path"]
)
# Add components to location.
location.add_component(
entity, origin_location, recursive=True)
def _remove_component_from_location(self, entity, location):
print(location)
# Removing existing members from location
components = list(entity.get("members", []))
components += [entity]
for component in components:
for loc in component.get("component_locations", []):
if location["id"] == loc["location_id"]:
print("<< Removing component: {}".format(component))
location.remove_component(
component, recursive=False
)
# Deleting existing members on component entity
for member in entity.get("members", []):
self.session.delete(member)
print("<< Deleting member: {}".format(member))
del(member)
self._commit()
# Reset members in memory
if "members" in entity.keys():
entity["members"] = []
def _get_assettype(self, data):
return self.session.query(
self._query("AssetType", data)).first()
def _set_component(self, comp_data, base_data):
component_metadata = comp_data.pop("metadata", {})
component_entity = self.session.query(
self._query("Component", base_data)
).first()
if component_entity:
# overwrite existing members in component enity
# - get data for member from `ftrack.origin` location
self._overwrite_members(component_entity, comp_data)
# Adding metadata
existing_component_metadata = component_entity["metadata"]
existing_component_metadata.update(component_metadata)
component_entity["metadata"] = existing_component_metadata
return
assetversion_entity = base_data["version"]
location = comp_data.pop("location")
component_entity = assetversion_entity.create_component(
comp_data["file_path"],
data=comp_data,
location=location
)
# Adding metadata
existing_component_metadata = component_entity["metadata"]
existing_component_metadata.update(component_metadata)
component_entity["metadata"] = existing_component_metadata
if comp_data["name"] == "thumbnail":
self.shot_entity["thumbnail_id"] = component_entity["id"]
assetversion_entity["thumbnail_id"] = component_entity["id"]
self._commit()
def _get_asset(self, data):
# first find already created
asset_entity = self.session.query(
self._query("Asset", data)
).first()
if asset_entity:
return asset_entity
asset_entity = self.session.create("Asset", data)
# _commit if created
self._commit()
return asset_entity
def _get_assetversion(self, data):
assetversion_entity = self.session.query(
self._query("AssetVersion", data)
).first()
if assetversion_entity:
return assetversion_entity
assetversion_entity = self.session.create("AssetVersion", data)
# _commit if created
self._commit()
return assetversion_entity
def _commit(self):
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 _get_ftrack_location(self, name=None):
name = name or self.default_location
if name in self.ftrack_locations:
return self.ftrack_locations[name]
location = self.session.query(
'Location where name is "{}"'.format(name)
).one()
self.ftrack_locations[name] = location
return location
def _query(self, entitytype, data):
""" Generate a query expression from data supplied.
If a value is not a string, we'll add the id of the entity to the
query.
Args:
entitytype (str): The type of entity to query.
data (dict): The data to identify the entity.
exclusions (list): All keys to exclude from the query.
Returns:
str: String query to use with "session.query"
"""
queries = []
if sys.version_info[0] < 3:
for key, value in data.items():
if not isinstance(value, (str, int)):
print("value: {}".format(value))
if "id" in value.keys():
queries.append(
"{0}.id is \"{1}\"".format(key, value["id"])
)
else:
queries.append("{0} is \"{1}\"".format(key, value))
else:
for key, value in data.items():
if not isinstance(value, (str, int)):
print("value: {}".format(value))
if "id" in value.keys():
queries.append(
"{0}.id is \"{1}\"".format(key, value["id"])
)
else:
queries.append("{0} is \"{1}\"".format(key, value))
query = (
"select id from " + entitytype + " where " + " and ".join(queries)
)
print(query)
return query
class FtrackEntityOperator:
def __init__(self, session, project_entity):
self.session = session
self.project_entity = project_entity
def commit(self):
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 create_ftrack_entity(self, session, type, name, parent=None):
parent = parent or self.project_entity
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(self, session, type, name, parent):
query = '{} where name is "{}" and project_id is "{}"'.format(
type, name, self.project_entity["id"])
try:
entity = session.query(query).one()
except Exception:
entity = None
# if entity doesnt exist then create one
if not entity:
entity = self.create_ftrack_entity(
session,
type,
name,
parent
)
return entity
def create_parents(self, 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 create_task(self, task_type, task_types, 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.pop()
task = self.session.create('Task', {
"name": task_type.lower(),
"parent": parent
})
task["type"] = task_types[task_type]
return task

View file

@ -0,0 +1,524 @@
from PySide2 import QtWidgets, QtCore
import uiwidgets
import app_utils
import ftrack_lib
def clear_inner_modules():
import sys
if "ftrack_lib" in sys.modules.keys():
del sys.modules["ftrack_lib"]
print("Ftrack Lib module removed from sys.modules")
if "app_utils" in sys.modules.keys():
del sys.modules["app_utils"]
print("app_utils module removed from sys.modules")
if "uiwidgets" in sys.modules.keys():
del sys.modules["uiwidgets"]
print("uiwidgets module removed from sys.modules")
class MainWindow(QtWidgets.QWidget):
def __init__(self, klass, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.panel_class = klass
def closeEvent(self, event):
# clear all temp data
print("Removing temp data")
self.panel_class.clear_temp_data()
self.panel_class.close()
clear_inner_modules()
# now the panel can be closed
event.accept()
class FlameToFtrackPanel(object):
session = None
temp_data_dir = None
processed_components = []
project_entity = None
task_types = {}
all_task_types = {}
# TreeWidget
columns = {
"Sequence name": {
"columnWidth": 200,
"order": 0
},
"Shot name": {
"columnWidth": 200,
"order": 1
},
"Clip duration": {
"columnWidth": 100,
"order": 2
},
"Shot description": {
"columnWidth": 500,
"order": 3
},
"Task description": {
"columnWidth": 500,
"order": 4
},
}
def __init__(self, selection):
print(selection)
self.session = ftrack_lib.get_ftrack_session()
self.selection = selection
self.window = MainWindow(self)
# creating ui
self.window.setMinimumSize(1500, 600)
self.window.setWindowTitle('Sequence Shots to Ftrack')
self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
self.window.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.window.setFocusPolicy(QtCore.Qt.StrongFocus)
self.window.setStyleSheet('background-color: #313131')
self._create_project_widget()
self._create_tree_widget()
self._set_sequence_params()
self._generate_widgets()
self._generate_layouts()
self._timeline_info()
self._fix_resolution()
self.window.show()
def _generate_widgets(self):
with app_utils.get_config("main") as cfg_data:
cfg_d = cfg_data
self._create_task_type_widget(cfg_d)
# input fields
self.shot_name_label = uiwidgets.FlameLabel(
'Shot name template', 'normal', self.window)
self.shot_name_template_input = uiwidgets.FlameLineEdit(
cfg_d["shot_name_template"], self.window)
self.hierarchy_label = uiwidgets.FlameLabel(
'Parents template', 'normal', self.window)
self.hierarchy_template_input = uiwidgets.FlameLineEdit(
cfg_d["hierarchy_template"], self.window)
self.start_frame_label = uiwidgets.FlameLabel(
'Workfile start frame', 'normal', self.window)
self.start_frame_input = uiwidgets.FlameLineEdit(
cfg_d["workfile_start_frame"], self.window)
self.handles_label = uiwidgets.FlameLabel(
'Shot handles', 'normal', self.window)
self.handles_input = uiwidgets.FlameLineEdit(
cfg_d["shot_handles"], self.window)
self.width_label = uiwidgets.FlameLabel(
'Sequence width', 'normal', self.window)
self.width_input = uiwidgets.FlameLineEdit(
str(self.seq_width), self.window)
self.height_label = uiwidgets.FlameLabel(
'Sequence height', 'normal', self.window)
self.height_input = uiwidgets.FlameLineEdit(
str(self.seq_height), self.window)
self.pixel_aspect_label = uiwidgets.FlameLabel(
'Pixel aspect ratio', 'normal', self.window)
self.pixel_aspect_input = uiwidgets.FlameLineEdit(
str(1.00), self.window)
self.fps_label = uiwidgets.FlameLabel(
'Frame rate', 'normal', self.window)
self.fps_input = uiwidgets.FlameLineEdit(
str(self.fps), self.window)
# Button
self.select_all_btn = uiwidgets.FlameButton(
'Select All', self.select_all, self.window)
self.remove_temp_data_btn = uiwidgets.FlameButton(
'Remove temp data', self.clear_temp_data, self.window)
self.ftrack_send_btn = uiwidgets.FlameButton(
'Send to Ftrack', self._send_to_ftrack, self.window)
def _generate_layouts(self):
# left props
v_shift = 0
prop_layout_l = QtWidgets.QGridLayout()
prop_layout_l.setHorizontalSpacing(30)
if self.project_selector_enabled:
prop_layout_l.addWidget(self.project_select_label, v_shift, 0)
prop_layout_l.addWidget(self.project_select_input, v_shift, 1)
v_shift += 1
prop_layout_l.addWidget(self.shot_name_label, (v_shift + 0), 0)
prop_layout_l.addWidget(
self.shot_name_template_input, (v_shift + 0), 1)
prop_layout_l.addWidget(self.hierarchy_label, (v_shift + 1), 0)
prop_layout_l.addWidget(
self.hierarchy_template_input, (v_shift + 1), 1)
prop_layout_l.addWidget(self.start_frame_label, (v_shift + 2), 0)
prop_layout_l.addWidget(self.start_frame_input, (v_shift + 2), 1)
prop_layout_l.addWidget(self.handles_label, (v_shift + 3), 0)
prop_layout_l.addWidget(self.handles_input, (v_shift + 3), 1)
prop_layout_l.addWidget(self.task_type_label, (v_shift + 4), 0)
prop_layout_l.addWidget(
self.task_type_input, (v_shift + 4), 1)
# right props
prop_widget_r = QtWidgets.QWidget(self.window)
prop_layout_r = QtWidgets.QGridLayout(prop_widget_r)
prop_layout_r.setHorizontalSpacing(30)
prop_layout_r.setAlignment(
QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
prop_layout_r.setContentsMargins(0, 0, 0, 0)
prop_layout_r.addWidget(self.width_label, 1, 0)
prop_layout_r.addWidget(self.width_input, 1, 1)
prop_layout_r.addWidget(self.height_label, 2, 0)
prop_layout_r.addWidget(self.height_input, 2, 1)
prop_layout_r.addWidget(self.pixel_aspect_label, 3, 0)
prop_layout_r.addWidget(self.pixel_aspect_input, 3, 1)
prop_layout_r.addWidget(self.fps_label, 4, 0)
prop_layout_r.addWidget(self.fps_input, 4, 1)
# prop layout
prop_main_layout = QtWidgets.QHBoxLayout()
prop_main_layout.addLayout(prop_layout_l, 1)
prop_main_layout.addSpacing(20)
prop_main_layout.addWidget(prop_widget_r, 1)
# buttons layout
hbox = QtWidgets.QHBoxLayout()
hbox.addWidget(self.remove_temp_data_btn)
hbox.addWidget(self.select_all_btn)
hbox.addWidget(self.ftrack_send_btn)
# put all layouts together
main_frame = QtWidgets.QVBoxLayout(self.window)
main_frame.setMargin(20)
main_frame.addLayout(prop_main_layout)
main_frame.addWidget(self.tree)
main_frame.addLayout(hbox)
def _set_sequence_params(self):
for select in self.selection:
self.seq_height = select.height
self.seq_width = select.width
self.fps = float(str(select.frame_rate)[:-4])
break
def _create_task_type_widget(self, cfg_d):
print(self.project_entity)
self.task_types = ftrack_lib.get_project_task_types(
self.project_entity)
self.task_type_label = uiwidgets.FlameLabel(
'Create Task (type)', 'normal', self.window)
self.task_type_input = uiwidgets.FlamePushButtonMenu(
cfg_d["create_task_type"], self.task_types.keys(), self.window)
def _create_project_widget(self):
import flame
# get project name from flame current project
self.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(self.project_name)
# globally used variables
self.project_entity = self.session.query(query).first()
self.project_selector_enabled = bool(not self.project_entity)
if self.project_selector_enabled:
self.all_projects = self.session.query(
"Project where status is active").all()
self.project_entity = self.all_projects[0]
project_names = [p["full_name"] for p in self.all_projects]
self.all_task_types = {
p["full_name"]: ftrack_lib.get_project_task_types(p).keys()
for p in self.all_projects
}
self.project_select_label = uiwidgets.FlameLabel(
'Select Ftrack project', 'normal', self.window)
self.project_select_input = uiwidgets.FlamePushButtonMenu(
self.project_entity["full_name"], project_names, self.window)
self.project_select_input.selection_changed.connect(
self._on_project_changed)
def _create_tree_widget(self):
ordered_column_labels = self.columns.keys()
for _name, _value in self.columns.items():
ordered_column_labels.pop(_value["order"])
ordered_column_labels.insert(_value["order"], _name)
self.tree = uiwidgets.FlameTreeWidget(
ordered_column_labels, self.window)
# Allow multiple items in tree to be selected
self.tree.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
# Set tree column width
for _name, _val in self.columns.items():
self.tree.setColumnWidth(
_val["order"],
_val["columnWidth"]
)
# Prevent weird characters when shrinking tree columns
self.tree.setTextElideMode(QtCore.Qt.ElideNone)
def _resolve_project_entity(self):
if self.project_selector_enabled:
selected_project_name = self.project_select_input.text()
self.project_entity = next(
(p for p in self.all_projects
if p["full_name"] in selected_project_name),
None
)
def _save_ui_state_to_cfg(self):
_cfg_data_back = {
"shot_name_template": self.shot_name_template_input.text(),
"workfile_start_frame": self.start_frame_input.text(),
"shot_handles": self.handles_input.text(),
"hierarchy_template": self.hierarchy_template_input.text(),
"create_task_type": self.task_type_input.text()
}
# add cfg data back to settings.ini
app_utils.set_config(_cfg_data_back, "main")
def _send_to_ftrack(self):
# resolve active project and add it to self.project_entity
self._resolve_project_entity()
self._save_ui_state_to_cfg()
# get hanldes from gui input
handles = self.handles_input.text()
# get frame start from gui input
frame_start = int(self.start_frame_input.text())
# get task type from gui input
task_type = self.task_type_input.text()
# get resolution from gui inputs
fps = self.fps_input.text()
entity_operator = ftrack_lib.FtrackEntityOperator(
self.session, self.project_entity)
component_creator = ftrack_lib.FtrackComponentCreator(self.session)
if not self.temp_data_dir:
self.window.hide()
self.temp_data_dir = component_creator.generate_temp_data(
self.selection,
{
"nbHandles": handles
}
)
self.window.show()
# collect generated files to list data for farther use
component_creator.collect_generated_data(self.temp_data_dir)
# Get all selected items from treewidget
for item in self.tree.selectedItems():
# frame ranges
frame_duration = int(item.text(2))
frame_end = frame_start + frame_duration
# description
shot_description = item.text(3)
task_description = item.text(4)
# other
sequence_name = item.text(0)
shot_name = item.text(1)
thumb_fp = component_creator.get_thumb_path(shot_name)
video_fp = component_creator.get_video_path(shot_name)
print("processed comps: {}".format(self.processed_components))
print("processed thumb_fp: {}".format(thumb_fp))
processed = False
if thumb_fp not in self.processed_components:
self.processed_components.append(thumb_fp)
else:
processed = True
print("processed: {}".format(processed))
# populate full shot info
shot_attributes = {
"sequence": sequence_name,
"shot": shot_name,
"task": task_type
}
# format shot name template
_shot_name = self.shot_name_template_input.text().format(
**shot_attributes)
# format hierarchy template
_hierarchy_text = self.hierarchy_template_input.text().format(
**shot_attributes)
print(_hierarchy_text)
# solve parents
parents = entity_operator.create_parents(_hierarchy_text)
print(parents)
# obtain shot parents entities
_parent = None
for _name, _type in parents:
p_entity = entity_operator.get_ftrack_entity(
self.session,
_type,
_name,
_parent
)
print(p_entity)
_parent = p_entity
# obtain shot ftrack entity
f_s_entity = entity_operator.get_ftrack_entity(
self.session,
"Shot",
_shot_name,
_parent
)
print("Shot entity is: {}".format(f_s_entity))
if not processed:
# first create thumbnail and get version entity
assetversion_entity = component_creator.create_comonent(
f_s_entity, {
"file_path": thumb_fp
}
)
# secondly add video to version entity
component_creator.create_comonent(
f_s_entity, {
"file_path": video_fp,
"duration": frame_duration,
"handles": int(handles),
"fps": float(fps)
}, assetversion_entity
)
# create custom attributtes
custom_attrs = {
"frameStart": frame_start,
"frameEnd": frame_end,
"handleStart": int(handles),
"handleEnd": int(handles),
"resolutionWidth": int(self.width_input.text()),
"resolutionHeight": int(self.height_input.text()),
"pixelAspect": float(self.pixel_aspect_input.text()),
"fps": float(fps)
}
# update custom attributes on shot entity
for key in custom_attrs:
f_s_entity['custom_attributes'][key] = custom_attrs[key]
task_entity = entity_operator.create_task(
task_type, self.task_types, f_s_entity)
# Create notes.
user = self.session.query(
"User where username is \"{}\"".format(self.session.api_user)
).first()
f_s_entity.create_note(shot_description, author=user)
if task_description:
task_entity.create_note(task_description, user)
entity_operator.commit()
component_creator.close()
def _fix_resolution(self):
# Center window in linux
resolution = QtWidgets.QDesktopWidget().screenGeometry()
self.window.move(
(resolution.width() / 2) - (self.window.frameSize().width() / 2),
(resolution.height() / 2) - (self.window.frameSize().height() / 2))
def _on_project_changed(self):
task_types = self.all_task_types[self.project_name]
self.task_type_input.set_menu_options(task_types)
def _timeline_info(self):
# identificar as informacoes dos segmentos na timeline
for sequence in self.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.attributes)
if str(segment.name)[1:-1] == "":
continue
# get clip frame duration
record_duration = str(segment.record_duration)[1:-1]
clip_duration = app_utils.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(self.tree, [
str(sequence.name)[1:-1], # seq
str(segment.name)[1:-1], # shot
str(clip_duration), # clip duration
shot_description, # shot description
str(segment.comment)[1:-1] # task description
]).setFlags(
QtCore.Qt.ItemIsEditable
| QtCore.Qt.ItemIsEnabled
| QtCore.Qt.ItemIsSelectable
)
# Select top item in tree
self.tree.setCurrentItem(self.tree.topLevelItem(0))
def select_all(self, ):
self.tree.selectAll()
def clear_temp_data(self):
import shutil
self.processed_components = []
if self.temp_data_dir:
shutil.rmtree(self.temp_data_dir)
self.temp_data_dir = None
print("All Temp data were destroied ...")
def close(self):
self._save_ui_state_to_cfg()
self.session.close()

View file

@ -0,0 +1,212 @@
from PySide2 import QtWidgets, QtCore
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)
"""
def __init__(self, label_name, label_type, parent_window, *args, **kwargs):
super(FlameLabel, self).__init__(*args, **kwargs)
self.setText(label_name)
self.setParent(parent_window)
self.setMinimumSize(130, 28)
self.setMaximumHeight(28)
self.setFocusPolicy(QtCore.Qt.NoFocus)
# Set label stylesheet based on label_type
if label_type == 'normal':
self.setStyleSheet(
'QLabel {color: #9a9a9a; border-bottom: 1px inset #282828; font: 14px "Discreet"}' # noqa
'QLabel:disabled {color: #6a6a6a}'
)
elif label_type == 'background':
self.setAlignment(QtCore.Qt.AlignCenter)
self.setStyleSheet(
'color: #9a9a9a; background-color: #393939; font: 14px "Discreet"' # noqa
)
elif label_type == 'outline':
self.setAlignment(QtCore.Qt.AlignCenter)
self.setStyleSheet(
'color: #9a9a9a; background-color: #212121; border: 1px solid #404040; font: 14px "Discreet"' # noqa
)
class FlameLineEdit(QtWidgets.QLineEdit):
"""
Custom Qt Flame Line Edit Widget
Main window should include this:
window.setFocusPolicy(QtCore.Qt.StrongFocus)
To use:
line_edit = FlameLineEdit('Some text here', window)
"""
def __init__(self, text, parent_window, *args, **kwargs):
super(FlameLineEdit, self).__init__(*args, **kwargs)
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"}' # noqa
'QLineEdit:focus {background-color: #474e58}' # noqa
'QLineEdit:disabled {color: #6a6a6a; background-color: #373737}'
)
class FlameTreeWidget(QtWidgets.QTreeWidget):
"""
Custom Qt Flame Tree Widget
To use:
tree_headers = ['Header1', 'Header2', 'Header3', 'Header4']
tree = FlameTreeWidget(tree_headers, window)
"""
def __init__(self, tree_headers, parent_window, *args, **kwargs):
super(FlameTreeWidget, self).__init__(*args, **kwargs)
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"}' # noqa
'QTreeWidget::item:selected {color: #d9d9d9; background-color: #474747; border: 1px solid #111111}' # noqa
'QHeaderView {color: #9a9a9a; background-color: #393939; font: 14px "Discreet"}' # noqa
'QTreeWidget::item:selected {selection-background-color: #111111}'
'QMenu {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' # noqa
'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"}' # noqa
'QPushButton:pressed {color: #d9d9d9; background-color: #4f4f4f; border-top: 1px inset #666666; font: italic}' # noqa
'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}' # noqa
)
class FlamePushButton(QtWidgets.QPushButton):
"""
Custom Qt Flame Push Button Widget
To use:
pushbutton = FlamePushButton(' Button Name', True_or_False, window)
"""
def __init__(self, button_name, button_checked, parent_window,
*args, **kwargs):
super(FlamePushButton, self).__init__(*args, **kwargs)
self.setText(button_name)
self.setParent(parent_window)
self.setCheckable(True)
self.setChecked(button_checked)
self.setMinimumSize(155, 28)
self.setMaximumSize(155, 28)
self.setFocusPolicy(QtCore.Qt.NoFocus)
self.setStyleSheet(
'QPushButton {color: #9a9a9a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #424142, stop: .94 #2e3b48); text-align: left; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' # noqa
'QPushButton:checked {color: #d9d9d9; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #4f4f4f, stop: .94 #5a7fb4); font: italic; border: 1px inset black; border-bottom: 1px inset #404040; border-right: 1px inset #404040}' # noqa
'QPushButton:disabled {color: #6a6a6a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #383838, stop: .94 #353535); font: light; border-top: 1px solid #575757; border-bottom: 1px solid #242424; border-right: 1px solid #353535; border-left: 1px solid #353535}' # noqa
'QToolTip {color: black; background-color: #ffffde; border: black solid 1px}' # noqa
)
class FlamePushButtonMenu(QtWidgets.QPushButton):
"""
Custom Qt Flame Menu Push Button Widget
To use:
push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4']
menu_push_button = FlamePushButtonMenu('push_button_name',
push_button_menu_options, window)
or
push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4']
menu_push_button = FlamePushButtonMenu(push_button_menu_options[0],
push_button_menu_options, window)
"""
selection_changed = QtCore.Signal(str)
def __init__(self, button_name, menu_options, parent_window,
*args, **kwargs):
super(FlamePushButtonMenu, self).__init__(*args, **kwargs)
self.setParent(parent_window)
self.setMinimumHeight(28)
self.setMinimumWidth(110)
self.setFocusPolicy(QtCore.Qt.NoFocus)
self.setStyleSheet(
'QPushButton {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' # noqa
'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}' # noqa
)
pushbutton_menu = QtWidgets.QMenu(parent_window)
pushbutton_menu.setFocusPolicy(QtCore.Qt.NoFocus)
pushbutton_menu.setStyleSheet(
'QMenu {color: #9a9a9a; background-color:#24303d; font: 14px "Discreet"}' # noqa
'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}'
)
self._pushbutton_menu = pushbutton_menu
self.setMenu(pushbutton_menu)
self.set_menu_options(menu_options, button_name)
def set_menu_options(self, menu_options, current_option=None):
self._pushbutton_menu.clear()
current_option = current_option or menu_options[0]
for option in menu_options:
action = self._pushbutton_menu.addAction(option)
action.triggered.connect(self._on_action_trigger)
if current_option is not None:
self.setText(current_option)
def _on_action_trigger(self):
action = self.sender()
self.setText(action.text())
self.selection_changed.emit(action.text())

View file

@ -0,0 +1,38 @@
from __future__ import print_function
import os
import sys
SCRIPT_DIR = os.path.dirname(__file__)
PACKAGE_DIR = os.path.join(SCRIPT_DIR, "modules")
sys.path.append(PACKAGE_DIR)
def flame_panel_executor(selection):
if "panel_app" in sys.modules.keys():
print("panel_app module is already loaded")
del sys.modules["panel_app"]
print("panel_app module removed from sys.modules")
import panel_app
panel_app.FlameToFtrackPanel(selection)
def scope_sequence(selection):
import flame
return any(isinstance(item, flame.PySequence) for item in selection)
def get_media_panel_custom_ui_actions():
return [
{
"name": "OpenPype: Ftrack",
"actions": [
{
"name": "Create Shots",
"isVisible": scope_sequence,
"execute": flame_panel_executor
}
]
}
]

View file

@ -107,7 +107,10 @@
"windows": "",
"darwin": "",
"linux": ""
}
},
"FLAME_WIRETAP_HOSTNAME": "",
"FLAME_WIRETAP_VOLUME": "stonefs",
"FLAME_WIRETAP_GROUP": "staff"
},
"variants": {
"2021": {