From 606ef6415d229e7ee29f2776749fc33b288e0dd8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 17 May 2022 17:42:55 +0200 Subject: [PATCH 01/22] Fix popping of `handles` --- openpype/hosts/maya/plugins/create/create_yeti_cache.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_yeti_cache.py b/openpype/hosts/maya/plugins/create/create_yeti_cache.py index 86e13b95b2..e8c3203f21 100644 --- a/openpype/hosts/maya/plugins/create/create_yeti_cache.py +++ b/openpype/hosts/maya/plugins/create/create_yeti_cache.py @@ -22,7 +22,8 @@ class CreateYetiCache(plugin.Creator): # Add animation data without step and handles anim_data = lib.collect_animation_data() anim_data.pop("step") - anim_data.pop("handles") + anim_data.pop("handleStart") + anim_data.pop("handleEnd") self.data.update(anim_data) # Add samples From e4d54aaa7a8304b426b67b707565451ce5e5599d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 17 May 2022 17:44:27 +0200 Subject: [PATCH 02/22] Fix invalid refactored usage --- openpype/hosts/maya/plugins/publish/extract_yeti_rig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py index d12567a55a..6b5054a198 100644 --- a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py @@ -167,7 +167,7 @@ class ExtractYetiRig(openpype.api.Extractor): resources = instance.data.get("resources", {}) with disconnect_plugs(settings, members): with yetigraph_attribute_values(resources_dir, resources): - with maya.attribute_values(attr_value): + with lib.attribute_values(attr_value): cmds.select(nodes, noExpand=True) cmds.file(maya_path, force=True, From 0f97c9f3d388a8973dce13e68fb9d263c0441b48 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 17 May 2022 17:50:41 +0200 Subject: [PATCH 03/22] Time values are required for exporting without errors --- openpype/hosts/maya/plugins/publish/extract_yeti_cache.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_cache.py b/openpype/hosts/maya/plugins/publish/extract_yeti_cache.py index 0d85708789..b0a60b77f4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_yeti_cache.py +++ b/openpype/hosts/maya/plugins/publish/extract_yeti_cache.py @@ -29,9 +29,9 @@ class ExtractYetiCache(openpype.api.Extractor): data_file = os.path.join(dirname, "yeti.fursettings") # Collect information for writing cache - start_frame = instance.data.get("frameStartHandle") - end_frame = instance.data.get("frameEndHandle") - preroll = instance.data.get("preroll") + start_frame = instance.data["frameStartHandle"] + end_frame = instance.data["frameEndHandle"] + preroll = instance.data["preroll"] if preroll > 0: start_frame -= preroll From 6fb9bb1558401273e2437e3e50a18877c296851e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 17 May 2022 17:54:41 +0200 Subject: [PATCH 04/22] Allow empty input_SET --- openpype/hosts/maya/plugins/publish/extract_yeti_rig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py index 6b5054a198..f981c4fe50 100644 --- a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py @@ -157,7 +157,7 @@ class ExtractYetiRig(openpype.api.Extractor): input_set = next(i for i in instance if i == "input_SET") # Get all items - set_members = cmds.sets(input_set, query=True) + set_members = cmds.sets(input_set, query=True) or [] set_members += cmds.listRelatives(set_members, allDescendents=True, fullPath=True) or [] From 2a2dbd243408f4eed8863fd86967a39080b4931d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 17 May 2022 17:56:45 +0200 Subject: [PATCH 05/22] Force required frame values for a single frame cache extract for `yetiRig` family. --- .../hosts/maya/plugins/publish/collect_yeti_rig.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py index 029432223b..bc15edd9e0 100644 --- a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py @@ -43,11 +43,12 @@ class CollectYetiRig(pyblish.api.InstancePlugin): instance.data["resources"] = yeti_resources - # Force frame range for export - instance.data["frameStart"] = cmds.playbackOptions( - query=True, animationStartTime=True) - instance.data["frameEnd"] = cmds.playbackOptions( - query=True, animationStartTime=True) + # Force frame range for yeti cache export for the rig + start = cmds.playbackOptions(query=True, animationStartTime=True) + for key in ["frameStart", "frameEnd", + "frameStartHandle", "frameEndHandle"]: + instance.data[key] = start + instance.data["preroll"] = 0 def collect_input_connections(self, instance): """Collect the inputs for all nodes in the input_SET""" From feb07912c50728054a5078c23a870f445969f947 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 20 May 2022 18:27:50 +0200 Subject: [PATCH 06/22] Fix yeti publish and load for caches --- .../maya/plugins/load/load_yeti_cache.py | 244 +++++++++--------- .../plugins/publish/extract_yeti_cache.py | 38 +-- .../maya/plugins/publish/extract_yeti_rig.py | 4 +- 3 files changed, 146 insertions(+), 140 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_yeti_cache.py b/openpype/hosts/maya/plugins/load/load_yeti_cache.py index fb903785ae..9752188551 100644 --- a/openpype/hosts/maya/plugins/load/load_yeti_cache.py +++ b/openpype/hosts/maya/plugins/load/load_yeti_cache.py @@ -1,15 +1,13 @@ import os import json import re -import glob from collections import defaultdict -from pprint import pprint +import clique from maya import cmds from openpype.api import get_project_settings from openpype.pipeline import ( - legacy_io, load, get_representation_path ) @@ -17,7 +15,15 @@ from openpype.hosts.maya.api import lib from openpype.hosts.maya.api.pipeline import containerise +def set_attribute(node, attr, value): + """Wrapper of set attribute which ignores None values""" + if value is None: + return + lib.set_attribute(node, attr, value) + + class YetiCacheLoader(load.LoaderPlugin): + """Load Yeti Cache with one or more Yeti nodes""" families = ["yeticache", "yetiRig"] representations = ["fur"] @@ -28,6 +34,16 @@ class YetiCacheLoader(load.LoaderPlugin): color = "orange" def load(self, context, name=None, namespace=None, data=None): + """Loads a .fursettings file defining how to load .fur sequences + + A single yeticache or yetiRig can have more than a single pgYetiMaya + nodes and thus load more than a single yeti.fur sequence. + + The .fursettings file defines what the node names should be and also + what "cbId" attribute they should receive to match the original source + and allow published looks to also work for Yeti rigs and its caches. + + """ try: family = context["representation"]["context"]["family"] @@ -43,22 +59,11 @@ class YetiCacheLoader(load.LoaderPlugin): if not cmds.pluginInfo("pgYetiMaya", query=True, loaded=True): cmds.loadPlugin("pgYetiMaya", quiet=True) - # Get JSON - fbase = re.search(r'^(.+)\.(\d+|#+)\.fur', self.fname) - if not fbase: - raise RuntimeError('Cannot determine fursettings file path') - settings_fname = "{}.fursettings".format(fbase.group(1)) - with open(settings_fname, "r") as fp: - fursettings = json.load(fp) - - # Check if resources map exists - # Get node name from JSON - if "nodes" not in fursettings: - raise RuntimeError("Encountered invalid data, expect 'nodes' in " - "fursettings.") - - node_data = fursettings["nodes"] - nodes = self.create_nodes(namespace, node_data) + # Create Yeti cache nodes according to settings + settings = self.read_settings(self.fname) + nodes = [] + for node in settings["nodes"]: + nodes.extend(self.create_node(namespace, node)) group_name = "{}:{}".format(namespace, name) group_node = cmds.group(nodes, name=group_name) @@ -111,28 +116,14 @@ class YetiCacheLoader(load.LoaderPlugin): def update(self, container, representation): - legacy_io.install() namespace = container["namespace"] container_node = container["objectName"] - fur_settings = legacy_io.find_one( - {"parent": representation["parent"], "name": "fursettings"} - ) - - pprint({"parent": representation["parent"], "name": "fursettings"}) - pprint(fur_settings) - assert fur_settings is not None, ( - "cannot find fursettings representation" - ) - - settings_fname = get_representation_path(fur_settings) path = get_representation_path(representation) - # Get all node data - with open(settings_fname, "r") as fp: - settings = json.load(fp) + settings = self.read_settings(path) # Collect scene information of asset - set_members = cmds.sets(container["objectName"], query=True) + set_members = lib.get_container_members(container) container_root = lib.get_container_transforms(container, members=set_members, root=True) @@ -147,7 +138,7 @@ class YetiCacheLoader(load.LoaderPlugin): # Re-assemble metadata with cbId as keys meta_data_lookup = {n["cbId"]: n for n in settings["nodes"]} - # Compare look ups and get the nodes which ar not relevant any more + # Delete nodes by "cbId" that are not in the updated version to_delete_lookup = {cb_id for cb_id in scene_lookup.keys() if cb_id not in meta_data_lookup} if to_delete_lookup: @@ -163,25 +154,18 @@ class YetiCacheLoader(load.LoaderPlugin): fullPath=True) or [] to_remove.extend(shapes + transforms) - # Remove id from look uop + # Remove id from lookup scene_lookup.pop(_id, None) cmds.delete(to_remove) - # replace frame in filename with %04d - RE_frame = re.compile(r"(\d+)(\.fur)$") - file_name = re.sub(RE_frame, r"%04d\g<2>", os.path.basename(path)) - for cb_id, data in meta_data_lookup.items(): - - # Update cache file name - data["attrs"]["cacheFileName"] = os.path.join( - os.path.dirname(path), file_name) + for cb_id, node_settings in meta_data_lookup.items(): if cb_id not in scene_lookup: - + # Create new nodes self.log.info("Creating new nodes ..") - new_nodes = self.create_nodes(namespace, [data]) + new_nodes = self.create_node(namespace, node_settings) cmds.sets(new_nodes, addElement=container_node) cmds.parent(new_nodes, container_root) @@ -218,14 +202,8 @@ class YetiCacheLoader(load.LoaderPlugin): children=True) yeti_node = yeti_nodes[0] - for attr, value in data["attrs"].items(): - # handle empty attribute strings. Those are reported - # as None, so their type is NoneType and this is not - # supported on attributes in Maya. We change it to - # empty string. - if value is None: - value = "" - lib.set_attribute(attr, value, yeti_node) + for attr, value in node_settings["attrs"].items(): + set_attribute(attr, value, yeti_node) cmds.setAttr("{}.representation".format(container_node), str(representation["_id"]), @@ -235,7 +213,6 @@ class YetiCacheLoader(load.LoaderPlugin): self.update(container, representation) # helper functions - def create_namespace(self, asset): """Create a unique namespace Args: @@ -253,100 +230,129 @@ class YetiCacheLoader(load.LoaderPlugin): return namespace - def validate_cache(self, filename, pattern="%04d"): - """Check if the cache has more than 1 frame + def get_cache_node_filepath(self, root, node_name): + """Get the cache file path for one of the yeti nodes. - All caches with more than 1 frame need to be called with `%04d` - If the cache has only one frame we return that file name as we assume + All caches with more than 1 frame need cache file name set with `%04d` + If the cache has only one frame we return the file name as we assume it is a snapshot. + This expects the files to be named after the "node name" through + exports with in Yeti. + Args: - filename(str) - pattern(str) + root(str): Folder containing cache files to search in. + node_name(str): Node name to search cache files for Returns: - str + str: Cache file path value needed for cacheFileName attribute """ - glob_pattern = filename.replace(pattern, "*") + name = node_name.replace(":", "_") + pattern = r"^({name})(\.[0-4]+)?(\.fur)$".format(name=re.escape(name)) - escaped = re.escape(filename) - re_pattern = escaped.replace(pattern, "-?[0-9]+") - - files = glob.glob(glob_pattern) - files = [str(f) for f in files if re.match(re_pattern, f)] + files = [fname for fname in os.listdir(root) if re.match(pattern, + fname)] + if not files: + self.log.error("Could not find cache files for '{}' " + "with pattern {}".format(node_name, pattern)) + return if len(files) == 1: - return files[0] - elif len(files) == 0: - self.log.error("Could not find cache files for '%s'" % filename) + # Single file + return os.path.join(root, files[0]) - return filename + # Get filename for the sequence with padding + collections, remainder = clique.assemble(files) + assert not remainder, "This is a bug" + assert len(collections) == 1, "This is a bug" + collection = collections[0] - def create_nodes(self, namespace, settings): + # Assume padding from the first frame since clique returns 0 if the + # sequence contains no files padded with a zero at the start (e.g. + # a sequence starting at 1001) + padding = len(str(collection.indexes[0])) + + fname = "{head}%0{padding}d{tail}".format(collection.head, + padding, + collection.tail) + + return os.path.join(root, fname) + + def create_node(self, namespace, node_settings): """Create nodes with the correct namespace and settings Args: namespace(str): namespace - settings(list): list of dictionaries + node_settings(dict): Single "nodes" entry from .fursettings file. Returns: - list + list: Created nodes """ - nodes = [] - for node_settings in settings: - # Create pgYetiMaya node - original_node = node_settings["name"] - node_name = "{}:{}".format(namespace, original_node) - yeti_node = cmds.createNode("pgYetiMaya", name=node_name) + # Get original names and ids + orig_transform_name = node_settings["transform"]["name"] + orig_shape_name = node_settings["name"] - # Create transform node - transform_node = node_name.rstrip("Shape") + # Add namespace + transform_name = "{}:{}".format(namespace, orig_transform_name) + shape_name = "{}:{}".format(namespace, orig_shape_name) - lib.set_id(transform_node, node_settings["transform"]["cbId"]) - lib.set_id(yeti_node, node_settings["cbId"]) + # Create pgYetiMaya node + transform_node = cmds.createNode("transform", + name=transform_name) + yeti_node = cmds.createNode("pgYetiMaya", + name=shape_name, + parent=transform_node) - nodes.extend([transform_node, yeti_node]) + lib.set_id(transform_node, node_settings["transform"]["cbId"]) + lib.set_id(yeti_node, node_settings["cbId"]) - # Ensure the node has no namespace identifiers - attributes = node_settings["attrs"] + nodes.extend([transform_node, yeti_node]) - # Check if cache file name is stored + # Update attributes with defaults + attributes = node_settings["attrs"] + attributes.update({ + "viewportDensity": 0.1, + "verbosity": 2, + "fileMode": 1, - # get number of # in path and convert it to C prinf format - # like %04d expected by Yeti - fbase = re.search(r'^(.+)\.(\d+|#+)\.fur', self.fname) - if not fbase: - raise RuntimeError('Cannot determine file path') - padding = len(fbase.group(2)) - if "cacheFileName" not in attributes: - cache = "{}.%0{}d.fur".format(fbase.group(1), padding) + # Fix render stats, like Yeti's own + # ../scripts/pgYetiNode.mel script + "visibleInReflections": True, + "visibleInRefractions": True + }) - self.validate_cache(cache) - attributes["cacheFileName"] = cache + # Apply attributes to pgYetiMaya node + for attr, value in attributes.items(): + set_attribute(attr, value, yeti_node) - # Update attributes with requirements - attributes.update({"viewportDensity": 0.1, - "verbosity": 2, - "fileMode": 1}) - - # Apply attributes to pgYetiMaya node - for attr, value in attributes.items(): - if value is None: - continue - lib.set_attribute(attr, value, yeti_node) - - # Fix for : YETI-6 - # Fixes the render stats (this is literally taken from Perigrene's - # ../scripts/pgYetiNode.mel script) - cmds.setAttr("{}.visibleInReflections".format(yeti_node), True) - cmds.setAttr("{}.visibleInRefractions".format(yeti_node), True) - - # Connect to the time node - cmds.connectAttr("time1.outTime", "%s.currentTime" % yeti_node) + # Connect to the time node + cmds.connectAttr("time1.outTime", "%s.currentTime" % yeti_node) return nodes + + def read_settings(self, path): + """Read .fursettings file and compute some additional attributes""" + + with open(path, "r") as fp: + fur_settings = json.load(fp) + + if "nodes" not in fur_settings: + raise RuntimeError("Encountered invalid data, " + "expected 'nodes' in fursettings.") + + # Compute the cache file name values we want to set for the nodes + root = os.path.dirname(path) + for node in fur_settings["nodes"]: + cache_filename = self.get_cache_node_filepath( + root=root, node_name=node["name"]) + + attrs = node.get("attrs", {}) # allow 'attrs' to not exist + attrs["cacheFileName"] = cache_filename + node["attrs"] = attrs + + return fur_settings diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_cache.py b/openpype/hosts/maya/plugins/publish/extract_yeti_cache.py index b0a60b77f4..cf6db00e9a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_yeti_cache.py +++ b/openpype/hosts/maya/plugins/publish/extract_yeti_cache.py @@ -25,9 +25,6 @@ class ExtractYetiCache(openpype.api.Extractor): # Define extract output file path dirname = self.staging_dir(instance) - # Yeti related staging dirs - data_file = os.path.join(dirname, "yeti.fursettings") - # Collect information for writing cache start_frame = instance.data["frameStartHandle"] end_frame = instance.data["frameEndHandle"] @@ -57,32 +54,35 @@ class ExtractYetiCache(openpype.api.Extractor): cache_files = [x for x in os.listdir(dirname) if x.endswith(".fur")] self.log.info("Writing metadata file") - settings = instance.data.get("fursettings", None) - if settings is not None: - with open(data_file, "w") as fp: - json.dump(settings, fp, ensure_ascii=False) + settings = instance.data["fursettings"] + fursettings_path = os.path.join(dirname, "yeti.fursettings") + with open(fursettings_path, "w") as fp: + json.dump(settings, fp, ensure_ascii=False) # build representations if "representations" not in instance.data: instance.data["representations"] = [] self.log.info("cache files: {}".format(cache_files[0])) - instance.data["representations"].append( - { - 'name': 'fur', - 'ext': 'fur', - 'files': cache_files[0] if len(cache_files) == 1 else cache_files, - 'stagingDir': dirname, - 'frameStart': int(start_frame), - 'frameEnd': int(end_frame) - } - ) + + # Workaround: We do not explicitly register these files with the + # representation solely so that we can write multiple sequences + # a single Subset without renaming - it's a bit of a hack + # TODO: Implement better way to manage this sort of integration + if 'transfers' not in instance.data: + instance.data['transfers'] = [] + + publish_dir = instance.data["publishDir"] + for cache_filename in cache_files: + src = os.path.join(dirname, cache_filename) + dst = os.path.join(publish_dir, os.path.basename(cache_filename)) + instance.data['transfers'].append([src, dst]) instance.data["representations"].append( { - 'name': 'fursettings', + 'name': 'fur', 'ext': 'fursettings', - 'files': os.path.basename(data_file), + 'files': os.path.basename(fursettings_path), 'stagingDir': dirname } ) diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py index f981c4fe50..6e21bffa4e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py @@ -124,8 +124,8 @@ class ExtractYetiRig(openpype.api.Extractor): settings_path = os.path.join(dirname, "yeti.rigsettings") # Yeti related staging dirs - maya_path = os.path.join( - dirname, "yeti_rig.{}".format(self.scene_type)) + maya_path = os.path.join(dirname, + "yeti_rig.{}".format(self.scene_type)) self.log.info("Writing metadata file") From 87f7fa5470ed5fb74433633810bf379d92a3b58e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 20 May 2022 18:32:02 +0200 Subject: [PATCH 07/22] Format name correctly for sequences on load --- openpype/hosts/maya/plugins/load/load_yeti_cache.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_yeti_cache.py b/openpype/hosts/maya/plugins/load/load_yeti_cache.py index 9752188551..8435ba2493 100644 --- a/openpype/hosts/maya/plugins/load/load_yeti_cache.py +++ b/openpype/hosts/maya/plugins/load/load_yeti_cache.py @@ -269,15 +269,8 @@ class YetiCacheLoader(load.LoaderPlugin): assert len(collections) == 1, "This is a bug" collection = collections[0] - # Assume padding from the first frame since clique returns 0 if the - # sequence contains no files padded with a zero at the start (e.g. - # a sequence starting at 1001) - padding = len(str(collection.indexes[0])) - - fname = "{head}%0{padding}d{tail}".format(collection.head, - padding, - collection.tail) - + # Formats name as {head}%d{tail} like cache.%04d.fur + fname = collection.format("{head}{padding}{tail}") return os.path.join(root, fname) def create_node(self, namespace, node_settings): From 242b9f5f3a9ed734743d7b88cba7191be4841fa3 Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Fri, 10 Jun 2022 00:49:01 +0200 Subject: [PATCH 08/22] added module setting and tray item --- openpype/modules/ftrack/tray/ftrack_tray.py | 44 ++++++++++++++----- .../defaults/system_settings/modules.json | 14 ++++++ .../module_settings/schema_ftrack.json | 37 ++++++++++++++++ tools/run_ftrack_eventserver.ps1 | 39 ++++++++++++++++ 4 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 tools/run_ftrack_eventserver.ps1 diff --git a/openpype/modules/ftrack/tray/ftrack_tray.py b/openpype/modules/ftrack/tray/ftrack_tray.py index c6201a94f6..699b33e187 100644 --- a/openpype/modules/ftrack/tray/ftrack_tray.py +++ b/openpype/modules/ftrack/tray/ftrack_tray.py @@ -2,6 +2,8 @@ import os import time import datetime import threading +import platform +from subprocess import Popen from Qt import QtCore, QtWidgets, QtGui import ftrack_api @@ -12,6 +14,7 @@ from ..ftrack_module import FTRACK_MODULE_DIR from . import login_dialog from openpype.api import Logger, resources +from openpype.settings import get_system_settings log = Logger().get_logger("FtrackModule") @@ -42,12 +45,25 @@ class FtrackTrayWrapper: self.icon_not_logged = QtGui.QIcon( resources.get_resource("icons", "circle_orange.png") ) + self.icon_ftrackapp = QtGui.QIcon( + resources.get_resource("icons", "inventory.png") + ) def show_login_widget(self): self.widget_login.show() self.widget_login.activateWindow() self.widget_login.raise_() + def show_ftrack_browser(self): + am = get_system_settings() + browser_path = am["modules"]["ftrack"]["ftrack_browser_path"][platform.system().lower()][0] + browser_arg = am["modules"]["ftrack"]["ftrack_browser_arguments"][platform.system().lower()][0] + if "=" not in browser_arg: + browser_arg = '{:1}'.format(browser_arg) + cmd = f"{browser_path} {browser_arg}{self.module.ftrack_url}" + log.info(f"Opening Ftrack Browser: {cmd}") + Popen(cmd) + def validate(self): validation = False cred = credentials.get_credentials() @@ -251,16 +267,12 @@ class FtrackTrayWrapper: # Menu for Tray App tray_menu = QtWidgets.QMenu("Ftrack", parent_menu) - # Actions - basic - action_credentials = QtWidgets.QAction("Credentials", tray_menu) - action_credentials.triggered.connect(self.show_login_widget) - if self.bool_logged: - icon = self.icon_logged - else: - icon = self.icon_not_logged - action_credentials.setIcon(icon) - tray_menu.addAction(action_credentials) - self.action_credentials = action_credentials + # Ftrack Browser + browser_open = QtWidgets.QAction("Open Ftrack...", tray_menu) + browser_open.triggered.connect(self.show_ftrack_browser) + browser_open.setIcon(self.icon_ftrackapp) + tray_menu.addAction(browser_open) + self.browser_open = browser_open # Actions - server tray_server_menu = tray_menu.addMenu("Action server") @@ -284,6 +296,18 @@ class FtrackTrayWrapper: tray_server_menu.addAction(self.action_server_stop) self.tray_server_menu = tray_server_menu + + # Actions - basic + action_credentials = QtWidgets.QAction("Credentials", tray_menu) + action_credentials.triggered.connect(self.show_login_widget) + if self.bool_logged: + icon = self.icon_logged + else: + icon = self.icon_not_logged + action_credentials.setIcon(icon) + tray_menu.addAction(action_credentials) + self.action_credentials = action_credentials + self.bool_logged = False self.set_menu_visibility() diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index 537e287366..aaf01b1631 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -15,6 +15,20 @@ "ftrack": { "enabled": false, "ftrack_server": "", + "ftrack_browser_path": { + "windows": [ + "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" + ], + "darwin": [], + "linux": [] + }, + "ftrack_browser_arguments": { + "windows": [ + "--app=" + ], + "darwin": [], + "linux": [] + }, "ftrack_actions_path": { "windows": [], "darwin": [], diff --git a/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json b/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json index 654ddf2938..f30d536052 100644 --- a/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json +++ b/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json @@ -19,6 +19,43 @@ { "type": "splitter" }, + { + "type": "path", + "key": "ftrack_browser_path", + "label": "Browser Path", + "use_label_wrap": true, + "multipath": true, + "multiplatform": true + }, + { + "type": "dict", + "key": "ftrack_browser_arguments", + "label": "Browser Arguments", + "use_label_wrap": true, + "children": [ + { + "key": "windows", + "label": "Windows", + "type": "list", + "object_type": "text" + }, + { + "key": "darwin", + "label": "MacOS", + "type": "list", + "object_type": "text" + }, + { + "key": "linux", + "label": "Linux", + "type": "list", + "object_type": "text" + } + ] + }, + { + "type": "splitter" + }, { "type": "label", "label": "Additional Ftrack event handlers paths" diff --git a/tools/run_ftrack_eventserver.ps1 b/tools/run_ftrack_eventserver.ps1 new file mode 100644 index 0000000000..9c22f3d88e --- /dev/null +++ b/tools/run_ftrack_eventserver.ps1 @@ -0,0 +1,39 @@ +<# +.SYNOPSIS + Helper script to start OpenPype Ftrack EventServer without relying on built executables. + +.DESCRIPTION + + +.EXAMPLE + +PS> .\run_eventserver.ps1 + +#> +$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent +$openpype_root = (Get-Item $script_dir).parent.FullName + +$env:_INSIDE_OPENPYPE_TOOL = "1" +$env:OPENPYPE_DEBUG = "1" +# $env:OPENPYPE_MONGO = "mongodb://127.0.0.1:27017" + +# make sure Poetry is in PATH +if (-not (Test-Path 'env:POETRY_HOME')) { + $env:POETRY_HOME = "$openpype_root\.poetry" +} +$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" + +Set-Location -Path $openpype_root + +Write-Host ">>> " -NoNewline -ForegroundColor Green +Write-Host "Reading Poetry ... " -NoNewline +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { + Write-Host "NOT FOUND" -ForegroundColor Yellow + Write-Host "*** " -NoNewline -ForegroundColor Yellow + Write-Host "We need to install Poetry create virtual env first ..." + & "$openpype_root\tools\create_env.ps1" +} else { + Write-Host "OK" -ForegroundColor Green +} + +& "$($env:POETRY_HOME)\bin\poetry" run python "$($openpype_root)\start.py" eventserver \ No newline at end of file From 924535a6ce57c3d07bb3cdb61cde92978edc9236 Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Fri, 10 Jun 2022 15:30:14 +0200 Subject: [PATCH 09/22] removed icon since not the same as other entries --- openpype/modules/ftrack/tray/ftrack_tray.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/modules/ftrack/tray/ftrack_tray.py b/openpype/modules/ftrack/tray/ftrack_tray.py index 699b33e187..54d3f3132f 100644 --- a/openpype/modules/ftrack/tray/ftrack_tray.py +++ b/openpype/modules/ftrack/tray/ftrack_tray.py @@ -45,9 +45,6 @@ class FtrackTrayWrapper: self.icon_not_logged = QtGui.QIcon( resources.get_resource("icons", "circle_orange.png") ) - self.icon_ftrackapp = QtGui.QIcon( - resources.get_resource("icons", "inventory.png") - ) def show_login_widget(self): self.widget_login.show() @@ -270,7 +267,6 @@ class FtrackTrayWrapper: # Ftrack Browser browser_open = QtWidgets.QAction("Open Ftrack...", tray_menu) browser_open.triggered.connect(self.show_ftrack_browser) - browser_open.setIcon(self.icon_ftrackapp) tray_menu.addAction(browser_open) self.browser_open = browser_open From 5e82c96a3da493de7259ffc122c21ecc617340c7 Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Fri, 10 Jun 2022 15:53:39 +0200 Subject: [PATCH 10/22] info label in settings, handles lists better --- openpype/modules/ftrack/tray/ftrack_tray.py | 17 +++++++++++------ .../module_settings/schema_ftrack.json | 4 ++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/tray/ftrack_tray.py b/openpype/modules/ftrack/tray/ftrack_tray.py index 54d3f3132f..30e1d9f983 100644 --- a/openpype/modules/ftrack/tray/ftrack_tray.py +++ b/openpype/modules/ftrack/tray/ftrack_tray.py @@ -52,12 +52,17 @@ class FtrackTrayWrapper: self.widget_login.raise_() def show_ftrack_browser(self): - am = get_system_settings() - browser_path = am["modules"]["ftrack"]["ftrack_browser_path"][platform.system().lower()][0] - browser_arg = am["modules"]["ftrack"]["ftrack_browser_arguments"][platform.system().lower()][0] - if "=" not in browser_arg: - browser_arg = '{:1}'.format(browser_arg) - cmd = f"{browser_path} {browser_arg}{self.module.ftrack_url}" + settings = get_system_settings() + browser_paths = settings["modules"]["ftrack"]["ftrack_browser_path"][platform.system().lower()] + browser_args = settings["modules"]["ftrack"]["ftrack_browser_arguments"][platform.system().lower()] + browser_args.append(self.module.ftrack_url) + path = "" + for p in browser_paths: + if os.path.exists(p): + path = p + break + args = " ".join(str(item) for item in browser_args).replace("= ", "=") + cmd = f"{path} {args}" log.info(f"Opening Ftrack Browser: {cmd}") Popen(cmd) diff --git a/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json b/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json index f30d536052..268c5479fe 100644 --- a/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json +++ b/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json @@ -33,6 +33,10 @@ "label": "Browser Arguments", "use_label_wrap": true, "children": [ + { + "type": "label", + "label": "Any arguent which is used to open Ftrack URL (as in \"app=\" for chrome) needs to be placed last in the list!" + }, { "key": "windows", "label": "Windows", From 4a45225ca27d175adc481a8899a3379bcab77dc1 Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Fri, 10 Jun 2022 16:55:50 +0200 Subject: [PATCH 11/22] moved menu entry to last position --- openpype/modules/ftrack/tray/ftrack_tray.py | 30 ++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/openpype/modules/ftrack/tray/ftrack_tray.py b/openpype/modules/ftrack/tray/ftrack_tray.py index 30e1d9f983..4329b03b45 100644 --- a/openpype/modules/ftrack/tray/ftrack_tray.py +++ b/openpype/modules/ftrack/tray/ftrack_tray.py @@ -269,11 +269,16 @@ class FtrackTrayWrapper: # Menu for Tray App tray_menu = QtWidgets.QMenu("Ftrack", parent_menu) - # Ftrack Browser - browser_open = QtWidgets.QAction("Open Ftrack...", tray_menu) - browser_open.triggered.connect(self.show_ftrack_browser) - tray_menu.addAction(browser_open) - self.browser_open = browser_open + # Actions - basic + action_credentials = QtWidgets.QAction("Credentials", tray_menu) + action_credentials.triggered.connect(self.show_login_widget) + if self.bool_logged: + icon = self.icon_logged + else: + icon = self.icon_not_logged + action_credentials.setIcon(icon) + tray_menu.addAction(action_credentials) + self.action_credentials = action_credentials # Actions - server tray_server_menu = tray_menu.addMenu("Action server") @@ -298,16 +303,11 @@ class FtrackTrayWrapper: self.tray_server_menu = tray_server_menu - # Actions - basic - action_credentials = QtWidgets.QAction("Credentials", tray_menu) - action_credentials.triggered.connect(self.show_login_widget) - if self.bool_logged: - icon = self.icon_logged - else: - icon = self.icon_not_logged - action_credentials.setIcon(icon) - tray_menu.addAction(action_credentials) - self.action_credentials = action_credentials + # Ftrack Browser + browser_open = QtWidgets.QAction("Open Ftrack...", tray_menu) + browser_open.triggered.connect(self.show_ftrack_browser) + tray_menu.addAction(browser_open) + self.browser_open = browser_open self.bool_logged = False self.set_menu_visibility() From 8add4b588d762d470a9e6138cb9c3c4542b75826 Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Fri, 10 Jun 2022 17:19:59 +0200 Subject: [PATCH 12/22] fixed checks and logged correct info --- openpype/modules/ftrack/tray/ftrack_tray.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/tray/ftrack_tray.py b/openpype/modules/ftrack/tray/ftrack_tray.py index 4329b03b45..e3df8eff12 100644 --- a/openpype/modules/ftrack/tray/ftrack_tray.py +++ b/openpype/modules/ftrack/tray/ftrack_tray.py @@ -3,7 +3,7 @@ import time import datetime import threading import platform -from subprocess import Popen +import subprocess from Qt import QtCore, QtWidgets, QtGui import ftrack_api @@ -60,11 +60,18 @@ class FtrackTrayWrapper: for p in browser_paths: if os.path.exists(p): path = p + log.debug(f"Found valid executable at path: {p}") break + else: + log.warning(f"Path: {p} is not valid, please doublecheck your settings!") + if path == "": + log.warning("Found no valid executables to launch Ftrack with. Feature will not work as expected!") + return args = " ".join(str(item) for item in browser_args).replace("= ", "=") + log.debug(f"Computed arguments: {args}") cmd = f"{path} {args}" - log.info(f"Opening Ftrack Browser: {cmd}") - Popen(cmd) + log.debug(f"Opening Ftrack Browser...") + subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) def validate(self): validation = False From 11fc8d26e70689f76bb6ddfac60923c70cd8bbc9 Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Fri, 10 Jun 2022 17:36:38 +0200 Subject: [PATCH 13/22] Line breaks as per hound's "suggestion" --- openpype/modules/ftrack/tray/ftrack_tray.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/modules/ftrack/tray/ftrack_tray.py b/openpype/modules/ftrack/tray/ftrack_tray.py index e3df8eff12..ee1e50f7f5 100644 --- a/openpype/modules/ftrack/tray/ftrack_tray.py +++ b/openpype/modules/ftrack/tray/ftrack_tray.py @@ -53,8 +53,10 @@ class FtrackTrayWrapper: def show_ftrack_browser(self): settings = get_system_settings() - browser_paths = settings["modules"]["ftrack"]["ftrack_browser_path"][platform.system().lower()] - browser_args = settings["modules"]["ftrack"]["ftrack_browser_arguments"][platform.system().lower()] + browser_paths = settings["modules"]["ftrack"]\ + ["ftrack_browser_path"][platform.system().lower()] + browser_args = settings["modules"]["ftrack"]\ + ["ftrack_browser_arguments"][platform.system().lower()] browser_args.append(self.module.ftrack_url) path = "" for p in browser_paths: @@ -63,9 +65,11 @@ class FtrackTrayWrapper: log.debug(f"Found valid executable at path: {p}") break else: - log.warning(f"Path: {p} is not valid, please doublecheck your settings!") + log.warning(f"Path: {p} is not valid, please \ + doublecheck your settings!") if path == "": - log.warning("Found no valid executables to launch Ftrack with. Feature will not work as expected!") + log.warning("Found no valid executables to launch \ + Ftrack with. Feature will not work as expected!") return args = " ".join(str(item) for item in browser_args).replace("= ", "=") log.debug(f"Computed arguments: {args}") From 24a31e5e1058705d6d58fc8559fc42cfd8d661b8 Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Fri, 10 Jun 2022 17:49:16 +0200 Subject: [PATCH 14/22] fixed hound complaints hopefully --- openpype/modules/ftrack/tray/ftrack_tray.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/modules/ftrack/tray/ftrack_tray.py b/openpype/modules/ftrack/tray/ftrack_tray.py index ee1e50f7f5..7ac994e967 100644 --- a/openpype/modules/ftrack/tray/ftrack_tray.py +++ b/openpype/modules/ftrack/tray/ftrack_tray.py @@ -52,11 +52,10 @@ class FtrackTrayWrapper: self.widget_login.raise_() def show_ftrack_browser(self): - settings = get_system_settings() - browser_paths = settings["modules"]["ftrack"]\ - ["ftrack_browser_path"][platform.system().lower()] - browser_args = settings["modules"]["ftrack"]\ - ["ftrack_browser_arguments"][platform.system().lower()] + cur_os = platform.system().lower() + settings = get_system_settings()["modules"]["ftrack"] + browser_paths = settings["ftrack_browser_path"][cur_os] + browser_args = settings["ftrack_browser_arguments"][cur_os] browser_args.append(self.module.ftrack_url) path = "" for p in browser_paths: From cef804aa1670b755b5c0072a9f0d5990f6fa92d3 Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Mon, 13 Jun 2022 15:37:47 +0200 Subject: [PATCH 15/22] added eventserver utoility script for linux/mac --- tools/run_ftrack_eventserver.sh | 99 +++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 tools/run_ftrack_eventserver.sh diff --git a/tools/run_ftrack_eventserver.sh b/tools/run_ftrack_eventserver.sh new file mode 100644 index 0000000000..97daa14c2d --- /dev/null +++ b/tools/run_ftrack_eventserver.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +art () { + cat <<-EOF + + . . .. . .. + _oOOP3OPP3Op_. . + .PPpo~· ·· ~2p. ·· ···· · · + ·Ppo · .pPO3Op.· · O:· · · · + .3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · · + ·~OP 3PO· .Op3 : · ·· _____ _____ _____ + ·P3O · oP3oP3O3P' · · · · / /·/ /·/ / + O3:· O3p~ · ·:· · ·/____/·/____/ /____/ + 'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · · + · ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · · + · '_ .. · . _OP3·· · ·https://openpype.io·· · + ~P3·OPPPO3OP~ · ·· · + · ' '· · ·· · · · ·· · + +EOF +} + +# Colors for terminal + +RST='\033[0m' # Text Reset + +# Regular Colors +Black='\033[0;30m' # Black +Red='\033[0;31m' # Red +Green='\033[0;32m' # Green +Yellow='\033[0;33m' # Yellow +Blue='\033[0;34m' # Blue +Purple='\033[0;35m' # Purple +Cyan='\033[0;36m' # Cyan +White='\033[0;37m' # White + +# Bold +BBlack='\033[1;30m' # Black +BRed='\033[1;31m' # Red +BGreen='\033[1;32m' # Green +BYellow='\033[1;33m' # Yellow +BBlue='\033[1;34m' # Blue +BPurple='\033[1;35m' # Purple +BCyan='\033[1;36m' # Cyan +BWhite='\033[1;37m' # White + +# Bold High Intensity +BIBlack='\033[1;90m' # Black +BIRed='\033[1;91m' # Red +BIGreen='\033[1;92m' # Green +BIYellow='\033[1;93m' # Yellow +BIBlue='\033[1;94m' # Blue +BIPurple='\033[1;95m' # Purple +BICyan='\033[1;96m' # Cyan +BIWhite='\033[1;97m' # White + + +############################################################################## +# Return absolute path +# Globals: +# None +# Arguments: +# Path to resolve +# Returns: +# None +############################################################################### +realpath () { + echo $(cd $(dirname "$1"); pwd)/$(basename "$1") +} + +# Main +main () { + echo -e "${BGreen}" + art + echo -e "${RST}" + + # Directories + openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) + + if [[ -z $POETRY_HOME ]]; then + export POETRY_HOME="$openpype_root/.poetry" + fi + + echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c" + if [ -f "$POETRY_HOME/bin/poetry" ]; then + echo -e "${BIGreen}OK${RST}" + else + echo -e "${BIYellow}NOT FOUND${RST}" + echo -e "${BIYellow}***${RST} We need to install Poetry and virtual env ..." + . "$openpype_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; } + fi + + pushd "$openpype_root" > /dev/null || return > /dev/null + + echo -e "${BIGreen}>>>${RST} Running Ftrack Eventserver ..." + "$POETRY_HOME/bin/poetry" run python $openpype_root/start.py eventserver +} + +main From 3d294edf6ef08441e71fdaf56c7b84e339de8a5e Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Tue, 14 Jun 2022 17:55:48 +0200 Subject: [PATCH 16/22] Fixed logic and settings, uses webbrowser module --- openpype/modules/ftrack/ftrack_module.py | 3 + openpype/modules/ftrack/tray/ftrack_tray.py | 63 +++++++----- .../defaults/system_settings/modules.json | 15 +-- .../module_settings/schema_ftrack.json | 42 +------- tools/run_ftrack_eventserver.ps1 | 39 -------- tools/run_ftrack_eventserver.sh | 99 ------------------- 6 files changed, 47 insertions(+), 214 deletions(-) delete mode 100644 tools/run_ftrack_eventserver.ps1 delete mode 100644 tools/run_ftrack_eventserver.sh diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index f99e189082..048e5ebfb1 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -42,6 +42,9 @@ class FtrackModule( self.ftrack_url = ftrack_url + ftrack_open_as_app = ftrack_settings["ftrack_open_as_app"] + self.ftrack_open_as_app = ftrack_open_as_app + current_dir = os.path.dirname(os.path.abspath(__file__)) low_platform = platform.system().lower() diff --git a/openpype/modules/ftrack/tray/ftrack_tray.py b/openpype/modules/ftrack/tray/ftrack_tray.py index 7ac994e967..065528dcff 100644 --- a/openpype/modules/ftrack/tray/ftrack_tray.py +++ b/openpype/modules/ftrack/tray/ftrack_tray.py @@ -1,9 +1,14 @@ +from hashlib import new +from operator import pos import os import time import datetime import threading import platform import subprocess +import posixpath, ntpath +import webbrowser +import shutil from Qt import QtCore, QtWidgets, QtGui import ftrack_api @@ -14,7 +19,6 @@ from ..ftrack_module import FTRACK_MODULE_DIR from . import login_dialog from openpype.api import Logger, resources -from openpype.settings import get_system_settings log = Logger().get_logger("FtrackModule") @@ -52,29 +56,42 @@ class FtrackTrayWrapper: self.widget_login.raise_() def show_ftrack_browser(self): - cur_os = platform.system().lower() - settings = get_system_settings()["modules"]["ftrack"] - browser_paths = settings["ftrack_browser_path"][cur_os] - browser_args = settings["ftrack_browser_arguments"][cur_os] - browser_args.append(self.module.ftrack_url) - path = "" - for p in browser_paths: - if os.path.exists(p): - path = p - log.debug(f"Found valid executable at path: {p}") - break + env_pf64 = os.environ['ProgramW6432'].replace( + ntpath.sep, posixpath.sep) + env_pf32 = os.environ['ProgramFiles(x86)'].replace( + ntpath.sep, posixpath.sep) + env_loc = os.environ['LocalAppData'].replace( + ntpath.sep, posixpath.sep) + chromium_paths_win = [ + f"{env_pf64}/Google/Chrome/Application/chrome.exe", + f"{env_pf32}/Google/Chrome/Application/chrome.exe", + f"{env_loc}/Google/Chrome/Application/chrome.exe", + f"{env_pf32}/Microsoft/Edge/Application/msedge.exe" + ] + cur_os = cur_os = platform.system().lower() + if cur_os == "windows": + is_chromium = False + for p in chromium_paths_win: + if os.path.exists(p): + is_chromium = True + chromium_path = p + break + if is_chromium and self.module.ftrack_open_as_app: + webbrowser.get(f"{chromium_path} %s").open_new( + f"--app={self.module.ftrack_url}") else: - log.warning(f"Path: {p} is not valid, please \ - doublecheck your settings!") - if path == "": - log.warning("Found no valid executables to launch \ - Ftrack with. Feature will not work as expected!") - return - args = " ".join(str(item) for item in browser_args).replace("= ", "=") - log.debug(f"Computed arguments: {args}") - cmd = f"{path} {args}" - log.debug(f"Opening Ftrack Browser...") - subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + webbrowser.get(using="windows-default").open_new( + self.module.ftrack_url) + + else: + if self.module.ftrack_open_as_app: + try: + webbrowser.get(using='chrome').open_new( + f"--app={self.module.ftrack_url}") + except webbrowser.Error: + webbrowser.open_new(self.module.ftrack_url) + else: + webbrowser.open_new(self.module.ftrack_url) def validate(self): validation = False diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index aaf01b1631..6d09652bb9 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -15,20 +15,7 @@ "ftrack": { "enabled": false, "ftrack_server": "", - "ftrack_browser_path": { - "windows": [ - "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" - ], - "darwin": [], - "linux": [] - }, - "ftrack_browser_arguments": { - "windows": [ - "--app=" - ], - "darwin": [], - "linux": [] - }, + "ftrack_open_as_app": false, "ftrack_actions_path": { "windows": [], "darwin": [], diff --git a/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json b/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json index 268c5479fe..570d856cf8 100644 --- a/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json +++ b/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json @@ -17,45 +17,9 @@ "label": "Server" }, { - "type": "splitter" - }, - { - "type": "path", - "key": "ftrack_browser_path", - "label": "Browser Path", - "use_label_wrap": true, - "multipath": true, - "multiplatform": true - }, - { - "type": "dict", - "key": "ftrack_browser_arguments", - "label": "Browser Arguments", - "use_label_wrap": true, - "children": [ - { - "type": "label", - "label": "Any arguent which is used to open Ftrack URL (as in \"app=\" for chrome) needs to be placed last in the list!" - }, - { - "key": "windows", - "label": "Windows", - "type": "list", - "object_type": "text" - }, - { - "key": "darwin", - "label": "MacOS", - "type": "list", - "object_type": "text" - }, - { - "key": "linux", - "label": "Linux", - "type": "list", - "object_type": "text" - } - ] + "type": "boolean", + "key": "ftrack_open_as_app", + "label": "Open in app mode" }, { "type": "splitter" diff --git a/tools/run_ftrack_eventserver.ps1 b/tools/run_ftrack_eventserver.ps1 deleted file mode 100644 index 9c22f3d88e..0000000000 --- a/tools/run_ftrack_eventserver.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -<# -.SYNOPSIS - Helper script to start OpenPype Ftrack EventServer without relying on built executables. - -.DESCRIPTION - - -.EXAMPLE - -PS> .\run_eventserver.ps1 - -#> -$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent -$openpype_root = (Get-Item $script_dir).parent.FullName - -$env:_INSIDE_OPENPYPE_TOOL = "1" -$env:OPENPYPE_DEBUG = "1" -# $env:OPENPYPE_MONGO = "mongodb://127.0.0.1:27017" - -# make sure Poetry is in PATH -if (-not (Test-Path 'env:POETRY_HOME')) { - $env:POETRY_HOME = "$openpype_root\.poetry" -} -$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" - -Set-Location -Path $openpype_root - -Write-Host ">>> " -NoNewline -ForegroundColor Green -Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { - Write-Host "NOT FOUND" -ForegroundColor Yellow - Write-Host "*** " -NoNewline -ForegroundColor Yellow - Write-Host "We need to install Poetry create virtual env first ..." - & "$openpype_root\tools\create_env.ps1" -} else { - Write-Host "OK" -ForegroundColor Green -} - -& "$($env:POETRY_HOME)\bin\poetry" run python "$($openpype_root)\start.py" eventserver \ No newline at end of file diff --git a/tools/run_ftrack_eventserver.sh b/tools/run_ftrack_eventserver.sh deleted file mode 100644 index 97daa14c2d..0000000000 --- a/tools/run_ftrack_eventserver.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash - -art () { - cat <<-EOF - - . . .. . .. - _oOOP3OPP3Op_. . - .PPpo~· ·· ~2p. ·· ···· · · - ·Ppo · .pPO3Op.· · O:· · · · - .3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · · - ·~OP 3PO· .Op3 : · ·· _____ _____ _____ - ·P3O · oP3oP3O3P' · · · · / /·/ /·/ / - O3:· O3p~ · ·:· · ·/____/·/____/ /____/ - 'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · · - · ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · · - · '_ .. · . _OP3·· · ·https://openpype.io·· · - ~P3·OPPPO3OP~ · ·· · - · ' '· · ·· · · · ·· · - -EOF -} - -# Colors for terminal - -RST='\033[0m' # Text Reset - -# Regular Colors -Black='\033[0;30m' # Black -Red='\033[0;31m' # Red -Green='\033[0;32m' # Green -Yellow='\033[0;33m' # Yellow -Blue='\033[0;34m' # Blue -Purple='\033[0;35m' # Purple -Cyan='\033[0;36m' # Cyan -White='\033[0;37m' # White - -# Bold -BBlack='\033[1;30m' # Black -BRed='\033[1;31m' # Red -BGreen='\033[1;32m' # Green -BYellow='\033[1;33m' # Yellow -BBlue='\033[1;34m' # Blue -BPurple='\033[1;35m' # Purple -BCyan='\033[1;36m' # Cyan -BWhite='\033[1;37m' # White - -# Bold High Intensity -BIBlack='\033[1;90m' # Black -BIRed='\033[1;91m' # Red -BIGreen='\033[1;92m' # Green -BIYellow='\033[1;93m' # Yellow -BIBlue='\033[1;94m' # Blue -BIPurple='\033[1;95m' # Purple -BICyan='\033[1;96m' # Cyan -BIWhite='\033[1;97m' # White - - -############################################################################## -# Return absolute path -# Globals: -# None -# Arguments: -# Path to resolve -# Returns: -# None -############################################################################### -realpath () { - echo $(cd $(dirname "$1"); pwd)/$(basename "$1") -} - -# Main -main () { - echo -e "${BGreen}" - art - echo -e "${RST}" - - # Directories - openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) - - if [[ -z $POETRY_HOME ]]; then - export POETRY_HOME="$openpype_root/.poetry" - fi - - echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c" - if [ -f "$POETRY_HOME/bin/poetry" ]; then - echo -e "${BIGreen}OK${RST}" - else - echo -e "${BIYellow}NOT FOUND${RST}" - echo -e "${BIYellow}***${RST} We need to install Poetry and virtual env ..." - . "$openpype_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; } - fi - - pushd "$openpype_root" > /dev/null || return > /dev/null - - echo -e "${BIGreen}>>>${RST} Running Ftrack Eventserver ..." - "$POETRY_HOME/bin/poetry" run python $openpype_root/start.py eventserver -} - -main From 6a4387a866d52e027d74805a54fe4f8a43004c38 Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Tue, 14 Jun 2022 17:58:56 +0200 Subject: [PATCH 17/22] finxed hounds --- openpype/modules/ftrack/tray/ftrack_tray.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/tray/ftrack_tray.py b/openpype/modules/ftrack/tray/ftrack_tray.py index 065528dcff..70f6e69323 100644 --- a/openpype/modules/ftrack/tray/ftrack_tray.py +++ b/openpype/modules/ftrack/tray/ftrack_tray.py @@ -1,14 +1,12 @@ -from hashlib import new -from operator import pos import os import time import datetime import threading import platform -import subprocess -import posixpath, ntpath +import posixpath +import ntpath import webbrowser -import shutil + from Qt import QtCore, QtWidgets, QtGui import ftrack_api @@ -82,7 +80,7 @@ class FtrackTrayWrapper: else: webbrowser.get(using="windows-default").open_new( self.module.ftrack_url) - + else: if self.module.ftrack_open_as_app: try: From e98f81c70c4d8054f3cd2b8ef4ce6e0e1b7b11ba Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Tue, 14 Jun 2022 19:17:54 +0200 Subject: [PATCH 18/22] made the browser opening non blocking --- openpype/modules/ftrack/tray/ftrack_tray.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/ftrack/tray/ftrack_tray.py b/openpype/modules/ftrack/tray/ftrack_tray.py index 70f6e69323..e822fd4639 100644 --- a/openpype/modules/ftrack/tray/ftrack_tray.py +++ b/openpype/modules/ftrack/tray/ftrack_tray.py @@ -90,6 +90,7 @@ class FtrackTrayWrapper: webbrowser.open_new(self.module.ftrack_url) else: webbrowser.open_new(self.module.ftrack_url) + return def validate(self): validation = False From ca926cf3102d0ddb40d17e88c117415773314278 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 5 May 2022 17:05:23 +0200 Subject: [PATCH 19/22] nuke: adding extract thumbnail settings --- .../defaults/project_settings/nuke.json | 3 ++ .../schemas/schema_nuke_publish.json | 28 +++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 16348bec85..6c45e2a9c1 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -166,6 +166,9 @@ }, "ExtractThumbnail": { "enabled": true, + "use_rendered": true, + "bake_viewer_process": true, + "bake_viewer_input_process": true, "nodes": { "Reformat": [ [ diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index 04df957d67..575bfe79e7 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -135,9 +135,31 @@ "label": "Enabled" }, { - "type": "raw-json", - "key": "nodes", - "label": "Nodes" + "type": "boolean", + "key": "use_rendered", + "label": "Use rendered images" + }, + { + "type": "boolean", + "key": "bake_viewer_process", + "label": "Bake viewer process" + }, + { + "type": "boolean", + "key": "bake_viewer_input_process", + "label": "Bake viewer input process" + }, + { + "type": "collapsible-wrap", + "label": "Nodes", + "collapsible": true, + "children": [ + { + "type": "raw-json", + "key": "nodes", + "label": "Nodes" + } + ] } ] }, From e21106aa9250d5317a157adcac7b38cffab7d990 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 5 May 2022 17:06:19 +0200 Subject: [PATCH 20/22] nuke: adding new attributes to extract thumnail --- .../nuke/plugins/publish/extract_thumbnail.py | 96 +++++++++++-------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index ef6d486ca2..ce01f12a41 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -23,9 +23,13 @@ class ExtractThumbnail(openpype.api.Extractor): families = ["review"] hosts = ["nuke"] - # presets + # settings + use_rendered = False + bake_viewer_process = True + bake_viewer_input_process = True nodes = {} + def process(self, instance): if "render.farm" in instance.data["families"]: return @@ -53,48 +57,58 @@ class ExtractThumbnail(openpype.api.Extractor): "StagingDir `{0}`...".format(instance.data["stagingDir"])) temporary_nodes = [] - collection = instance.data.get("collection", None) - if collection: - # get path - fname = os.path.basename(collection.format( - "{head}{padding}{tail}")) - fhead = collection.format("{head}") + # try to connect already rendered images + if self.use_rendered: + collection = instance.data.get("collection", None) + self.log.debug("__ collection: `{}`".format(collection)) - # get first and last frame - first_frame = min(collection.indexes) - last_frame = max(collection.indexes) - else: - fname = os.path.basename(instance.data.get("path", None)) - fhead = os.path.splitext(fname)[0] + "." - first_frame = instance.data.get("frameStart", None) - last_frame = instance.data.get("frameEnd", None) + if collection: + # get path + fname = os.path.basename(collection.format( + "{head}{padding}{tail}")) + fhead = collection.format("{head}") - if "#" in fhead: - fhead = fhead.replace("#", "")[:-1] + # get first and last frame + first_frame = min(collection.indexes) + last_frame = max(collection.indexes) + else: + fname = os.path.basename(instance.data.get("path", None)) + fhead = os.path.splitext(fname)[0] + "." + first_frame = instance.data.get("frameStart", None) + last_frame = instance.data.get("frameEnd", None) - path_render = os.path.join(staging_dir, fname).replace("\\", "/") - # check if file exist otherwise connect to write node - if os.path.isfile(path_render): - rnode = nuke.createNode("Read") + self.log.debug("__ fhead: `{}`".format(fhead)) - rnode["file"].setValue(path_render) + if "#" in fhead: + fhead = fhead.replace("#", "")[:-1] - rnode["first"].setValue(first_frame) - rnode["origfirst"].setValue(first_frame) - rnode["last"].setValue(last_frame) - rnode["origlast"].setValue(last_frame) - temporary_nodes.append(rnode) - previous_node = rnode - else: - previous_node = node + path_render = os.path.join(staging_dir, fname).replace("\\", "/") + self.log.debug("__ path_render: `{}`".format(path_render)) - # get input process and connect it to baking - ipn = self.get_view_process_node() - if ipn is not None: - ipn.setInput(0, previous_node) - previous_node = ipn - temporary_nodes.append(ipn) + # check if file exist otherwise connect to write node + if os.path.isfile(path_render): + rnode = nuke.createNode("Read") + + rnode["file"].setValue(path_render) + + rnode["first"].setValue(first_frame) + rnode["origfirst"].setValue(first_frame) + rnode["last"].setValue(last_frame) + rnode["origlast"].setValue(last_frame) + temporary_nodes.append(rnode) + previous_node = rnode + else: + previous_node = node + + # bake viewer input look node into thumbnail image + if self.bake_viewer_input_process: + # get input process and connect it to baking + ipn = self.get_view_process_node() + if ipn is not None: + ipn.setInput(0, previous_node) + previous_node = ipn + temporary_nodes.append(ipn) reformat_node = nuke.createNode("Reformat") @@ -110,10 +124,12 @@ class ExtractThumbnail(openpype.api.Extractor): previous_node = reformat_node temporary_nodes.append(reformat_node) - dag_node = nuke.createNode("OCIODisplay") - dag_node.setInput(0, previous_node) - previous_node = dag_node - temporary_nodes.append(dag_node) + # bake viewer colorspace into thumbnail image + if self.bake_viewer_process: + dag_node = nuke.createNode("OCIODisplay") + dag_node.setInput(0, previous_node) + previous_node = dag_node + temporary_nodes.append(dag_node) # create write node write_node = nuke.createNode("Write") From 79f81b6b36bb94d077f1b3ef7e35db06e68b002f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 5 May 2022 21:09:47 +0200 Subject: [PATCH 21/22] nuke: refactory extract thumbnail for new settings attributes --- .../nuke/plugins/publish/extract_thumbnail.py | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index ce01f12a41..092fc07d6c 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -42,11 +42,17 @@ class ExtractThumbnail(openpype.api.Extractor): self.render_thumbnail(instance) def render_thumbnail(self, instance): + first_frame = instance.data["frameStartHandle"] + last_frame = instance.data["frameEndHandle"] + + # find frame range and define middle thumb frame + mid_frame = int((last_frame - first_frame) / 2) + node = instance[0] # group node self.log.info("Creating staging dir...") if "representations" not in instance.data: - instance.data["representations"] = list() + instance.data["representations"] = [] staging_dir = os.path.normpath( os.path.dirname(instance.data['path'])) @@ -69,21 +75,19 @@ class ExtractThumbnail(openpype.api.Extractor): "{head}{padding}{tail}")) fhead = collection.format("{head}") - # get first and last frame - first_frame = min(collection.indexes) - last_frame = max(collection.indexes) + thumb_fname = list(collection)[mid_frame] else: - fname = os.path.basename(instance.data.get("path", None)) + fname = thumb_fname = os.path.basename( + instance.data.get("path", None)) fhead = os.path.splitext(fname)[0] + "." - first_frame = instance.data.get("frameStart", None) - last_frame = instance.data.get("frameEnd", None) self.log.debug("__ fhead: `{}`".format(fhead)) if "#" in fhead: fhead = fhead.replace("#", "")[:-1] - path_render = os.path.join(staging_dir, fname).replace("\\", "/") + path_render = os.path.join( + staging_dir, thumb_fname).replace("\\", "/") self.log.debug("__ path_render: `{}`".format(path_render)) # check if file exist otherwise connect to write node @@ -92,10 +96,13 @@ class ExtractThumbnail(openpype.api.Extractor): rnode["file"].setValue(path_render) - rnode["first"].setValue(first_frame) - rnode["origfirst"].setValue(first_frame) - rnode["last"].setValue(last_frame) - rnode["origlast"].setValue(last_frame) + # turn it raw if none of baking is ON + if all([ + not self.bake_viewer_input_process, + not self.bake_viewer_process + ]): + rnode["raw"].setValue(True) + temporary_nodes.append(rnode) previous_node = rnode else: @@ -144,26 +151,18 @@ class ExtractThumbnail(openpype.api.Extractor): temporary_nodes.append(write_node) tags = ["thumbnail", "publish_on_farm"] - # retime for - mid_frame = int((int(last_frame) - int(first_frame)) / 2) \ - + int(first_frame) - first_frame = int(last_frame) / 2 - last_frame = int(last_frame) / 2 - repre = { 'name': name, 'ext': "jpg", "outputName": "thumb", 'files': file, "stagingDir": staging_dir, - "frameStart": first_frame, - "frameEnd": last_frame, "tags": tags } instance.data["representations"].append(repre) # Render frames - nuke.execute(write_node.name(), int(mid_frame), int(mid_frame)) + nuke.execute(write_node.name(), mid_frame, mid_frame) self.log.debug( "representations: {}".format(instance.data["representations"])) From db1316dd688dc2bba62395aa968ab8af7b5ce552 Mon Sep 17 00:00:00 2001 From: maxpareschi Date: Wed, 15 Jun 2022 20:56:33 +0200 Subject: [PATCH 22/22] Did this the easy way and let's go! --- openpype/modules/ftrack/ftrack_module.py | 3 -- openpype/modules/ftrack/tray/ftrack_tray.py | 42 +------------------ .../defaults/system_settings/modules.json | 1 - .../module_settings/schema_ftrack.json | 5 --- 4 files changed, 1 insertion(+), 50 deletions(-) diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index 048e5ebfb1..f99e189082 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -42,9 +42,6 @@ class FtrackModule( self.ftrack_url = ftrack_url - ftrack_open_as_app = ftrack_settings["ftrack_open_as_app"] - self.ftrack_open_as_app = ftrack_open_as_app - current_dir = os.path.dirname(os.path.abspath(__file__)) low_platform = platform.system().lower() diff --git a/openpype/modules/ftrack/tray/ftrack_tray.py b/openpype/modules/ftrack/tray/ftrack_tray.py index e822fd4639..2919ae22fb 100644 --- a/openpype/modules/ftrack/tray/ftrack_tray.py +++ b/openpype/modules/ftrack/tray/ftrack_tray.py @@ -2,10 +2,6 @@ import os import time import datetime import threading -import platform -import posixpath -import ntpath -import webbrowser from Qt import QtCore, QtWidgets, QtGui @@ -54,43 +50,7 @@ class FtrackTrayWrapper: self.widget_login.raise_() def show_ftrack_browser(self): - env_pf64 = os.environ['ProgramW6432'].replace( - ntpath.sep, posixpath.sep) - env_pf32 = os.environ['ProgramFiles(x86)'].replace( - ntpath.sep, posixpath.sep) - env_loc = os.environ['LocalAppData'].replace( - ntpath.sep, posixpath.sep) - chromium_paths_win = [ - f"{env_pf64}/Google/Chrome/Application/chrome.exe", - f"{env_pf32}/Google/Chrome/Application/chrome.exe", - f"{env_loc}/Google/Chrome/Application/chrome.exe", - f"{env_pf32}/Microsoft/Edge/Application/msedge.exe" - ] - cur_os = cur_os = platform.system().lower() - if cur_os == "windows": - is_chromium = False - for p in chromium_paths_win: - if os.path.exists(p): - is_chromium = True - chromium_path = p - break - if is_chromium and self.module.ftrack_open_as_app: - webbrowser.get(f"{chromium_path} %s").open_new( - f"--app={self.module.ftrack_url}") - else: - webbrowser.get(using="windows-default").open_new( - self.module.ftrack_url) - - else: - if self.module.ftrack_open_as_app: - try: - webbrowser.get(using='chrome').open_new( - f"--app={self.module.ftrack_url}") - except webbrowser.Error: - webbrowser.open_new(self.module.ftrack_url) - else: - webbrowser.open_new(self.module.ftrack_url) - return + QtGui.QDesktopServices.openUrl(self.module.ftrack_url) def validate(self): validation = False diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index 6d09652bb9..537e287366 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -15,7 +15,6 @@ "ftrack": { "enabled": false, "ftrack_server": "", - "ftrack_open_as_app": false, "ftrack_actions_path": { "windows": [], "darwin": [], diff --git a/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json b/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json index 570d856cf8..654ddf2938 100644 --- a/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json +++ b/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json @@ -16,11 +16,6 @@ "key": "ftrack_server", "label": "Server" }, - { - "type": "boolean", - "key": "ftrack_open_as_app", - "label": "Open in app mode" - }, { "type": "splitter" },