mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #2381 from pypeclub/feature/OP-1915_flame-ftrack-direct-link
This commit is contained in:
commit
c0c3fceae3
10 changed files with 1524 additions and 4 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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><name></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><segment name></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>
|
||||
|
|
@ -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><name></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><segment name></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><segment name>_<video codec></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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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())
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -107,7 +107,10 @@
|
|||
"windows": "",
|
||||
"darwin": "",
|
||||
"linux": ""
|
||||
}
|
||||
},
|
||||
"FLAME_WIRETAP_HOSTNAME": "",
|
||||
"FLAME_WIRETAP_VOLUME": "stonefs",
|
||||
"FLAME_WIRETAP_GROUP": "staff"
|
||||
},
|
||||
"variants": {
|
||||
"2021": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue