+# # :copyright: Copyright (c) 2019 ftrack
+from ftrack_action_handler.action import BaseAction
+def get_filter_string(entity_ids):
+ '''Return a comma separated string of quoted ids from *entity_ids* list.'''
+ '"{0}"'.format(entity_id) for entity_id in entity_ids
+class UpdateFrameDurationAction(BaseAction):
+ '''Action to update the frame out metadata of reviewable components.'''
+ label = 'Update frame durations'
+ identifier = 'ftrack.update_frame_duration_action'
+ def discover(self, session, entities, event):
+ if not self.validate_selection(entities):
+ return super(UpdateFrameDurationAction, self).discover(
+ session, entities, event
+ def get_review_components(self, session, entities):
+ '''Return list of components in *entities*.'''
+ entity_groups = collections.defaultdict(list)
+ for entity_type, entity_id in entities:
+ entity_groups[entity_type].append(entity_id)
+ if entity_groups['Project']:
+ component_queries.append(
+ 'Component where (version.asset.parent.project.id in ({0}) or '
+ 'version.asset.parent.id in ({0}))'.format(
+ get_filter_string(entity_groups['Project'])
+ if entity_groups['TypedContext']:
+ component_queries.append(
+ 'Component where version.asset.parent.ancestors.id '
+ get_filter_string(entity_groups['TypedContext'])
+ if entity_groups['AssetVersion']:
+ component_queries.append(
+ 'Component where version_id in ({0})'.format(
+ get_filter_string(entity_groups['AssetVersion'])
+ if entity_groups['Component']:
+ component_queries.append(
+ 'Component where id in ({0})'.format(
+ get_filter_string(entity_groups['Component'])
+ for query_string in component_queries:
+ 'select version.link, metadata from {0} and file_type is ".mp4" and component_locations.location_id is "{1}" and metadata.key is "ftr_meta"'.format(
+ query_string, ftrack_api.symbol.SERVER_LOCATION_ID
+ 'Found {0} components in selection'.format(len(components))
+ return list(components)
+ def launch(self, session, entities, event):
+ '''Callback method for action.'''
+ u'Launching action with selection {0}'.format(entities)
+ # Validate selection and abort if not valid
+ if not self.validate_selection(entities):
+ 'Selection is not valid, aborting action'
+ values = event['data'].get('values', {})
+ self.update_frame_out(session, values)
+ # Commit failed, rollback session and re-raise.
+ 'message': 'Updated frame out on review components',
+ def update_frame_out(self, session, values):
+ '''Find and replace *find* and *replace* in *attribute* for *selection*.'''
+ for component_id, frame_out in values.iteritems():
+ component = session.get('Component', component_id)
+ meta = json.loads(component['metadata']['ftr_meta'])
+ meta['frameOut'] = float(frame_out)
+ component['metadata']['ftr_meta'] = json.dumps(meta)
+ self.logger.debug('Updated component {}: {}'.format(component_id, frame_out))
+ def validate_selection(self, entities):
+ '''Return True if *entities* is valid'''
+ # Replace with custom logic for validating selection.
+ # For example check the length or entityType of items in selection.
+ def interface(self, session, entities, event):
+ values = event['data'].get('values', {})
+ components = self.get_review_components(session, entities)
+ if len(components) > 100:
+ raise ValueError('Can not handle more than 100 components at once.')
+ 'value': '## Frame duration for reviewable components'
+ for component in components:
+ link = component['version']['link']
+ label = u' / '.join([item['name'] for item in link[-2:]])
+ value = json.loads(component['metadata']['ftr_meta'])['frameOut']
+ self.logger.exception('Failed to get frame out')
+ 'name': component['id'],
+def register(session, **kw):
+ '''Register plugin. Called when used as an plugin.'''
+ # Validate that session is an instance of ftrack_api.Session. If not,
+ # assume that register is being called from an old or incompatible API and
+ # return without doing anything.
+ if not isinstance(session, ftrack_api.session.Session):
+ action_handler = UpdateFrameDurationAction(session)
+ action_handler.register()
+if __name__ == '__main__':
+ logging.basicConfig(level=logging.INFO)
+ session = ftrack_api.Session()
+ logging.info('Registered actions and listening for events. Use Ctrl-C to abort.')
+ session.event_hub.wait()