class ExtractProject(api.ContextPlugin): """Extract an Ftrack project from context.data["ftrackProjectData"]""" order = inventory.get_order(__file__, "ExtractProject") label = "Ftrack Project" hosts = ["nukestudio"] def process(self, context): session = context.data["ftrackSession"] data = {} for key, value in context.data["ftrackProjectData"].iteritems(): if not value: continue data[key] = value # Get project from data query = "Project where " for key, value in data.iteritems(): query += "{0} is \"{1}\" and ".format(key, value) query = query[:-5] project = session.query(query).first() # Create project if it does not exist if not project: self.log.info("Creating project with data: {0}".format(data)) project = session.create("Project", data) session.commit() context.data["ftrackProject"] = project
class ExtractTasks(api.InstancePlugin): """Creates ftrack shots by the name of the instance.""" order = inventory.get_order(__file__, "ExtractTasks") families = ["trackItem.ftrackEntity.task"] label = "Ftrack Tasks" optional = True def process(self, instance): session = instance.context.data["ftrackSession"] task_type = session.query('Type where name is "{0}"'.format( instance.data["type"])).one() query = ('Task where type.name is "{0}" and name is "{1}" and ' 'parent.id is "{2}"') task = session.query( query.format( instance.data["type"], instance.data["name"], instance.data["parent"].data["entity"]["id"])).first() if not task: task = session.create( "Task", { "name": instance.data["name"], "type": task_type, "parent": instance.data["parent"].data["entity"] }) instance.data["entity"] = task
class ExtractAudio(api.InstancePlugin): """ Extracting audio """ families = ["audio"] label = "Audio" hosts = ["hiero"] order = inventory.get_order(__file__, "ExtractAudio") optional = True def process(self, instance): import os item = instance[0] output_file = os.path.join( os.path.dirname(instance.context.data["currentFile"]), "workspace", item.parent().parent().name(), item.parent().name(), item.name() + ".wav" ) output_dir = os.path.dirname(output_file) if not os.path.exists(output_dir): os.makedirs(output_dir) item.sequence().writeAudioToFile(output_file, item.timelineIn(), item.timelineOut()) instance.data["audio"] = output_file
class ExtractLocal(api.InstancePlugin): """ Extracts nodes locally. """ families = ["local"] order = inventory.get_order(__file__, "ExtractLocal") label = "Local" optional = True hosts = ["houdini"] def process(self, instance): import os node = instance[0] node.parm("execute").pressButton() # raising any errors if node.errors(): raise ValueError(node.errors()) # gather extracted files collection = instance.data["collection"] for f in collection: if not os.path.exists(f): collection.remove(f)
class ValidateOutputRange(api.InstancePlugin): """Validate the output range of the task. This compares the output range and clip associated with the task, so see whether there is a difference. This difference indicates that the user has selected to export the clip length for the task which is very uncommon to do. """ order = inventory.get_order(__file__, "ValidateOutputRange") families = ["trackItem.task"] label = "Output Range" hosts = ["nukestudio"] optional = True def process(self, instance): task = instance.data["task"] item = instance.data["parent"] output_range = task.outputRange() first_frame = int(item.data["item"].source().sourceIn()) last_frame = int(item.data["item"].source().sourceOut()) clip_duration = last_frame - first_frame + 1 difference = clip_duration - output_range[1] failure_message = ( 'Looks like you are rendering the clip length for the task ' 'rather than the cut length. If this is intended, just uncheck ' 'this validator after resetting, else adjust the export range in ' 'the "Handles" section of the export dialog.') assert difference, failure_message
class ValidateMayaParameters(api.InstancePlugin): """ Validates the existence of deadline parameters on node. """ order = inventory.get_order(__file__, "ValidateMayaParameters") label = "Parameters" families = ["deadline"] hosts = ["maya"] actions = [RepairParametersAction] targets = ["process.deadline"] def process(self, instance): node = instance[0] msg = "Could not find Chunk Size on node \"{0}\"".format(node) assert "deadlineChunkSize" in instance.data, msg msg = "Could not find Priority on node \"{0}\"".format(node) assert "deadlinePriority" in instance.data, msg msg = "Could not find Pool on node \"{0}\"".format(node) assert "deadlinePool" in instance.data, msg msg = "Could not find Concurrent Tasks on node \"{0}\"".format(node) assert "deadlineConcurrentTasks" in instance.data, msg
class ValidateMantraSettings(api.InstancePlugin): """ Validates mantra settings """ families = ["mantra"] order = inventory.get_order(__file__, "ValidateMantraSettings") label = "Mantra Settings" actions = [RepairMantraSettings] optional = True hosts = ["houdini"] def process(self, instance): node = instance[0] # Igonore local ifds if ("ifd" in instance.data["families"] and "local" in instance.data["families"]): return # When rendering locally we need to block, so Pyblish doesn"t execute # other plugins. When render on a remote, the block needs to be lifted. if "remote" in instance.data["families"]: msg = "Mantra needs to render in the background for remote " msg += "rendering. Disable \"Block Until Render Complete\" in " msg += "\"Driver\"." assert not node.parm("soho_foreground").eval(), msg else: msg = "Mantra needs to render in the foreground for local " msg += "rendering. Enable \"Block Until Render Complete\" in " msg += "\"Driver\"." assert node.parm("soho_foreground").eval(), msg
class ValidateShapeName(api.ContextPlugin): """ No two shapes can have the same name. """ order = inventory.get_order(__file__, "ValidateShapeName") label = "Shape Name" actions = [RepairShapeName] optional = True targets = ["process.local"] def process(self, context): import pymel.core validate = False valid_families = set(["mayaAscii", "mayaBinary", "alembic"]) for instance in context: if set(instance.data["families"]) & valid_families: if instance.data.get("publish", True): validate = True if not validate: return invalid_shapes = [] msg = "Duplicate shape names:" for shp in pymel.core.ls(type="mesh"): if "|" in shp.name(): invalid_shapes.append(shp) msg += "\n\n" + shp.name() assert not invalid_shapes, msg
class CollectPlayblastsPublish(api.ContextPlugin): """Collect all local processing write instances.""" order = inventory.get_order(__file__, "CollectPlayblastsPublish") label = "Playblasts Local" hosts = ["maya"] targets = ["default"] def process(self, context): import os for item in context.data["instances"]: # Skip any instances that is not valid. if "playblast" not in item.data.get("families", []): continue if not os.path.exists(item.data["output_path"]): continue instance = context.create_instance(item.data["name"]) for key, value in item.data.iteritems(): instance.data[key] = value instance.data["label"] = "{0} - {1}".format( instance.data["name"], os.path.basename(instance.data["output_path"])) instance.data["families"] += ["output"] for node in item: instance.add(node)
class ExtractGeometry(Extract): order = inventory.get_order(__file__, "ExtractGeometry") label = "Geometry" families = ["geometry", "local"] def process(self, instance): import os node = instance[0] node["writeCameras"].setValue(False) node["writePointClouds"].setValue(False) node["writeAxes"].setValue(False) file_path = node["file"].getValue() node["file"].setValue(instance.data["output_path"]) self.execute(instance) node["writeCameras"].setValue(True) node["writePointClouds"].setValue(True) node["writeAxes"].setValue(True) node["file"].setValue(file_path) # Validate output msg = "\"{0}\" didn't render.".format(instance.data["output_path"]) assert os.path.exists(instance.data["output_path"]), msg
class CollectSetsProcess(api.ContextPlugin): """Collect all local processing write instances.""" order = inventory.get_order(__file__, "CollectSetsProcess") label = "Sets Process" hosts = ["maya"] targets = ["process.local"] def process(self, context): for item in context.data["instances"]: # Skip any instances that is not valid. valid_families = [ "alembic", "mayaAscii", "mayaBinary", "camera", "geometry" ] if len(set(valid_families) & set(item.data["families"])) != 1: continue instance = context.create_instance(item.data["name"]) for key, value in item.data.iteritems(): instance.data[key] = value instance.data["families"] += ["local"] for node in item: instance.add(node)
class ValidateProcessing(api.ContextPlugin): """Validates whether there are any instances to process.""" order = inventory.get_order(__file__, "ValidateProcessing") optional = True label = "Data to Process" targets = ["default", "process"] hosts = ["nuke", "nukeassist", "maya"] def process(self, context): instances_to_process = [] instance_labels = "" for instance in context: # Ignore source family families = instance.data.get("families", []) families += [instance.data["family"]] if "source" in families: continue instance_labels += "\n\n" + instance.data.get("label", "name") if instance.data("publish", True): instances_to_process.append(instance) msg = ( "No nodes were enabled for processing. Please hit reset and choose" " a node to process in the left-hand list.\n\nPossible nodes to " "process:{0}".format(instance_labels)) assert instances_to_process, msg
class CollectVersion(api.ContextPlugin): """ Collects the version from the latest scene asset """ # offset to get current version from CollectSceneVersion order = inventory.get_order(__file__, "CollectVersion") label = "Ftrack Version" def process(self, context): session = context.data["ftrackSession"] task = context.data["ftrackTask"] if not task: return query = "select versions.version from Asset where parent.id is \"{0}\"" query += " and type.short is \"scene\" and name is \"{1}\"" asset = session.query(query.format(task["parent"]["id"], task["name"])).first() # Get current version current_version = context.data.get("version", 1) if asset: for version in asset["versions"]: if current_version < version["version"]: current_version = version["version"] context.data["version"] = current_version self.log.info("Current version: " + str(current_version))
class ValidateOutputPath(api.InstancePlugin): order = inventory.get_order(__file__, "ValidateOutputPath") label = "Output Path" families = ["img.*"] actions = [RepairOutputPathAction] def process(self, instance): current_path = instance.data["output"].replace("\\", "/") expected_path = self.get_expected_path(instance) msg = "Output path is not correct." msg += " Current: {0}".format(current_path) msg += " Expected: {0}".format(expected_path) assert current_path == expected_path, msg def get_expected_path(self, instance): import os current_path = instance.data["output"] path = instance.context.data["currentFile"] func = os.path.join basename = instance.data["name"].replace(" ", "_") + ".[####]" basename += os.path.splitext(current_path)[1] expected_path = func(os.path.dirname(path), "workspace", os.path.splitext(os.path.basename(path))[0], basename) return expected_path.replace("\\", "/")
class ValidateRenderCamera(api.InstancePlugin): """ Validates render camera """ order = inventory.get_order(__file__, "ValidateRenderCamera") families = ["renderlayer"] optional = True label = "Render Camera" def process(self, instance): import pymel.core # Validate non native camera active render_cameras = instance.data["cameras"] for c in pymel.core.ls(type="camera"): if c.renderable.get(): render_cameras.append(c) render_cameras = list(set(render_cameras)) msg = "No renderable camera selected." assert render_cameras, msg msg = "Can't render multiple cameras. " msg += "Please use a render layer instead" assert len(render_cameras) == 1, msg
class ValidateDatatype(api.InstancePlugin): """Validate output datatype matches with input.""" order = inventory.get_order(__file__, "ValidateDatatype") families = ["write"] label = "Datatype" optional = True targets = ["default", "process"] def process(self, instance): # Only validate these channels channels = [ "N_Object", "N_World", "P_Object", "P_World", "Pref", "UV", "velocity", "cryptomatte" ] valid_channels = [] for node_channel in instance[0].channels(): for channel in channels: if node_channel.startswith(channel): valid_channels.append(node_channel) if valid_channels: msg = ( "There are 32-bit channels: {0}.\n\nConsider changing the" " output to 32-bit to preserve data.".format(valid_channels) ) assert instance[0]["datatype"].value().startswith("32"), msg
class ExtractFtrackPath(api.InstancePlugin): """ Extract Ftrack path for Deadline """ order = inventory.get_order(__file__, "ExtractFtrackPath") families = ["deadline"] label = "Ftrack Path" def process(self, instance): import ftrack if not instance.context.has_data("ftrackData"): return job_data = {} plugin_data = {} if "deadlineData" in instance.data: job_data = instance.data["deadlineData"]["job"].copy() plugin_data = instance.data["deadlineData"]["plugin"].copy() # commenting to store full ftrack path comment = "" task = ftrack.Task(instance.context.data["ftrackData"]["Task"]["id"]) for p in reversed(task.getParents()): comment += p.getName() + "/" comment += task.getName() comment = "Ftrack path: \"{0}\"".format(comment) job_data["Comment"] = comment # setting data data = {"job": job_data, "plugin": plugin_data} instance.data["deadlineData"] = data
class ExtractReview(api.InstancePlugin): """Extract review hash value.""" order = inventory.get_order(__file__, "ExtractReview") label = "Review Hash" optional = True families = ["review"] hosts = ["nuke", "maya"] def md5(self, fname): import hashlib hash_md5 = hashlib.md5() with open(fname, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): hash_md5.update(chunk) return hash_md5.hexdigest() def process(self, instance): import os hash_value = self.md5(instance.data["output_path"]) md5_file = instance.data["output_path"].replace( os.path.splitext(instance.data["output_path"])[1], ".md5") with open(md5_file, "w") as the_file: the_file.write(hash_value)
class ValidateScenePath(api.InstancePlugin): """ Validates the path of the hiero file """ order = inventory.get_order(__file__, "ValidateScenePath") families = ["scene"] label = "Scene Path" actions = [RepairScenePathAction] def process(self, instance): import pipeline_schema # getting current work file work_path = instance.data["workPath"].lower() # expected path data = pipeline_schema.get_data() data["extension"] = "aep" version = 1 if instance.context.has_data("version"): version = instance.context.data("version") data["version"] = version file_path = pipeline_schema.get_path("task_work", data) # validating scene work path msg = "Scene path is not correct:" msg += "\n\nCurrent: %s" % (work_path) msg += "\n\nExpected: %s" % (file_path) assert file_path == work_path, msg
class ExtractAssetDataNukeStudio(api.ContextPlugin): """Changes the parent of the review component.""" order = inventory.get_order(__file__, "ExtractAssetDataNukeStudio") label = "Ftrack Link Review" optional = True hosts = ["nukestudio"] def process(self, context): data = {} for instance in context: families = [instance.data["family"]] families += instance.data.get("families", []) name = instance.data["name"].split("--")[-1] instance_data = data.get(name, {}) if "review" in families: instance_data["review"] = instance if "trackItem.ftrackEntity.shot" in families: instance_data["shot"] = instance data[name] = instance_data for name, instance_data in data.iteritems(): if not instance_data: continue asset_data = instance_data["review"].data.get("asset_data", {}) asset_data["parent"] = instance_data["shot"].data["entity"] instance_data["review"].data["asset_data"] = asset_data
class ExtractHobsoftScene(api.InstancePlugin): """ Extract work file to Hobsoft drive """ order = inventory.get_order(__file__, "ExtractHobsoftScene") families = ['scene'] label = 'Hobsoft Sync' def process(self, instance): import os import shutil current_file = instance.data('workPath') ftrack_data = instance.context.data('ftrackData') if ftrack_data['Project']['name'] != 'ethel_and_ernest': return sequence_name = ftrack_data['Sequence']['name'] shot_name = 'c' + ftrack_data['Shot']['name'].split('c')[1] filename = ftrack_data['Shot']['name'] + '_ee.tvpp' publish_file = os.path.join('B:\\', 'film', sequence_name, shot_name, 'tvpaint', filename) # create publish directory if not os.path.exists(os.path.dirname(publish_file)): os.makedirs(os.path.dirname(publish_file)) # copy work file to publish shutil.copy(current_file, publish_file)
class ValidateNames(api.InstancePlugin): """ Validate sequence, video track and track item names. When creating output directories with the name of an item, ending with a whitespace will fail the extraction. """ order = inventory.get_order(__file__, "ValidateNames") families = ["trackItem"] label = "Names" hosts = ["hiero"] def process(self, instance): item = instance[0] msg = "Track item \"{0}\" ends with a whitespace." assert not item.name().endswith(" "), msg.format(item.name()) msg = "Video track \"{0}\" ends with a whitespace." msg = msg.format(item.parent().name()) assert not item.parent().name().endswith(" "), msg msg = "Sequence \"{0}\" ends with a whitespace." msg = msg.format(item.parent().parent().name()) assert not item.parent().parent().name().endswith(" "), msg
class ExtractSuspended(api.InstancePlugin): """ Option to suspend Deadline job on submission """ order = inventory.get_order(__file__, "ExtractSuspended") label = "Suspend Deadline Job Initally" families = ["deadline"] active = False optional = True targets = ["process.deadline"] def process(self, instance): # getting job data job_data = {} plugin_data = {} if instance.has_data('deadlineData'): job_data = instance.data('deadlineData')['job'].copy() plugin_data = instance.data('deadlineData')['plugin'].copy() job_data["InitialStatus"] = "Suspended" instance.data["deadlineData"] = { "job": job_data, "plugin": plugin_data }
class ValidateResolvedPaths(api.ContextPlugin): """Validate there are no overlapping resolved paths.""" order = inventory.get_order(__file__, "ValidateResolvedPaths") label = "Resolved Paths" hosts = ["nukestudio"] def process(self, context): import os import collections paths = [] for instance in context: if "trackItem.task" == instance.data["family"]: paths.append( os.path.abspath( instance.data["task"].resolvedExportPath())) duplicates = [] for item, count in collections.Counter(paths).items(): if count > 1: duplicates.append(item) msg = "Duplicate output paths found: {0}".format(duplicates) assert not duplicates, msg
class ValidateVraySettings(api.InstancePlugin): """ Validates render layer settings. """ order = inventory.get_order(__file__, "ValidateVraySettings") optional = True families = ["vray"] label = "Vray Settings" actions = [RepairVraySettingsAction] hosts = ["maya"] def process(self, instance): import pymel settings = pymel.core.PyNode("vraySettings") # File name prefix current = settings.fileNamePrefix.get() expected = "<Layer>/<Scene>" msg = "File name prefix is incorrect. Current: \"{0}\"." msg += " Expected: \"{1}\"" assert expected == current, msg.format(current, expected) # Enable animation msg = "Expecting Animation to be \"Standard\"" assert settings.animType.get() == 1, msg # Frame name padding msg = "Expecting Frame name padding to be \"4\"" assert settings.fileNamePadding.get() == 4, msg
class CollectScene(api.ContextPlugin): """Collecting the scene from the context.""" # offset to get latest currentFile from context order = inventory.get_order(__file__, "CollectScene") label = "Source" targets = ["default", "process"] def process(self, context): import os current_file = context.data("currentFile") # Skip if current file is directory if os.path.isdir(current_file): return # create instance instance = context.create_instance(name=os.path.basename(current_file)) instance.data["families"] = ["scene"] instance.data["family"] = "source" instance.data["path"] = current_file label = "{0} - source".format(os.path.basename(current_file)) instance.data["label"] = label
class ValidateGroupNode(api.InstancePlugin): """Validates group node. Ensures none of the groups content is locally stored. """ order = inventory.get_order(__file__, "ValidateGroupNode") optional = True families = ["gizmo", "lut"] label = "Group Node" hosts = ["nuke", "nukeassist"] def process(self, instance): import os for node in instance[0].nodes(): # Skip input and output nodes if node.Class() in ["Input", "Output"]: continue # Get file path file_path = "" if node.Class() == "Vectorfield": file_path = node["vfield_file"].getValue() if node.Class() == "Read": file_path = node["file"].getValue() # Validate file path to not be local # Windows specifc msg = "Node \"{0}\" in group \"{1}\"".format( node["name"].getValue(), instance[0]["name"].getValue()) msg += ", has a local file path: \"{0}\"".format(file_path) assert "c:" != os.path.splitdrive(file_path)[0].lower(), msg
class CollectScene(api.ContextPlugin): """ Converts the path flag value to the current file in the context. """ order = inventory.get_order(__file__, "CollectScene") def process(self, context): context.data["currentFile"] = context.data("kwargs")["path"][0]
class ValidateTransforms(api.InstancePlugin): """ Freeze/Reset transforms. Ensure all meshes have their pivot at world zero, and their transforms are zero'ed out. """ order = inventory.get_order(__file__, "ValidateTransforms") families = ["mayaAscii", "mayaBinary", "alembic"] label = "Transforms" optional = True targets = ["process.local"] def process(self, instance): import pymel.core as pm attributes = { "tx": 0, "ty": 0, "tz": 0, "rx": 0, "ry": 0, "rz": 0, "sx": 1, "sy": 1, "sz": 1 } check = True for node in instance[0].members(): v = sum(node.getRotatePivot(space="world")) if v > 0.09: msg = "\"{0}\" pivot is not at world zero." self.log.error(msg.format(node.name())) check = False v = sum(node.getRotation(space="world")) if v != 0: msg = "\"{0}\" pivot axis is not aligned to world." self.log.error(msg.format(node.name())) check = False if sum(node.scale.get()) != 3: msg = "\"{0}\" scale is not neutral." self.log.error(msg.format(node.name())) check = False for key, value in attributes.iteritems(): attr = pm.PyNode("{0}.{1}".format(node.name(), key)) if attr.get() != value: msg = "Expected \"{0}\" value \"{1}\". Current \"{2}\"" self.log.error(msg.format(attr, value, attr.get())) check = False msg = "Transforms in the scene aren't reset." msg += " Please reset by \"Modify\" > \"Freeze Transformations\"," msg += " followed by \"Modify\" > \"Reset Transformations\"." assert check, msg
class OtherLinkSource(api.ContextPlugin): """Link the source version to all other versions.""" order = inventory.get_order(__file__, "OtherLinkSource") label = "Ftrack Link Source" def process(self, context): # Collect source assetversion source_version = None for instance in context: # Filter to source instance families = [instance.data["family"]] families += instance.data.get("families", []) if "source" not in families: continue # Get AssetVersion from published component for data in instance.data.get("ftrackComponentsList", []): if "component" not in data.keys(): continue source_version = data["component"]["version"] # Skip if no source version were published if source_version is None: return for instance in context: # Filter to non source instance families = [instance.data["family"]] + instance.data["families"] if "source" in families: continue # Get AssetVersion from published component covered_versions = [] for data in instance.data.get("ftrackComponentsList", []): if "component" not in data.keys(): continue # Prevent duplicate links being created with multiple # components on the same version. if data["component"]["version"]["id"] in covered_versions: continue covered_versions.append(data["component"]["version"]["id"]) existing_link = None for link in data["component"]["version"]["incoming_links"]: if link["from_id"] == source_version["id"]: existing_link = link if existing_link is None: context.data["ftrackSession"].create( "AssetVersionLink", { "from": source_version, "to": data["component"]["version"] }) context.data["ftrackSession"].commit()