diff --git a/pype/ftrack/actions/action_component_open.py b/pype/ftrack/actions/action_component_open.py index 1c67f4f22f..34ee752f61 100644 --- a/pype/ftrack/actions/action_component_open.py +++ b/pype/ftrack/actions/action_component_open.py @@ -24,8 +24,7 @@ class ComponentOpen(BaseAction): def discover(self, session, entities, event): ''' Validation ''' - - if len(entities) != 1 or entities[0].entity_type != 'Component': + if len(entities) != 1 or entities[0].entity_type != 'FileComponent': return False return True @@ -43,10 +42,14 @@ class ComponentOpen(BaseAction): } # Get component filepath + # TODO with locations it will be different??? fpath = entity['component_locations'][0]['resource_identifier'] + items = fpath.split(os.sep) + items.pop(-1) + fpath = os.sep.join(items) - if os.path.isfile(fpath): - if sys.platform == 'win': # windows + if os.path.isdir(fpath): + if 'win' in sys.platform: # windows subprocess.Popen('explorer "%s"' % fpath) elif sys.platform == 'darwin': # macOS subprocess.Popen(['open', fpath]) @@ -63,7 +66,7 @@ class ComponentOpen(BaseAction): return { 'success': True, - 'message': 'Component Opened' + 'message': 'Component folder Opened' } diff --git a/pype/ftrack/actions/action_syncToAvalon.py b/pype/ftrack/actions/action_syncToAvalon.py index a557a169ab..3808f5c8ff 100644 --- a/pype/ftrack/actions/action_syncToAvalon.py +++ b/pype/ftrack/actions/action_syncToAvalon.py @@ -4,7 +4,6 @@ import sys import argparse import logging import os -import json import ftrack_api from ftrack_action_handler import BaseAction @@ -161,9 +160,7 @@ class SyncToAvalon(BaseAction): job = session.create('Job', { 'user': user, 'status': 'running', - 'data': json.dumps({ - 'description': 'Synch Ftrack to Avalon.' - }) + 'data': {'description': 'Synch Ftrack to Avalon.'} }) try: diff --git a/pype/ftrack/actions/action_test.py b/pype/ftrack/actions/action_test.py index 08d7704825..a9f2aba0eb 100644 --- a/pype/ftrack/actions/action_test.py +++ b/pype/ftrack/actions/action_test.py @@ -33,7 +33,22 @@ class TestAction(BaseAction): def launch(self, session, entities, event): for entity in entities: - print("TEST") + index = 0 + name = entity['components'][index]['name'] + filetype = entity['components'][index]['file_type'] + path = entity['components'][index]['component_locations'][0]['resource_identifier'] + + # entity['components'][index]['component_locations'][0]['resource_identifier'] = r"C:\Users\jakub.trllo\Desktop\test\exr\int_c022_lighting_v001_main_AO.%04d.exr" + location = entity['components'][0]['component_locations'][0]['location'] + component = entity['components'][0] + + + # print(location.get_filesystem_path(component)) + + # for k in p: + # print(100*"-") + # print(k) + # print(p[k]) return True diff --git a/pype/ftrack/actions/djvview.py b/pype/ftrack/actions/djvview.py new file mode 100644 index 0000000000..87213c1308 --- /dev/null +++ b/pype/ftrack/actions/djvview.py @@ -0,0 +1,383 @@ +import logging +import subprocess +import sys +import pprint +import os +import getpass +import re +from operator import itemgetter +import ftrack_api + + +class DJVViewAction(object): + """Launch DJVView action.""" + identifier = "djvview-launch-action" + # label = "DJV View" + # icon = "http://a.fsdn.com/allura/p/djv/icon" + + def __init__(self, session): + '''Expects a ftrack_api.Session instance''' + + self.logger = logging.getLogger( + '{0}.{1}'.format(__name__, self.__class__.__name__) + ) + + if self.identifier is None: + raise ValueError( + 'Action missing identifier.' + ) + + self.session = session + + def is_valid_selection(self, event): + selection = event["data"].get("selection", []) + + if not selection: + return + + entityType = selection[0]["entityType"] + + if entityType not in ["assetversion", "task"]: + return False + + return True + + def discover(self, event): + """Return available actions based on *event*. """ + + if not self.is_valid_selection(event): + return + + items = [] + applications = self.get_applications() + applications = sorted( + applications, key=lambda application: application["label"] + ) + + for application in applications: + self.djv_path = application.get("path", None) + applicationIdentifier = application["identifier"] + label = application["label"] + items.append({ + "actionIdentifier": self.identifier, + "label": label, + "variant": application.get("variant", None), + "description": application.get("description", None), + "icon": application.get("icon", "default"), + "applicationIdentifier": applicationIdentifier + }) + + return { + "items": items + } + + def register(self): + '''Registers the action, subscribing the the discover and launch topics.''' + self.session.event_hub.subscribe( + 'topic=ftrack.action.discover and source.user.username={0}'.format( + self.session.api_user + ), self.discover + ) + + self.session.event_hub.subscribe( + 'topic=ftrack.action.launch and data.actionIdentifier={0} and source.user.username={1}'.format( + self.identifier, + self.session.api_user + ), + self.launch + ) + print("----- action - <" + self.__class__.__name__ + "> - Has been registered -----") + + def get_applications(self): + applications = [] + + label="DJVView {version}" + versionExpression=re.compile(r"(?P\d+.\d+.\d+)") + applicationIdentifier="djvview" + description="DJV View Launcher" + icon="http://a.fsdn.com/allura/p/djv/icon" + expression = [] + if sys.platform == "win32": + expression = ["C:\\", "Program Files", "djv-\d.+", + "bin", "djv_view.exe"] + + elif sys.platform == "darwin": + expression = ["Application", "DJV.app", "Contents", "MacOS", "DJV"] + ## Linuxs + else: + expression = ["usr", "local", "djv", "djv_view"] + + pieces = expression[:] + start = pieces.pop(0) + + if sys.platform == 'win32': + # On Windows C: means current directory so convert roots that look + # like drive letters to the C:\ format. + if start and start[-1] == ':': + start += '\\' + + if not os.path.exists(start): + raise ValueError( + 'First part "{0}" of expression "{1}" must match exactly to an ' + 'existing entry on the filesystem.' + .format(start, expression) + ) + + + expressions = list(map(re.compile, pieces)) + expressionsCount = len(expression)-1 + + for location, folders, files in os.walk(start, topdown=True, followlinks=True): + level = location.rstrip(os.path.sep).count(os.path.sep) + expression = expressions[level] + + if level < (expressionsCount - 1): + # If not yet at final piece then just prune directories. + folders[:] = [folder for folder in folders + if expression.match(folder)] + else: + # Match executable. Note that on OSX executable might equate to + # a folder (.app). + for entry in folders + files: + match = expression.match(entry) + if match: + # Extract version from full matching path. + path = os.path.join(start, location, entry) + versionMatch = versionExpression.search(path) + if versionMatch: + version = versionMatch.group('version') + + applications.append({ + 'identifier': applicationIdentifier.format( + version=version + ), + 'path': path, + 'version': version, + 'label': label.format(version=version), + 'icon': icon, + # 'variant': variant.format(version=version), + 'description': description + }) + else: + self.logger.debug( + 'Discovered application executable, but it ' + 'does not appear to o contain required version ' + 'information: {0}'.format(path) + ) + + # Don't descend any further as out of patterns to match. + del folders[:] + + return applications + + def translate_event(self, session, event): + '''Return *event* translated structure to be used with the API.''' + + selection = event['data'].get('selection', []) + + entities = list() + for entity in selection: + entities.append( + (session.get(self.get_entity_type(entity), entity.get('entityId'))) + ) + + return entities + + def get_entity_type(self, entity): + entity_type = entity.get('entityType').replace('_', '').lower() + + for schema in self.session.schemas: + alias_for = schema.get('alias_for') + + if ( + alias_for and isinstance(alias_for, str) and + alias_for.lower() == entity_type + ): + return schema['id'] + + for schema in self.session.schemas: + if schema['id'].lower() == entity_type: + return schema['id'] + + raise ValueError( + 'Unable to translate entity type: {0}.'.format(entity_type) + ) + + def launch(self, event): + """Callback method for DJVView action.""" + session = self.session + entities = self.translate_event(session, event) + + # Launching application + if "values" in event["data"]: + + filename = event['data']['values']['path'] + + # TODO These should be obtained in another way + start = 375 + end = 379 + fps = 24 + # TODO issequence is probably already built-in validation in ftrack + isseq = re.findall( '%[0-9]*d', filename ) + if len(isseq) > 0: + padding = re.findall( '%[0-9]*d', filename ).pop() + range = ( padding % start ) + '-' + ( padding % end ) + filename = re.sub( '%[0-9]*d', range, filename ) + + + cmd = [] + # DJV path + cmd.append( os.path.normpath( self.djv_path ) ) + ### DJV Options Start ################################################ + # cmd.append( '-file_layer (value)' ) #layer name + cmd.append( '-file_proxy 1/2' ) #Proxy scale: 1/2, 1/4, 1/8 + cmd.append( '-file_cache True' ) # Cache: True, False. + # cmd.append( '-window_fullscreen' ) #Start in full screen + # cmd.append("-window_toolbar False") # Toolbar controls: False, True. + # cmd.append("-window_playbar False") # Window controls: False, True. + # cmd.append("-view_grid None") # Grid overlay: None, 1x1, 10x10, 100x100. + # cmd.append("-view_hud True") # Heads up display: True, False. + cmd.append("-playback Forward") # Playback: Stop, Forward, Reverse. + # cmd.append("-playback_frame (value)") # Frame. + cmd.append("-playback_speed " + str(fps)) + # cmd.append("-playback_timer (value)") # Timer: Sleep, Timeout. Value: Sleep. + # cmd.append("-playback_timer_resolution (value)") # Timer resolution (seconds): 0.001. + cmd.append("-time_units Frames") # Time units: Timecode, Frames. + ### DJV Options End ################################################## + + # PATH TO COMPONENT + cmd.append( os.path.normpath( filename ) ) + + # Run DJV with these commands + subprocess.Popen( ' '.join( cmd ) ) + + return { + 'success': True, + 'message': 'DJV View started.' + } + + if 'items' not in event["data"]: + event["data"]['items'] = [] + + try: + for entity in entities: + versions = [] + allowed_types = ["img", "mov", "exr"] + + if entity.entity_type.lower() == "assetversion": + if entity['components'][0]['file_type'] in allowed_types: + versions.append(entity) + + if entity.entity_type.lower() == "task": + # AssetVersions are obtainable only from shot! + shotentity = entity['parent'] + + for asset in shotentity['assets']: + for version in asset['versions']: + # Get only AssetVersion of selected task + if version['task']['id'] != entity['id']: + continue + # Get only components with allowed type + if version['components'][0]['file_type'] in allowed_types: + versions.append(version) + + # Raise error if no components were found + if len(versions) < 1: + raise ValueError('There are no Asset Versions to open.') + + for version in versions: + for component in version['components']: + label = "v{0} - {1} - {2}" + + label = label.format( + str(version['version']).zfill(3), + version['asset']['type']['name'], + component['name'] + ) + + try: + # TODO This is proper way to get filepath!!! + # NOT WORKING RIGHT NOW + location = component['component_locations'][0]['location'] + file_path = location.get_filesystem_path(component) + # if component.isSequence(): + # if component.getMembers(): + # frame = int(component.getMembers()[0].getName()) + # file_path = file_path % frame + except: + # This is NOT proper way + file_path = component['component_locations'][0]['resource_identifier'] + + event["data"]["items"].append( + {"label": label, "value": file_path} + ) + + except Exception as e: + return { + 'success': False, + 'message': str(e) + } + + return { + "items": [ + { + "label": "Items to view", + "type": "enumerator", + "name": "path", + "data": sorted( + event["data"]['items'], + key=itemgetter("label"), + reverse=True + ) + } + ] + } + + + + +def register(session, **kw): + """Register hooks.""" + if not isinstance(session, ftrack_api.session.Session): + return + + action = DJVViewAction(session) + action.register() + +def main(arguments=None): + '''Set up logging and register action.''' + if arguments is None: + arguments = [] + + parser = argparse.ArgumentParser() + # Allow setting of logging level from arguments. + loggingLevels = {} + for level in ( + logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, + logging.ERROR, logging.CRITICAL + ): + loggingLevels[logging.getLevelName(level).lower()] = level + + parser.add_argument( + '-v', '--verbosity', + help='Set the logging output verbosity.', + choices=loggingLevels.keys(), + default='info' + ) + namespace = parser.parse_args(arguments) + + # Set up basic logging + logging.basicConfig(level=loggingLevels[namespace.verbosity]) + + session = ftrack_api.Session() + register(session) + + # Wait for events + logging.info( + 'Registered actions and listening for events. Use Ctrl-C to abort.' + ) + session.event_hub.wait() + + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) diff --git a/pype/ftrack/actions/djvview_launch.py b/pype/ftrack/actions/djvview_launch.py deleted file mode 100644 index 6b3e491cff..0000000000 --- a/pype/ftrack/actions/djvview_launch.py +++ /dev/null @@ -1,115 +0,0 @@ -import os -import logging -import json - -import ftrack -import ftrack_api -import clique -import ftrack_template - -log = logging.getLogger(__name__) - - -def modify_launch(session, event): - """Modify the application launch command with potential files to open""" - - # Collect published paths - data = {} - for item in event["data"].get("selection", []): - - versions = [] - - if entity.entity_type == "Assetversion": - version = ftrack.AssetVersion(item["entityId"]) - if version.getAsset().getType().getShort() in ["img", "mov"]: - versions.append(version) - - # Add latest version of "img" and "mov" type from tasks. - if item["entityType"] == "task": - task = ftrack.Task(item["entityId"]) - for asset in task.getAssets(assetTypes=["img", "mov"]): - versions.append(asset.getVersions()[-1]) - - for version in versions: - for component in version.getComponents(): - component_list = data.get(component.getName(), []) - component_list.append(component) - data[component.getName()] = component_list - - label = "v{0} - {1} - {2}" - label = label.format( - str(version.getVersion()).zfill(3), - version.getAsset().getType().getName(), - component.getName() - ) - - file_path = component.getFilesystemPath() - if component.isSequence(): - if component.getMembers(): - frame = int(component.getMembers()[0].getName()) - file_path = file_path % frame - - event["data"]["items"].append( - {"label": label, "value": file_path} - ) - - # Collect workspace paths - session = ftrack_api.Session() - for item in event["data"].get("selection", []): - if item["entityType"] == "task": - templates = ftrack_template.discover_templates() - task_area, template = ftrack_template.format( - {}, templates, entity=session.get("Task", item["entityId"]) - ) - - # Traverse directory and collect collections from json files. - instances = [] - for root, dirs, files in os.walk(task_area): - for f in files: - if f.endswith(".json"): - with open(os.path.join(root, f)) as json_data: - for data in json.load(json_data): - instances.append(data) - - check_values = [] - for data in instances: - if "collection" in data: - - # Check all files in the collection - collection = clique.parse(data["collection"]) - for f in list(collection): - if not os.path.exists(f): - collection.remove(f) - - if list(collection): - value = list(collection)[0] - - # Check if value already exists - if value in check_values: - continue - else: - check_values.append(value) - - # Add workspace items - event["data"]["items"].append( - { - "label": "{0} - {1}".format( - data["name"], - os.path.basename(collection.format()) - ), - "value": value - } - ) - - return event - - -def register(session, **kw): - # Validate session - if not isinstance(session, ftrack_api.session.Session): - return - - session.event_hub.subscribe( - 'topic=djvview.launch', - modify_launch(session) - )