diff --git a/client/ayon_hiero/api/__init__.py b/client/ayon_hiero/api/__init__.py index 099db14..e93264c 100644 --- a/client/ayon_hiero/api/__init__.py +++ b/client/ayon_hiero/api/__init__.py @@ -8,10 +8,9 @@ ) from .pipeline import ( + HieroHost, launch_workfiles_app, ls, - install, - uninstall, reload_config, containerise, publish, @@ -22,7 +21,7 @@ ) from .constants import ( - OPENPYPE_TAG_NAME, + AYON_TAG_NAME, DEFAULT_SEQUENCE_NAME, DEFAULT_BIN_NAME ) @@ -35,15 +34,15 @@ get_timeline_selection, get_current_track, get_track_item_tags, - get_track_openpype_tag, - set_track_openpype_tag, - get_track_openpype_data, + get_track_ayon_tag, + set_track_ayon_tag, + get_track_ayon_data, get_track_item_pype_tag, set_track_item_pype_tag, get_track_item_pype_data, - get_trackitem_openpype_tag, - set_trackitem_openpype_tag, - get_trackitem_openpype_data, + get_trackitem_ayon_tag, + set_trackitem_ayon_tag, + get_trackitem_ayon_data, set_publish_attribute, get_publish_attribute, imprint, @@ -66,11 +65,10 @@ ) __all__ = [ - # avalon pipeline module + # pipeline module + "HieroHost", "launch_workfiles_app", "ls", - "install", - "uninstall", "reload_config", "containerise", "publish", @@ -88,7 +86,7 @@ "work_root", # Constants - "OPENPYPE_TAG_NAME", + "AYON_TAG_NAME", "DEFAULT_SEQUENCE_NAME", "DEFAULT_BIN_NAME", @@ -100,12 +98,12 @@ "get_timeline_selection", "get_current_track", "get_track_item_tags", - "get_track_openpype_tag", - "set_track_openpype_tag", - "get_track_openpype_data", - "get_trackitem_openpype_tag", - "set_trackitem_openpype_tag", - "get_trackitem_openpype_data", + "get_track_ayon_tag", + "set_track_ayon_tag", + "get_track_ayon_data", + "get_trackitem_ayon_tag", + "set_trackitem_ayon_tag", + "get_trackitem_ayon_data", "set_publish_attribute", "get_publish_attribute", "imprint", diff --git a/client/ayon_hiero/api/constants.py b/client/ayon_hiero/api/constants.py index 61a780a..9f7642f 100644 --- a/client/ayon_hiero/api/constants.py +++ b/client/ayon_hiero/api/constants.py @@ -1,3 +1,8 @@ -OPENPYPE_TAG_NAME = "openpypeData" -DEFAULT_SEQUENCE_NAME = "openpypeSequence" -DEFAULT_BIN_NAME = "openpypeBin" +AYON_TAG_NAME = "AYONdata" +LEGACY_OPENPYPE_TAG_NAME = "openpypeData" + +AYON_WORKFILE_TAG_BIN = "AYONdata" +AYON_WORKFILE_TAG_NAME = "workfile" + +DEFAULT_SEQUENCE_NAME = "AYONsequence" +DEFAULT_BIN_NAME = "AYONbin" diff --git a/client/ayon_hiero/api/lib.py b/client/ayon_hiero/api/lib.py index 2a6038f..d0c59a1 100644 --- a/client/ayon_hiero/api/lib.py +++ b/client/ayon_hiero/api/lib.py @@ -31,7 +31,7 @@ from ayon_core.lib import Logger from . import tags from .constants import ( - OPENPYPE_TAG_NAME, + AYON_TAG_NAME, DEFAULT_SEQUENCE_NAME, DEFAULT_BIN_NAME ) @@ -331,7 +331,7 @@ def _validate_type_track_item(): def get_track_item_tags(track_item): """ - Get track item tags excluded openpype tag + Get track item tags excluding AYON tag Attributes: trackItem (hiero.core.TrackItem): hiero object @@ -345,10 +345,10 @@ def get_track_item_tags(track_item): if not _tags: return [] - # collect all tags which are not openpype tag + # collect all tags which are not AYON tag returning_tag_data.extend( tag for tag in _tags - if tag.name() != OPENPYPE_TAG_NAME + if tag.name() != AYON_TAG_NAME ) return returning_tag_data @@ -359,9 +359,9 @@ def _get_tag_unique_hash(): return secrets.token_hex(nbytes=4) -def set_track_openpype_tag(track, data=None): +def set_track_ayon_tag(track, data=None): """ - Set openpype track tag to input track object. + Set AYON track tag to input track object. Attributes: track (hiero.core.VideoTrack): hiero object @@ -374,12 +374,12 @@ def set_track_openpype_tag(track, data=None): # basic Tag's attribute tag_data = { "editable": "0", - "note": "OpenPype data container", - "icon": "openpype_icon.png", + "note": "AYON data container", + "icon": "AYON_icon.png", "metadata": dict(data.items()) } # get available pype tag if any - _tag = get_track_openpype_tag(track) + _tag = get_track_ayon_tag(track) if _tag: # it not tag then create one @@ -388,7 +388,7 @@ def set_track_openpype_tag(track, data=None): # if pype tag available then update with input data tag = tags.create_tag( "{}_{}".format( - OPENPYPE_TAG_NAME, + AYON_TAG_NAME, _get_tag_unique_hash() ), tag_data @@ -399,9 +399,9 @@ def set_track_openpype_tag(track, data=None): return tag -def get_track_openpype_tag(track): +def get_track_ayon_tag(track): """ - Get pype track item tag created by creator or loader plugin. + Get AYON track item tag created by creator or loader plugin. Attributes: trackItem (hiero.core.TrackItem): hiero object @@ -415,36 +415,42 @@ def get_track_openpype_tag(track): return None for tag in _tags: # return only correct tag defined by global name - if OPENPYPE_TAG_NAME in tag.name(): + if AYON_TAG_NAME in tag.name(): return tag -def get_track_openpype_data(track, container_name=None): +def get_track_ayon_data(track, container_name=None): """ - Get track's openpype tag data. + Get track's AYON tag data. Attributes: trackItem (hiero.core.VideoTrack): hiero object Returns: - dict: data found on pype tag + dict: data found on the AYON tag """ return_data = {} # get pype data tag from track item - tag = get_track_openpype_tag(track) + tag = get_track_ayon_tag(track) if not tag: return None # get tag metadata attribute tag_data = deepcopy(dict(tag.metadata())) + if tag_data.get("tag.json_metadata"): + tag_data = json.loads(tag_data["tag.json_metadata"]) + ignore_names = {"applieswhole", "note", "label"} for obj_name, obj_data in tag_data.items(): obj_name = obj_name.replace("tag.", "") - if obj_name in ["applieswhole", "note", "label"]: + if obj_name in ignore_names: continue - return_data[obj_name] = json.loads(obj_data) + if isinstance(obj_data, dict): + return_data[obj_name] = obj_data + else: + return_data[obj_name] = json.loads(obj_data) return ( return_data[container_name] @@ -453,30 +459,31 @@ def get_track_openpype_data(track, container_name=None): ) -@deprecated("ayon_hiero.api.lib.get_trackitem_openpype_tag") +@deprecated("ayon_hiero.api.lib.get_trackitem_ayon_tag") def get_track_item_pype_tag(track_item): # backward compatibility alias - return get_trackitem_openpype_tag(track_item) + return get_trackitem_ayon_tag(track_item) -@deprecated("ayon_hiero.api.lib.set_trackitem_openpype_tag") +@deprecated("ayon_hiero.api.lib.set_trackitem_ayon_tag") def set_track_item_pype_tag(track_item, data=None): # backward compatibility alias - return set_trackitem_openpype_tag(track_item, data) + return set_trackitem_ayon_tag(track_item, data) -@deprecated("ayon_hiero.api.lib.get_trackitem_openpype_data") +@deprecated("ayon_hiero.api.lib.get_trackitem_ayon_data") def get_track_item_pype_data(track_item): # backward compatibility alias - return get_trackitem_openpype_data(track_item) + return get_trackitem_ayon_data(track_item) -def get_trackitem_openpype_tag(track_item): +def get_trackitem_ayon_tag(track_item, tag_name=AYON_TAG_NAME): """ Get pype track item tag created by creator or loader plugin. Attributes: trackItem (hiero.core.TrackItem): hiero object + tag_name (str): The tag name. Returns: hiero.core.Tag: hierarchy, orig clip attributes @@ -487,13 +494,13 @@ def get_trackitem_openpype_tag(track_item): return None for tag in _tags: # return only correct tag defined by global name - if OPENPYPE_TAG_NAME in tag.name(): + if tag_name in tag.name(): return tag -def set_trackitem_openpype_tag(track_item, data=None): +def set_trackitem_ayon_tag(track_item, data=None): """ - Set openpype track tag to input track object. + Set AYON track tag to input track object. Attributes: track (hiero.core.VideoTrack): hiero object @@ -506,20 +513,20 @@ def set_trackitem_openpype_tag(track_item, data=None): # basic Tag's attribute tag_data = { "editable": "0", - "note": "OpenPype data container", - "icon": "openpype_icon.png", + "note": "AYON data container", + "icon": "AYON_icon.png", "metadata": dict(data.items()) } # get available pype tag if any - _tag = get_trackitem_openpype_tag(track_item) + _tag = get_trackitem_ayon_tag(track_item) if _tag: - # it not tag then create one + # if pype tag available then update with input data tag = tags.update_tag(_tag, tag_data) else: - # if pype tag available then update with input data + # it not tag then create one tag = tags.create_tag( "{}_{}".format( - OPENPYPE_TAG_NAME, + AYON_TAG_NAME, _get_tag_unique_hash() ), tag_data @@ -530,9 +537,9 @@ def set_trackitem_openpype_tag(track_item, data=None): return tag -def get_trackitem_openpype_data(track_item): +def get_trackitem_ayon_data(track_item): """ - Get track item's pype tag data. + Get track item's AYON tag data. Attributes: trackItem (hiero.core.TrackItem): hiero object @@ -542,13 +549,16 @@ def get_trackitem_openpype_data(track_item): """ data = {} # get pype data tag from track item - tag = get_trackitem_openpype_tag(track_item) + tag = get_trackitem_ayon_tag(track_item) if not tag: return None # get tag metadata attribute tag_data = deepcopy(dict(tag.metadata())) + if tag_data.get("tag.json_metadata"): + return json.loads(tag_data.get("tag.json_metadata")) + # convert tag metadata to normal keys names and values to correct types for k, v in tag_data.items(): key = k.replace("tag.", "") @@ -567,8 +577,7 @@ def get_trackitem_openpype_data(track_item): value = v else: value = ast.literal_eval(v) - except (ValueError, SyntaxError) as msg: - log.warning(msg) + except (ValueError, SyntaxError): value = v data[key] = value @@ -595,7 +604,7 @@ def imprint(track_item, data=None): """ data = data or {} - tag = set_trackitem_openpype_tag(track_item, data) + tag = set_trackitem_ayon_tag(track_item, data) # add publish attribute set_publish_attribute(tag, True) @@ -609,8 +618,15 @@ def set_publish_attribute(tag, value): value (bool): True or False """ tag_data = tag.metadata() - # set data to the publish attribute - tag_data.setValue("tag.publish", str(value)) + try: + tag_json_data = tag_data["tag.json_metadata"] + metadata = json.loads(tag_json_data) + + except (KeyError, json.JSONDecodeError): # missing key or invalid tag data + metadata = {} + + metadata["publish"] = value + tag_data.setValue("tag.json_metadata", json.dumps(metadata)) def get_publish_attribute(tag): @@ -618,13 +634,21 @@ def get_publish_attribute(tag): Attribute: tag (hiero.core.Tag): a tag object - value (bool): True or False + + Returns: + object: data found on publish attribute or None """ tag_data = tag.metadata() + # get data to the publish attribute - value = tag_data.value("tag.publish") - # return value converted to bool value. Atring is stored in tag. - return ast.literal_eval(value) + try: + tag_json_data = tag_data["tag.json_metadata"] + tag_data = json.loads(tag_json_data) + + except (KeyError, json.JSONDecodeError): # missing key or invalid tag data + return None + + return tag_data["publish"] def sync_avalon_data_to_workfile(): @@ -1272,7 +1296,7 @@ def sync_clip_name_to_data_asset(track_items_list): # get name and data ti_name = track_item.name() - data = get_trackitem_openpype_data(track_item) + data = get_trackitem_ayon_data(track_item) # ignore if no data on the clip or not publish instance if not data: @@ -1286,10 +1310,10 @@ def sync_clip_name_to_data_asset(track_items_list): if data["asset"] != ti_name: data["asset"] = ti_name # remove the original tag - tag = get_trackitem_openpype_tag(track_item) + tag = get_trackitem_ayon_tag(track_item) track_item.removeTag(tag) # create new tag with updated data - set_trackitem_openpype_tag(track_item, data) + set_trackitem_ayon_tag(track_item, data) print("asset was changed in clip: {}".format(ti_name)) diff --git a/client/ayon_hiero/api/menu.py b/client/ayon_hiero/api/menu.py index 632b11c..1f39898 100644 --- a/client/ayon_hiero/api/menu.py +++ b/client/ayon_hiero/api/menu.py @@ -54,7 +54,7 @@ def menu_install(): """ from . import ( - publish, launch_workfiles_app, reload_config, + launch_workfiles_app, reload_config, apply_colorspace_project, apply_colorspace_clips ) from .lib import get_main_window @@ -98,13 +98,13 @@ def menu_install(): creator_action = menu.addAction("Create...") creator_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png")) creator_action.triggered.connect( - lambda: host_tools.show_creator(parent=main_window) + lambda: host_tools.show_publisher(tab="create", parent=main_window) ) publish_action = menu.addAction("Publish...") publish_action.setIcon(QtGui.QIcon("icons:Output.png")) publish_action.triggered.connect( - lambda *args: publish(hiero.ui.mainWindow()) + lambda *args: host_tools.show_publisher(tab="publish", parent=main_window) ) loader_action = menu.addAction("Load...") diff --git a/client/ayon_hiero/api/otio/hiero_export.py b/client/ayon_hiero/api/otio/hiero_export.py index de547f3..3ca280a 100644 --- a/client/ayon_hiero/api/otio/hiero_export.py +++ b/client/ayon_hiero/api/otio/hiero_export.py @@ -3,7 +3,6 @@ import os import re -import ast import opentimelineio as otio from . import utils import hiero.core @@ -239,13 +238,7 @@ def create_otio_markers(otio_item, item): for key, value in tag.metadata().dict().items(): _key = key.replace("tag.", "") - try: - # capture exceptions which are related to strings only - _value = ast.literal_eval(value) - except (ValueError, SyntaxError): - _value = value - - metadata.update({_key: _value}) + metadata.update({_key: value}) # Store the source item for future import assignment metadata['hiero_source_type'] = item.__class__.__name__ diff --git a/client/ayon_hiero/api/otio/utils.py b/client/ayon_hiero/api/otio/utils.py index f7cb58f..f1b3cff 100644 --- a/client/ayon_hiero/api/otio/utils.py +++ b/client/ayon_hiero/api/otio/utils.py @@ -1,4 +1,6 @@ import re +import json + import opentimelineio as otio @@ -78,3 +80,36 @@ def get_rate(item): return rate return round(rate, 4) + + +def get_marker_from_clip_index(otio_timeline, clip_index): + """ + Args: + otio_timeline (otio.Timeline): The otio timeline to inspect + clip_index (int): The clip index metadata to retrieve. + + Returns: + tuple(otio.Clip, otio.Marker): The associated clip and marker + or (None, None) + """ + try: # opentimelineio >= 0.16.0 + all_clips = otio_timeline.find_clips() + except AttributeError: # legacy + all_clips = otio_timeline.each_clip() + + # Retrieve otioClip from parent context otioTimeline + # See collect_current_project + for otio_clip in all_clips: + for marker in otio_clip.markers: + + try: + json_metadata = marker.metadata["json_metadata"] + except KeyError: + continue + + else: + metadata = json.loads(json_metadata) + if metadata.get("clip_index") == clip_index: + return otio_clip, marker + + return None, None diff --git a/client/ayon_hiero/api/pipeline.py b/client/ayon_hiero/api/pipeline.py index 6815c7a..94199eb 100644 --- a/client/ayon_hiero/api/pipeline.py +++ b/client/ayon_hiero/api/pipeline.py @@ -9,20 +9,33 @@ import hiero from pyblish import api as pyblish +from ayon_core.host import ( + HostBase, + IWorkfileHost, + ILoadHost, + IPublishHost +) from ayon_core.lib import Logger from ayon_core.pipeline import ( schema, register_creator_plugin_path, register_loader_plugin_path, - deregister_creator_plugin_path, - deregister_loader_plugin_path, AVALON_CONTAINER_ID, AYON_CONTAINER_ID, ) + from ayon_core.tools.utils import host_tools + from ayon_hiero import HIERO_ADDON_ROOT from . import lib, menu, events +from .workio import ( + open_file, + save_file, + file_extensions, + has_unsaved_changes, + current_file +) log = Logger.get_logger(__name__) @@ -35,42 +48,56 @@ AVALON_CONTAINERS = ":AVALON_CONTAINERS" -def install(): - """Installing Hiero integration.""" - # adding all events - events.register_events() +class HieroHost( + HostBase, IWorkfileHost, ILoadHost, IPublishHost +): + name = "hiero" - log.info("Registering Hiero plug-ins..") - pyblish.register_host("hiero") - pyblish.register_plugin_path(PUBLISH_PATH) - register_loader_plugin_path(LOAD_PATH) - register_creator_plugin_path(CREATE_PATH) + def open_workfile(self, filepath): + return open_file(filepath) - # register callback for switching publishable - pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) + def save_workfile(self, filepath=None): + return save_file(filepath) - # install menu - menu.menu_install() - menu.add_scripts_menu() + def get_current_workfile(self): + return current_file() - # register hiero events - events.register_hiero_events() + def workfile_has_unsaved_changes(self): + return has_unsaved_changes() + def get_workfile_extensions(self): + return file_extensions() -def uninstall(): - """ - Uninstalling Hiero integration for avalon + def get_containers(self): + return ls() - """ - log.info("Deregistering Hiero plug-ins..") - pyblish.deregister_host("hiero") - pyblish.deregister_plugin_path(PUBLISH_PATH) - deregister_loader_plugin_path(LOAD_PATH) - deregister_creator_plugin_path(CREATE_PATH) + def install(self): + """Installing all requirements for hiero host""" + + # adding all events + events.register_events() + + log.info("Registering Hiero plug-ins..") + pyblish.register_host("hiero") + pyblish.register_plugin_path(PUBLISH_PATH) + register_loader_plugin_path(LOAD_PATH) + register_creator_plugin_path(CREATE_PATH) + + # install menu + menu.menu_install() + menu.add_scripts_menu() + + # register hiero events + events.register_hiero_events() + + def get_context_data(self): + # TODO: implement to support persisting context attributes + return {} - # register callback for switching publishable - pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) + def update_context_data(self, data, changes): + # TODO: implement to support persisting context attributes + pass def containerise(track_item, @@ -97,8 +124,8 @@ def containerise(track_item, """ data_imprint = OrderedDict({ - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-3.0", + "id": AYON_CONTAINER_ID, "name": str(name), "namespace": str(namespace), "loader": str(loader), @@ -110,7 +137,7 @@ def containerise(track_item, data_imprint.update({k: v}) log.debug("_ data_imprint: {}".format(data_imprint)) - lib.set_trackitem_openpype_tag(track_item, data_imprint) + lib.set_trackitem_ayon_tag(track_item, data_imprint) return track_item @@ -191,7 +218,7 @@ def data_to_container(item, data): # convert tag metadata to normal keys names if type(item) is hiero.core.VideoTrack: return_list = [] - _data = lib.get_track_openpype_data(item) + _data = lib.get_track_ayon_data(item) if not _data: return @@ -201,7 +228,7 @@ def data_to_container(item, data): return_list.append(container) return return_list else: - _data = lib.get_trackitem_openpype_data(item) + _data = lib.get_trackitem_ayon_data(item) return data_to_container(item, _data) @@ -216,7 +243,7 @@ def _update_container_data(container, data): def update_container(item, data=None): """Update container data to input track_item or track's - openpype tag. + AYON tag. Args: item (hiero.core.TrackItem or hiero.core.VideoTrack): @@ -236,8 +263,8 @@ def update_container(item, data=None): object_name = data["objectName"] # get all available containers - containers = lib.get_track_openpype_data(item) - container = lib.get_track_openpype_data(item, object_name) + containers = lib.get_track_ayon_data(item) + container = lib.get_track_ayon_data(item, object_name) containers = deepcopy(containers) container = deepcopy(container) @@ -247,13 +274,13 @@ def update_container(item, data=None): # merge updated container back to containers containers.update({object_name: updated_container}) - return bool(lib.set_track_openpype_tag(item, containers)) + return bool(lib.set_track_ayon_tag(item, containers)) else: - container = lib.get_trackitem_openpype_data(item) + container = lib.get_trackitem_ayon_data(item) updated_container = _update_container_data(container, data) log.info("Updating container: `{}`".format(item.name())) - return bool(lib.set_trackitem_openpype_tag(item, updated_container)) + return bool(lib.set_trackitem_ayon_tag(item, updated_container)) def launch_workfiles_app(*args): @@ -330,11 +357,11 @@ def on_pyblish_instance_toggled(instance, old_value, new_value): instance, old_value, new_value)) from ayon_hiero.api import ( - get_trackitem_openpype_tag, + get_trackitem_ayon_tag, set_publish_attribute ) # Whether instances should be passthrough based on new value track_item = instance.data["item"] - tag = get_trackitem_openpype_tag(track_item) + tag = get_trackitem_ayon_tag(track_item) set_publish_attribute(tag, new_value) diff --git a/client/ayon_hiero/api/plugin.py b/client/ayon_hiero/api/plugin.py index 16eb1d5..6ecef54 100644 --- a/client/ayon_hiero/api/plugin.py +++ b/client/ayon_hiero/api/plugin.py @@ -1,19 +1,25 @@ import os from pprint import pformat import re -from copy import deepcopy +import uuid import hiero from qtpy import QtWidgets, QtCore import qargparse -from ayon_core.settings import get_current_project_settings from ayon_core.lib import Logger -from ayon_core.pipeline import LoaderPlugin, LegacyCreator +from ayon_core.pipeline import ( + Creator, + HiddenCreator, + LoaderPlugin, +) from ayon_core.pipeline.load import get_representation_path_from_context +from ayon_core.settings import get_current_project_settings + from . import lib + log = Logger.get_logger(__name__) @@ -383,7 +389,7 @@ def __init__(self, cls, context, path, **options): """ Initialize object Arguments: - cls (avalon.api.Loader): plugin object + cls (ayon_core.api.Loader): plugin object context (dict): loader plugin context options (dict)[optional]: possible keys: projectBinPath: "path/to/binItem" @@ -593,31 +599,51 @@ def load(self): return track_item -class Creator(LegacyCreator): +class HiddenHieroCreator(HiddenCreator): + """HiddenCreator class wrapper + """ + settings_category = "hiero" + + def collect_instances(self): + pass + + def update_instances(self, update_list): + pass + + def remove_instances(self, instances): + pass + + +class HieroCreator(Creator): """Creator class wrapper """ - clip_color = "Purple" - rename_index = None + settings_category = "hiero" def __init__(self, *args, **kwargs): super(Creator, self).__init__(*args, **kwargs) - import ayon_hiero.api as phiero self.presets = get_current_project_settings()[ "hiero"]["create"].get(self.__class__.__name__, {}) + def create(self, product_name, instance_data, pre_create_data): + """Prepare data for new instance creation. + + Args: + product_name(str): Product name of created instance. + instance_data(dict): Base data for instance. + pre_create_data(dict): Data based on pre creation attributes. + Those may affect how creator works. + """ # adding basic current context resolve objects - self.project = phiero.get_current_project() - self.sequence = phiero.get_current_sequence() + self.project = lib.get_current_project() + self.sequence = lib.get_current_sequence() - if (self.options or {}).get("useSelection"): - timeline_selection = phiero.get_timeline_selection() - self.selected = phiero.get_track_items( + if pre_create_data.get("use_selection", False): + timeline_selection = lib.get_timeline_selection() + self.selected = lib.get_track_items( selection=timeline_selection ) else: - self.selected = phiero.get_track_items() - - self.widget = CreatorWidget + self.selected = lib.get_track_items() class PublishClip: @@ -629,10 +655,8 @@ class PublishClip: kwargs (optional): additional data needed for rename=True (presets) Returns: - hiero.core.TrackItem: hiero track item object with pype tag + hiero.core.TrackItem: hiero track item object with AYON tag """ - vertical_clip_match = {} - tag_data = {} types = { "shot": "shot", "folder": "folder", @@ -648,7 +672,7 @@ class PublishClip: rename_default = False hierarchy_default = "{_folder_}/{_sequence_}/{_track_}" clip_name_default = "shot_{_trackIndex_:0>3}_{_clipIndex_:0>4}" - base_product_name_default = "" + product_name_default = "" review_track_default = "< none >" product_type_default = "plate" count_from_default = 10 @@ -656,9 +680,31 @@ class PublishClip: vertical_sync_default = False driving_layer_default = "" - def __init__(self, cls, track_item, **kwargs): - # populate input cls attribute onto self.[attr] - self.__dict__.update(cls.__dict__) + # Define which keys of the pre create data should also be 'tag data' + tag_keys = { + # renameHierarchy + "hierarchy", + # hierarchyData + "folder", "episode", "sequence", "track", "shot", + # publish settings + "audio", "sourceResolution", + # shot attributes + "workfileFrameStart", "handleStart", "handleEnd" + } + + def __init__( + self, + track_item, + pre_create_data=None, + data=None, + rename_index=0): + + self.rename_index = rename_index + self.vertical_clip_match = dict() + self.tag_data = dict() + + # adding ui inputs if any + self.pre_create_data = pre_create_data or {} # get main parent objects self.track_item = track_item @@ -674,16 +720,13 @@ def __init__(self, cls, track_item, **kwargs): self.track_name = str(track_name).replace(" ", "_") self.track_index = int(track_item.parent().trackIndex()) - # adding tag.family into tag - if kwargs.get("avalon"): - self.tag_data.update(kwargs["avalon"]) + # adding instance_data["productType"] into tag + if data: + self.tag_data.update(data) # add publish attribute to tag data self.tag_data.update({"publish": True}) - # adding ui inputs if any - self.ui_inputs = kwargs.get("ui_inputs", {}) - # populate default data before we get other attributes self._populate_track_item_default_data() @@ -694,10 +737,9 @@ def __init__(self, cls, track_item, **kwargs): self._create_parents() def convert(self): - # solve track item data and add them to tag data - tag_hierarchy_data = self._convert_to_tag_data() - self.tag_data.update(tag_hierarchy_data) + # solve track item data and add them to tag data + self._convert_to_tag_data() # if track name is in review track name and also if driving track name # is not in review track name: skip tag creation @@ -711,30 +753,23 @@ def convert(self): if self.rename: # rename track item self.track_item.setName(new_name) - self.tag_data["asset_name"] = new_name + self.tag_data["folderName"] = new_name else: - self.tag_data["asset_name"] = self.ti_name + self.tag_data["folderName"] = self.ti_name self.tag_data["hierarchyData"]["shot"] = self.ti_name # AYON unique identifier folder_path = "/{}/{}".format( - tag_hierarchy_data["hierarchy"], - self.tag_data["asset_name"] + self.tag_data["hierarchy"], + self.tag_data["folderName"] ) self.tag_data["folderPath"] = folder_path + if self.tag_data["heroTrack"] and self.review_layer: self.tag_data.update({"reviewTrack": self.review_layer}) else: self.tag_data.update({"reviewTrack": None}) - # TODO: remove debug print - log.debug("___ self.tag_data: {}".format( - pformat(self.tag_data) - )) - - # create pype tag on track_item and add data - lib.imprint(self.track_item, self.tag_data) - return self.track_item def _populate_track_item_default_data(self): @@ -760,40 +795,36 @@ def _populate_attributes(self): log.debug( "____ self.shot_num: {}".format(self.shot_num)) + # publisher ui attribute inputs or default values if gui was not used + def get(key): + """Shorthand access for code readability""" + return self.pre_create_data.get(key) + # ui_inputs data or default values if gui was not used - self.rename = self.ui_inputs.get( - "clipRename", {}).get("value") or self.rename_default - self.clip_name = self.ui_inputs.get( - "clipName", {}).get("value") or self.clip_name_default - self.hierarchy = self.ui_inputs.get( - "hierarchy", {}).get("value") or self.hierarchy_default - self.hierarchy_data = self.ui_inputs.get( - "hierarchyData", {}).get("value") or \ - self.track_item_default_data.copy() - self.count_from = self.ui_inputs.get( - "countFrom", {}).get("value") or self.count_from_default - self.count_steps = self.ui_inputs.get( - "countSteps", {}).get("value") or self.count_steps_default - self.base_product_name = self.ui_inputs.get( - "productName", {}).get("value") or self.base_product_name_default - self.product_type = self.ui_inputs.get( - "productType", {}).get("value") or self.product_type_default - self.vertical_sync = self.ui_inputs.get( - "vSyncOn", {}).get("value") or self.vertical_sync_default - self.driving_layer = self.ui_inputs.get( - "vSyncTrack", {}).get("value") or self.driving_layer_default - self.review_track = self.ui_inputs.get( - "reviewTrack", {}).get("value") or self.review_track_default - self.audio = self.ui_inputs.get( - "audio", {}).get("value") or False + self.rename = self.pre_create_data.get("clipRename", self.rename_default) + self.clip_name = get("clipName") or self.clip_name_default + self.hierarchy = get("hierarchy") or self.hierarchy_default + self.count_from = get("countFrom") or self.count_from_default + self.count_steps = get("countSteps") or self.count_steps_default + self.product_name = get("productName") or self.product_name_default + self.product_type = get("productType") or self.product_type_default + self.vertical_sync = get("vSyncOn") or self.vertical_sync_default + self.driving_layer = get("vSyncTrack") or self.driving_layer_default + self.review_track = get("reviewTrack") or self.review_track_default + self.audio = get("audio") or False + + self.hierarchy_data = { + key: get(key) or self.track_item_default_data[key] + for key in ["folder", "episode", "sequence", "track", "shot"] + } # build product name from layer name - if self.base_product_name == "": - self.base_product_name = self.track_name + if self.product_name == "": + self.product_name = self.track_name # create product for publishing self.product_name = ( - self.product_type + self.base_product_name.capitalize() + self.product_type + self.product_name.capitalize() ) def _replace_hash_to_expression(self, name, text): @@ -803,7 +834,6 @@ def _replace_hash_to_expression(self, name, text): _repl = "{{{0}:0>{1}}}".format(name, _len) return text.replace(("#" * _len), _repl) - def _convert_to_tag_data(self): """ Convert internal data to tag data. @@ -821,14 +851,14 @@ def _convert_to_tag_data(self): # increasing steps by index of rename iteration self.count_steps *= self.rename_index - hierarchy_formatting_data = {} - hierarchy_data = deepcopy(self.hierarchy_data) + hierarchy_formatting_data = dict() _data = self.track_item_default_data.copy() - if self.ui_inputs: + if self.pre_create_data: + # adding tag metadata from ui - for _k, _v in self.ui_inputs.items(): - if _v["target"] == "tag": - self.tag_data[_k] = _v["value"] + for _key, _value in self.pre_create_data.items(): + if _key in self.tag_keys: + self.tag_data[_key] = _value # driving layer is set as positive match if hero_track or self.vertical_sync: @@ -847,19 +877,19 @@ def _convert_to_tag_data(self): _data.update({"shot": self.shot_num}) # solve # in test to pythonic expression - for _k, _v in hierarchy_data.items(): - if "#" not in _v["value"]: + for _key, _value in self.hierarchy_data.items(): + if "#" not in _value: continue - hierarchy_data[ - _k]["value"] = self._replace_hash_to_expression( - _k, _v["value"]) + self.hierarchy_data[_key] = self._replace_hash_to_expression( + _key, _value + ) # fill up pythonic expresisons in hierarchy data - for k, _v in hierarchy_data.items(): - hierarchy_formatting_data[k] = _v["value"].format(**_data) + for _key, _value in self.hierarchy_data.items(): + hierarchy_formatting_data[_key] = _value.format(**_data) else: # if no gui mode then just pass default data - hierarchy_formatting_data = hierarchy_data + hierarchy_formatting_data = self.hierarchy_data tag_hierarchy_data = self._solve_tag_hierarchy_data( hierarchy_formatting_data @@ -882,13 +912,22 @@ def _convert_to_tag_data(self): hero_data["productName"] = self.product_name + str( self.track_index) # in case track name and product name is the same then add - if self.base_product_name == self.track_name: + if self.product_name == self.track_name: hero_data["productName"] = self.product_name # assign data to return hierarchy data to tag tag_hierarchy_data = hero_data # add data to return data dict - return tag_hierarchy_data + self.tag_data.update(tag_hierarchy_data) + + # add uuid to tag data + self.tag_data["uuid"] = str(uuid.uuid4()) + + # add review track only to hero track + if hero_track and self.review_layer: + self.tag_data["reviewTrack"] = self.review_layer + else: + self.tag_data.update({"reviewTrack": None}) def _solve_tag_hierarchy_data(self, hierarchy_formatting_data): """ Solve tag data from hierarchy data and templates. """ @@ -905,8 +944,7 @@ def _solve_tag_hierarchy_data(self, hierarchy_formatting_data): "parents": self.parents, "hierarchyData": hierarchy_formatting_data, "productName": self.product_name, - "productType": self.product_type, - "families": [self.product_type, self.data["productType"]] + "productType": self.product_type } def _convert_to_entity(self, src_type, template): @@ -917,19 +955,16 @@ def _convert_to_entity(self, src_type, template): assert folder_type, "Missing folder type for `{}`".format( src_type ) - - # first collect formatting data to use for formatting template formatting_data = {} for _k, _v in self.hierarchy_data.items(): - value = _v["value"].format( + value = _v.format( **self.track_item_default_data) formatting_data[_k] = value return { + "entity_type": folder_type, "folder_type": folder_type, - "entity_name": template.format( - **formatting_data - ) + "entity_name": template.format(**formatting_data) } def _create_parents(self): @@ -941,6 +976,6 @@ def _create_parents(self): par_split = [(pattern.findall(t).pop(), t) for t in self.hierarchy.split("/")] - for type, template in par_split: - parent = self._convert_to_entity(type, template) + for type_, template in par_split: + parent = self._convert_to_entity(type_, template) self.parents.append(parent) diff --git a/client/ayon_hiero/api/startup/Python/Startup/Startup.py b/client/ayon_hiero/api/startup/Python/Startup/Startup.py index c916bf3..07a9dfd 100644 --- a/client/ayon_hiero/api/startup/Python/Startup/Startup.py +++ b/client/ayon_hiero/api/startup/Python/Startup/Startup.py @@ -2,8 +2,10 @@ # activate hiero from pype from ayon_core.pipeline import install_host -import ayon_hiero.api as phiero -install_host(phiero) +from ayon_hiero.api import HieroHost + +host = HieroHost() +install_host(host) try: __import__("ayon_hiero.api") diff --git a/client/ayon_hiero/api/tags.py b/client/ayon_hiero/api/tags.py index d4acb23..ecb8b24 100644 --- a/client/ayon_hiero/api/tags.py +++ b/client/ayon_hiero/api/tags.py @@ -7,6 +7,9 @@ from ayon_core.lib import Logger from ayon_core.pipeline import get_current_project_name +from . import constants + + log = Logger.get_logger(__name__) @@ -86,23 +89,68 @@ def update_tag(tag, data): # get metadata key from data data_mtd = data.get("metadata", {}) - # set all data metadata to tag metadata - for _k, _v in data_mtd.items(): - value = str(_v) - if isinstance(_v, dict): - value = json.dumps(_v) - - # set the value - mtd.setValue( - "tag.{}".format(str(_k)), - value - ) - + mtd.setValue( + "tag.json_metadata", + json.dumps(data_mtd) + ) # set note description of tag - tag.setNote(str(data["note"])) + if "note" in data: + tag.setNote(str(data["note"])) + return tag +def get_tag_data(tag): + """ + Args: + tag (hiero.core.Tag): The tag to retrieve data from. + + Returns: + dict. The tag data. + """ + tag_data = dict(tag.metadata()) + + try: + json_data = tag_data["tag.json_metadata"] + return json.loads(json_data) + + except (KeyError, json.JSONDecodeError): + return {} + + +def get_or_create_workfile_tag(create=False): + """ + Args: + create (bool): Create the project tag if missing. + + Returns: + hiero.core.Tag: The workfile tag or None + """ + from .lib import get_current_project # noqa prevent-circular-import + current_project = get_current_project() + + # retrieve parent tag bin + project_tag_bin = current_project.tagsBin() + for tag_bin in project_tag_bin.bins(): + if tag_bin.name() == constants.AYON_WORKFILE_TAG_BIN: + break + else: + if create: + tag_bin = project_tag_bin.addItem(constants.AYON_WORKFILE_TAG_BIN) + else: + return None + + # retrieve tag + for item in tag_bin.items(): + if (isinstance(item, hiero.core.Tag) + and item.name() == constants.AYON_WORKFILE_TAG_NAME): + return item + + workfile_tag = hiero.core.Tag(constants.AYON_WORKFILE_TAG_NAME) + tag_bin.addItem(workfile_tag) + return workfile_tag + + def add_tags_to_workfile(): """ Will create default tags from presets. diff --git a/client/ayon_hiero/plugins/create/create_shot_clip.py b/client/ayon_hiero/plugins/create/create_shot_clip.py index 201cf38..38d772e 100644 --- a/client/ayon_hiero/plugins/create/create_shot_clip.py +++ b/client/ayon_hiero/plugins/create/create_shot_clip.py @@ -1,247 +1,477 @@ -from copy import deepcopy -import ayon_hiero.api as phiero -# from ayon_hiero.api import plugin, lib -# reload(lib) -# reload(plugin) -# reload(phiero) +import copy +import json +from ayon_hiero.api import constants, plugin, lib, tags -class CreateShotClip(phiero.Creator): +from ayon_core.pipeline.create import CreatorError, CreatedInstance +from ayon_core.lib import BoolDef, EnumDef, TextDef, UILabelDef, NumberDef + + +# Used as a key by the creators in order to +# retrieve the instances data into clip markers. +_CONTENT_ID = "hiero_sub_products" + + +# Shot attributes +CLIP_ATTR_DEFS = [ + EnumDef( + "fps", + items=[ + {"value": "from_selection", "label": "From selection"}, + {"value": 23.997, "label": "23.976"}, + {"value": 24, "label": "24"}, + {"value": 25, "label": "25"}, + {"value": 29.97, "label": "29.97"}, + {"value": 30, "label": "30"} + ], + label="FPS" + ), + NumberDef( + "workfileFrameStart", + default=1001, + label="Workfile start frame" + ), + NumberDef( + "handleStart", + default=0, + label="Handle start" + ), + NumberDef( + "handleEnd", + default=0, + label="Handle end" + ), + NumberDef( + "frameStart", + default=0, + label="Frame start", + disabled=True, + ), + NumberDef( + "frameEnd", + default=0, + label="Frame end", + disabled=True, + ), + NumberDef( + "clipIn", + default=0, + label="Clip in", + disabled=True, + ), + NumberDef( + "clipOut", + default=0, + label="Clip out", + disabled=True, + ), + NumberDef( + "clipDuration", + default=0, + label="Clip duration", + disabled=True, + ), + NumberDef( + "sourceIn", + default=0, + label="Media source in", + disabled=True, + ), + NumberDef( + "sourceOut", + default=0, + label="Media source out", + disabled=True, + ) +] + + +class _HieroInstanceCreator(plugin.HiddenHieroCreator): + """Wrapper class for clip types products. + """ + + def create(self, instance_data, _): + """Return a new CreateInstance for new shot from Hiero. + + Args: + instance_data (dict): global data from original instance + + Return: + CreatedInstance: The created instance object for the new shot. + """ + instance_data.update({ + "productName": f"{self.product_type}{instance_data['variant']}", + "productType": self.product_type, + "newHierarchyIntegration": True, + # Backwards compatible (Deprecated since 24/06/06) + "newAssetPublishing": True, + }) + + new_instance = CreatedInstance( + self.product_type, instance_data["productName"], instance_data, self + ) + self._add_instance_to_context(new_instance) + new_instance.transient_data["has_promised_context"] = True + return new_instance + + def update_instances(self, update_list): + """Store changes of existing instances so they can be recollected. + + Args: + update_list(List[UpdateData]): Gets list of tuples. Each item + contain changed instance and it's changes. + """ + for created_inst, _changes in update_list: + track_item = created_inst.transient_data["track_item"] + tag = lib.get_trackitem_ayon_tag(track_item) + tag_data = tags.get_tag_data(tag) + + try: + instances_data = tag_data[_CONTENT_ID] + + # Backwards compatible (Deprecated since 24/09/05) + except KeyError: + tag_data[_CONTENT_ID] = {} + instances_data = tag_data[_CONTENT_ID] + + instances_data[self.identifier] = created_inst.data_to_store() + tags.update_tag(tag, {"metadata": tag_data}) + + def remove_instances(self, instances): + """Remove instance marker from track item. + + Args: + instance(List[CreatedInstance]): Instance objects which should be + removed. + """ + for instance in instances: + track_item = instance.transient_data["track_item"] + tag = lib.get_trackitem_ayon_tag(track_item) + tag_data = tags.get_tag_data(tag) + instances_data = tag_data.get(_CONTENT_ID, {}) + instances_data.pop(self.identifier, None) + self._remove_instance_from_context(instance) + + # Remove markers if deleted all of the instances + if not instances_data: + track_item.removeTag(tag) + + # Push edited data in marker + else: + tags.update_tag(tag, {"metadata": tag_data}) + + +class HieroShotInstanceCreator(_HieroInstanceCreator): + """Shot product type creator class""" + identifier = "io.ayon.creators.hiero.shot" + product_type = "shot" + label = "Editorial Shot" + + def get_instance_attr_defs(self): + instance_attributes = CLIP_ATTR_DEFS + return instance_attributes + + +class _HieroInstanceClipCreatorBase(_HieroInstanceCreator): + """ Base clip product creator. + """ + + def get_instance_attr_defs(self): + + current_sequence = lib.get_current_sequence() + if current_sequence is not None: + gui_tracks = [tr.name() for tr in current_sequence.videoTracks()] + else: + gui_tracks = [] + + instance_attributes = [ + TextDef( + "parentInstance", + label="Linked to", + disabled=True, + ) + ] + if self.product_type == "plate": + instance_attributes.extend([ + BoolDef( + "vSyncOn", + label="Enable Vertical Sync", + tooltip="Switch on if you want clips above " + "each other to share its attributes", + default=True, + ), + EnumDef( + "vSyncTrack", + label="Hero Track", + tooltip="Select driving track name which should " + "be mastering all others", + items=gui_tracks or [""], + ), + ]) + + return instance_attributes + + +class EditorialPlateInstanceCreator(_HieroInstanceClipCreatorBase): + """Plate product type creator class""" + identifier = "io.ayon.creators.hiero.plate" + product_type = "plate" + label = "Editorial Plate" + + def create(self, instance_data, _): + """Return a new CreateInstance for new shot from Resolve. + + Args: + instance_data (dict): global data from original instance + + Return: + CreatedInstance: The created instance object for the new shot. + """ + if instance_data.get("clip_variant") == "": + instance_data["variant"] = instance_data["hierarchyData"]["track"] + + else: + instance_data["variant"] = instance_data["clip_variant"] + + return super().create(instance_data, None) + + +class EditorialAudioInstanceCreator(_HieroInstanceClipCreatorBase): + """Audio product type creator class""" + identifier = "io.ayon.creators.hiero.audio" + product_type = "audio" + label = "Editorial Audio" + + +class CreateShotClip(plugin.HieroCreator): """Publishable clip""" + identifier = "io.ayon.creators.hiero.clip" label = "Create Publishable Clip" - product_type = "clip" + product_type = "editorial" icon = "film" defaults = ["Main"] - gui_tracks = [track.name() - for track in phiero.get_current_sequence().videoTracks()] - gui_name = "AYON publish attributes creator" - gui_info = "Define sequential rename and fill hierarchy data." - gui_inputs = { - "renameHierarchy": { - "type": "section", - "label": "Shot Hierarchy And Rename Settings", - "target": "ui", - "order": 0, - "value": { - "hierarchy": { - "value": "{folder}/{sequence}", - "type": "QLineEdit", - "label": "Shot Parent Hierarchy", - "target": "tag", - "toolTip": "Parents folder for shot root folder, Template filled with `Hierarchy Data` section", # noqa - "order": 0}, - "clipRename": { - "value": False, - "type": "QCheckBox", - "label": "Rename clips", - "target": "ui", - "toolTip": "Renaming selected clips on fly", # noqa - "order": 1}, - "clipName": { - "value": "{sequence}{shot}", - "type": "QLineEdit", - "label": "Clip Name Template", - "target": "ui", - "toolTip": "template for creating shot namespaused for renaming (use rename: on)", # noqa - "order": 2}, - "countFrom": { - "value": 10, - "type": "QSpinBox", - "label": "Count sequence from", - "target": "ui", - "toolTip": "Set when the sequence number stafrom", # noqa - "order": 3}, - "countSteps": { - "value": 10, - "type": "QSpinBox", - "label": "Stepping number", - "target": "ui", - "toolTip": "What number is adding every new step", # noqa - "order": 4}, - } - }, - "hierarchyData": { - "type": "dict", - "label": "Shot Template Keywords", - "target": "tag", - "order": 1, - "value": { - "folder": { - "value": "shots", - "type": "QLineEdit", - "label": "{folder}", - "target": "tag", - "toolTip": "Name of folder used for root of generated shots.\nUsable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa - "order": 0}, - "episode": { - "value": "ep01", - "type": "QLineEdit", - "label": "{episode}", - "target": "tag", - "toolTip": "Name of episode.\nUsable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa - "order": 1}, - "sequence": { - "value": "sq01", - "type": "QLineEdit", - "label": "{sequence}", - "target": "tag", - "toolTip": "Name of sequence of shots.\nUsable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa - "order": 2}, - "track": { - "value": "{_track_}", - "type": "QLineEdit", - "label": "{track}", - "target": "tag", - "toolTip": "Name of sequence of shots.\nUsable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa - "order": 3}, - "shot": { - "value": "sh###", - "type": "QLineEdit", - "label": "{shot}", - "target": "tag", - "toolTip": "Name of shot. `#` is converted to paded number. \nAlso could be used with usable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa - "order": 4} - } - }, - "verticalSync": { - "type": "section", - "label": "Vertical Synchronization Of Attributes", - "target": "ui", - "order": 2, - "value": { - "vSyncOn": { - "value": True, - "type": "QCheckBox", - "label": "Enable Vertical Sync", - "target": "ui", - "toolTip": "Switch on if you want clips above each other to share its attributes", # noqa - "order": 0}, - "vSyncTrack": { - "value": gui_tracks, # noqa - "type": "QComboBox", - "label": "Hero track", - "target": "ui", - "toolTip": "Select driving track name which should be hero for all others", # noqa - "order": 1} - } - }, - "publishSettings": { - "type": "section", - "label": "Publish Settings", - "target": "ui", - "order": 3, - "value": { - "productName": { - "value": ["", "main", "bg", "fg", "bg", - "animatic"], - "type": "QComboBox", - "label": "Product Name", - "target": "ui", - "toolTip": "chose product name pattern, if is selected, name of track layer will be used", # noqa - "order": 0}, - "productType": { - "value": ["plate", "take"], - "type": "QComboBox", - "label": "Product Type", - "target": "ui", "toolTip": "What use of this product is for", # noqa - "order": 1}, - "reviewTrack": { - "value": ["< none >"] + gui_tracks, - "type": "QComboBox", - "label": "Use Review Track", - "target": "ui", - "toolTip": "Generate preview videos on fly, if `< none >` is defined nothing will be generated.", # noqa - "order": 2}, - "audio": { - "value": False, - "type": "QCheckBox", - "label": "Include audio", - "target": "tag", - "toolTip": "Process products with corresponding audio", # noqa - "order": 3}, - "sourceResolution": { - "value": False, - "type": "QCheckBox", - "label": "Source resolution", - "target": "tag", - "toolTip": "Is resolution taken from timeline or source?", # noqa - "order": 4}, - } - }, - "frameRangeAttr": { - "type": "section", - "label": "Shot Attributes", - "target": "ui", - "order": 4, - "value": { - "workfileFrameStart": { - "value": 1001, - "type": "QSpinBox", - "label": "Workfiles Start Frame", - "target": "tag", - "toolTip": "Set workfile starting frame number", # noqa - "order": 0 - }, - "handleStart": { - "value": 0, - "type": "QSpinBox", - "label": "Handle Start", - "target": "tag", - "toolTip": "Handle at start of clip", # noqa - "order": 1 - }, - "handleEnd": { - "value": 0, - "type": "QSpinBox", - "label": "Handle End", - "target": "tag", - "toolTip": "Handle at end of clip", # noqa - "order": 2 - } - } - } - } - - presets = None - - def process(self): - # Creator copy of object attributes that are modified during `process` - presets = deepcopy(self.presets) - gui_inputs = deepcopy(self.gui_inputs) - - # get key pairs from presets and match it on ui inputs - for k, v in gui_inputs.items(): - if v["type"] in ("dict", "section"): - # nested dictionary (only one level allowed - # for sections and dict) - for _k, _v in v["value"].items(): - if presets.get(_k): - gui_inputs[k][ - "value"][_k]["value"] = presets[_k] - if presets.get(k): - gui_inputs[k]["value"] = presets[k] - - # open widget for plugins inputs - widget = self.widget(self.gui_name, self.gui_info, gui_inputs) - widget.exec_() + detailed_description = """ +Publishing clips/plate, audio for new shots to project +or updating already created from Hiero. Publishing will create +OTIO file. +""" + create_allow_thumbnail = False + + def get_pre_create_attr_defs(self): + + def header_label(text): + return f"
{text}" + + tokens_help = """\nUsable tokens: + {_clip_}: name of used clip + {_track_}: name of parent track layer + {_sequence_}: name of parent sequence (timeline)""" + + current_sequence = lib.get_current_sequence() + if current_sequence is not None: + gui_tracks = [tr.name() for tr in current_sequence.videoTracks()] + else: + gui_tracks = [] + + # Project settings might be applied to this creator via + # the inherited `Creator.apply_settings` + presets = self.presets + + return [ + + BoolDef("use_selection", + label="Use only selected clip(s).", + tooltip=( + "When enabled it restricts create process " + "to selected clips." + ), + default=True), + + # renameHierarchy + UILabelDef( + label=header_label("Shot Hierarchy And Rename Settings") + ), + TextDef( + "hierarchy", + label="Shot Parent Hierarchy", + tooltip="Parents folder for shot root folder, " + "Template filled with *Hierarchy Data* section", + default=presets.get("hierarchy", "{folder}/{sequence}"), + ), + BoolDef( + "clipRename", + label="Rename clips", + tooltip="Renaming selected clips on fly", + default=presets.get("clipRename", False), + ), + TextDef( + "clipName", + label="Clip Name Template", + tooltip="template for creating shot names, used for " + "renaming (use rename: on)", + default=presets.get("clipName", "{sequence}{shot}"), + ), + NumberDef( + "countFrom", + label="Count sequence from", + tooltip="Set where the sequence number starts from", + default=presets.get("countFrom", 10), + ), + NumberDef( + "countSteps", + label="Stepping number", + tooltip="What number is adding every new step", + default=presets.get("countSteps", 10), + ), + + # hierarchyData + UILabelDef( + label=header_label("Shot Template Keywords") + ), + TextDef( + "folder", + label="{folder}", + tooltip="Name of folder used for root of generated shots.\n" + f"{tokens_help}", + default=presets.get("folder", "shots"), + ), + TextDef( + "episode", + label="{episode}", + tooltip=f"Name of episode.\n{tokens_help}", + default=presets.get("episode", "ep01"), + ), + TextDef( + "sequence", + label="{sequence}", + tooltip=f"Name of sequence of shots.\n{tokens_help}", + default=presets.get("sequence", "sq01"), + ), + TextDef( + "track", + label="{track}", + tooltip=f"Name of timeline track.\n{tokens_help}", + default=presets.get("track", "{_track_}"), + ), + TextDef( + "shot", + label="{shot}", + tooltip="Name of shot. '#' is converted to padded number." + f"\n{tokens_help}", + default=presets.get("shot", "sh###"), + ), + + # verticalSync + UILabelDef( + label=header_label("Vertical Synchronization Of Attributes") + ), + BoolDef( + "vSyncOn", + label="Enable Vertical Sync", + tooltip="Switch on if you want clips above " + "each other to share its attributes", + default=presets.get("vSyncOn", True), + ), + EnumDef( + "vSyncTrack", + label="Hero track", + tooltip="Select driving track name which should " + "be mastering all others", + items=gui_tracks or [""], + ), + + # publishSettings + UILabelDef( + label=header_label("Publish Settings") + ), + EnumDef( + "clip_variant", + label="Product Variant", + tooltip="Chose variant which will be then used for " + "product name, if " + "is selected, name of track layer will be used", + items=['', 'main', 'bg', 'fg', 'bg', 'animatic'], + ), + EnumDef( + "productType", + label="Product Type", + tooltip="How the product will be used", + items=['plate', 'take'], + ), + EnumDef( + "reviewTrack", + label="Use Review Track", + tooltip="Generate preview videos on fly, if " + "'< none >' is defined nothing will be generated.", + items=['< none >'] + gui_tracks, + ), + BoolDef( + "export_audio", + label="Include audio", + tooltip="Process subsets with corresponding audio", + default=False, + ), + BoolDef( + "sourceResolution", + label="Source resolution", + tooltip="Is resloution taken from timeline or source?", + default=False, + ), + + # shotAttr + UILabelDef( + label=header_label("Shot Attributes"), + ), + NumberDef( + "workfileFrameStart", + label="Workfiles Start Frame", + tooltip="Set workfile starting frame number", + default=presets.get("workfileFrameStart", 1001), + ), + NumberDef( + "handleStart", + label="Handle start (head)", + tooltip="Handle at start of clip", + default=presets.get("handleStart", 0), + ), + NumberDef( + "handleEnd", + label="Handle end (tail)", + tooltip="Handle at end of clip", + default=presets.get("handleEnd", 0), + ), + ] + + def create(self, subset_name, instance_data, pre_create_data): + super(CreateShotClip, self).create(subset_name, + instance_data, + pre_create_data) if len(self.selected) < 1: return - if not widget.result: - print("Operation aborted") - return + self.log.info(self.selected) + self.log.debug(f"Selected: {self.selected}") + + audio_clips = [] + for audio_track in self.sequence.audioTracks(): + audio_clips.extend(audio_track.items()) - self.rename_add = 0 + if not audio_clips and pre_create_data.get("export_audio"): + raise CreatorError( + "You must have audio in your active " + "timeline in order to export audio." + ) - # get ui output for track name for vertical sync - v_sync_track = widget.result["vSyncTrack"]["value"] + instance_data["clip_variant"] = pre_create_data["clip_variant"] + instance_data["task"] = None # sort selected trackItems by sorted_selected_track_items = list() unsorted_selected_track_items = list() + v_sync_track = pre_create_data.get("vSyncTrack", "") for _ti in self.selected: if _ti.parent().name() in v_sync_track: sorted_selected_track_items.append(_ti) @@ -250,13 +480,320 @@ def process(self): sorted_selected_track_items.extend(unsorted_selected_track_items) - kwargs = { - "ui_inputs": widget.result, - "avalon": self.data + # detect enabled creators for review, plate and audio + all_creators = { + "io.ayon.creators.hiero.shot": True, + "io.ayon.creators.hiero.plate": True, + "io.ayon.creators.hiero.audio": pre_create_data.get("export_audio", False), } + enabled_creators = tuple(cre for cre, enabled in all_creators.items() if enabled) + + instances = [] + + for idx, track_item in enumerate(sorted_selected_track_items): - for i, track_item in enumerate(sorted_selected_track_items): - self.rename_index = i + instance_data["clip_index"] = track_item.guid() # convert track item to timeline media pool item - phiero.PublishClip(self, track_item, **kwargs).convert() + publish_clip = plugin.PublishClip( + track_item, + pre_create_data=pre_create_data, + rename_index=idx, + data=instance_data) + + track_item = publish_clip.convert() + if track_item is None: + # Ignore input clips that do not convert into a track item + # from `PublishClip.convert` + continue + + self.log.info( + "Processing track item data: {} (index: {})".format( + track_item, idx) + ) + instance_data.update(publish_clip.tag_data) + + # Delete any existing instances previously generated for the clip. + prev_tag = lib.get_trackitem_ayon_tag(track_item) + if prev_tag: + prev_tag_data = tags.get_tag_data(prev_tag) + for creator_id, inst_data in prev_tag_data[_CONTENT_ID].items(): + creator = self.create_context.creators[creator_id] + prev_instances = [ + inst for inst_id, inst + in self.create_context.instances_by_id.items() + if inst_id == inst_data["instance_id"] + ] + creator.remove_instances(prev_instances) + + # Create new product(s) instances. + clip_instances = {} + shot_creator_id = "io.ayon.creators.hiero.shot" + for creator_id in enabled_creators: + creator = self.create_context.creators[creator_id] + sub_instance_data = copy.deepcopy(instance_data) + shot_folder_path = sub_instance_data["folderPath"] + + # Shot creation + if creator_id == shot_creator_id: + track_item_duration = track_item.duration() + workfileFrameStart = \ + sub_instance_data["workfileFrameStart"] + sub_instance_data.update({ + "creator_attributes": { + "workfileFrameStart": \ + sub_instance_data["workfileFrameStart"], + "handleStart": sub_instance_data["handleStart"], + "handleEnd": sub_instance_data["handleEnd"], + "frameStart": workfileFrameStart, + "frameEnd": (workfileFrameStart + + track_item_duration), + "clipIn": track_item.timelineIn(), + "clipOut": track_item.timelineOut(), + "clipDuration": track_item_duration, + "sourceIn": track_item.sourceIn(), + "sourceOut": track_item.sourceOut(), + }, + "label": ( + f"{shot_folder_path} shot" + ), + }) + + # Plate, Audio + # insert parent instance data to allow + # metadata recollection as publish time. + else: + parenting_data = clip_instances[shot_creator_id] + sub_instance_data.update({ + "parent_instance_id": parenting_data["instance_id"], + "label": ( + f"{shot_folder_path} " + f"{creator.product_type}" + ), + "creator_attributes": { + "parentInstance": parenting_data["label"], + } + }) + + instance = creator.create(sub_instance_data, None) + instance.transient_data["track_item"] = track_item + self._add_instance_to_context(instance) + clip_instances[creator_id] = instance.data_to_store() + + lib.imprint( + track_item, + data={ + _CONTENT_ID: clip_instances, + "clip_index": track_item.guid(), + } + ) + instances.append(instance) + + return instances + + def _create_and_add_instance(self, data, creator_id, + track_item, instances): + """ + Args: + data (dict): The data to re-recreate the instance from. + creator_id (str): The creator id to use. + track_item (obj): The associated track item. + instances (list): Result instance container. + + Returns: + CreatedInstance: The newly created instance. + """ + creator = self.create_context.creators[creator_id] + instance = creator.create(data, None) + instance.transient_data["track_item"] = track_item + self._add_instance_to_context(instance) + instances.append(instance) + return instance + + def _collect_legacy_instance(self, track_item): + """Collect a legacy instance from previous creator if any.# + + Args: + track_item (obj): The Hiero track_item to inspect. + """ + tag = lib.get_trackitem_ayon_tag( + track_item, + tag_name=constants.LEGACY_OPENPYPE_TAG_NAME, + ) + if not tag: + return + + data = tag.metadata() + + clip_instances = {} + instance_data = { + "clip_index": track_item.guid(), + "task": None, + "variant": track_item.parentTrack().name(), + "extract_audio": False, + } + for create_attr in self.get_pre_create_attr_defs(): + if isinstance(create_attr.key, str): + instance_data[create_attr.key] = create_attr.default + + required_key_mapping = { + "tag.audio": ("extract_audio", bool), + "tag.heroTrack": ("heroTrack", bool), + "tag.handleStart": ("handleStart", int), + "tag.handleEnd": ("handleEnd", int), + "tag.folderPath": ("folderPath", str), + "tag.reviewTrack": ("reviewTrack", str), + "tag.variant": ("variant", str), + "tag.workfileFrameStart": ("workfileFrameStart", int), + "tag.sourceResolution": ("sourceResolution", bool), + "tag.hierarchy": ("hierarchy", str), + "tag.hierarchyData": ("hierarchyData", json), + "tag.asset_name": ("folderName", str), + "tag.asset": ("productName", str), + "tag.active": ("active", bool), + "tag.productName": ("productName", str), + "tag.parents": ("parents", json), + } + + for key, value in required_key_mapping.items(): + if key not in data: + continue + + try: + instance_key, type_cast = value + if type_cast is bool: + instance_data[instance_key] = data[key] == "True" + elif type_cast is json: + conformed_data = data[key].replace("'", "\"") + conformed_data = conformed_data.replace('u"', '"') + instance_data[instance_key] = json.loads(conformed_data) + else: + instance_data[instance_key] = type_cast(data[key]) + + except Exception as error: + self.log.warning( + "Cannot retrieve instance from legacy " + f"tag data: {error}." + ) + + if "folderPath" not in instance_data: + try: + instance_data["folderPath"] = ( + "/" + instance_data["hierarchy"] + "/" + + instance_data["productName"] + ) + except KeyError: + instance_data["folderPath"] = "unknown" + instance_data["active"] = False + + if "tag.subset" in data: + instance_data["variant"] = data["tag.subset"].replace("plate", "") + + for folder in instance_data.get("parents", []): + if "entity_name" in folder: + folder["folder_name"] = folder["entity_name"] + if "entity_type" in folder: + folder["folder_type"] = folder["entity_type"] + + # Create parent shot instance. + sub_instance_data = instance_data.copy() + track_item_duration = track_item.duration() + workfileFrameStart = \ + sub_instance_data["workfileFrameStart"] + sub_instance_data.update({ + "label": f"{sub_instance_data['folderPath']} shot", + "variant": "main", + "creator_attributes": { + "workfileFrameStart": workfileFrameStart, + "handleStart": sub_instance_data["handleStart"], + "handleEnd": sub_instance_data["handleEnd"], + "frameStart": workfileFrameStart, + "frameEnd": (workfileFrameStart + + track_item_duration), + "clipIn": track_item.timelineIn(), + "clipOut": track_item.timelineOut(), + "clipDuration": track_item_duration, + "sourceIn": track_item.sourceIn(), + "sourceOut": track_item.sourceOut(), + } + }) + + shot_creator_id = "io.ayon.creators.hiero.shot" + creator = self.create_context.creators[shot_creator_id] + instance = creator.create(sub_instance_data, None) + instance.transient_data["track_item"] = track_item + self._add_instance_to_context(instance) + clip_instances[shot_creator_id] = instance.data_to_store() + parenting_data = instance + + # Create plate/audio instance + if instance_data["extract_audio"]: + sub_creators = ( + "io.ayon.creators.hiero.plate", + "io.ayon.creators.hiero.audio" + ) + else: + sub_creators = ("io.ayon.creators.hiero.plate",) + + for sub_creator_id in sub_creators: + sub_instance_data = instance_data.copy() + creator = self.create_context.creators[sub_creator_id] + sub_instance_data.update({ + "clip_variant": sub_instance_data["variant"], + "parent_instance_id": parenting_data["instance_id"], + "label": ( + f"{sub_instance_data['folderPath']} " + f"{creator.product_type}" + ), + "creator_attributes": { + "parentInstance": parenting_data["label"], + } + }) + + instance = creator.create(sub_instance_data, None) + instance.transient_data["track_item"] = track_item + self._add_instance_to_context(instance) + clip_instances[sub_creator_id] = instance.data_to_store() + + # Adjust clip tag to match new publisher + track_item.removeTag(tag) + lib.imprint( + track_item, + data={ + _CONTENT_ID: clip_instances, + "clip_index": track_item.guid(), + } + ) + + def collect_instances(self): + """Collect all created instances from current timeline.""" + current_sequence = lib.get_current_sequence() + if current_sequence: + all_video_tracks = current_sequence.videoTracks() + else: + all_video_tracks = [] + + instances = [] + for video_track in all_video_tracks: + for track_item in video_track: + + # attempt to get AYON tag data + tag = lib.get_trackitem_ayon_tag(track_item) + if not tag: + self._collect_legacy_instance(track_item) + continue + + tag_data = tags.get_tag_data(tag) + for creator_id, data in tag_data.get(_CONTENT_ID, {}).items(): + self._create_and_add_instance( + data, creator_id, track_item, instances) + + return instances + + def update_instances(self, update_list): + """Never called, update is handled via _HieroInstanceCreator.""" + pass + + def remove_instances(self, instances): + """Never called, update is handled via _HieroInstanceCreator.""" + pass diff --git a/client/ayon_hiero/plugins/create/create_workfile.py b/client/ayon_hiero/plugins/create/create_workfile.py new file mode 100644 index 0000000..043c095 --- /dev/null +++ b/client/ayon_hiero/plugins/create/create_workfile.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating workfiles.""" +from ayon_core.pipeline.create import CreatedInstance, AutoCreator + +from ayon_hiero.api import tags + + +class CreateWorkfile(AutoCreator): + """Workfile auto-creator.""" + settings_category = "hiero" + + identifier = "io.ayon.creators.hiero.workfile" + label = "Workfile" + product_type = "workfile" + icon = "fa5.file" + + default_variant = "Main" + + @classmethod + def dump_instance_data(cls, data): + """ Dump instance data into AyonData project tag. + + Args: + data (dict): The data to push to the project tag. + """ + project_tag = tags.get_or_create_workfile_tag(create=True) + + tag_data = { + "metadata": data, + "note": "AYON workfile data" + } + tags.update_tag(project_tag, tag_data) + + def load_instance_data(cls): + """ Returns the data stored in AyonData project tag if any. + + Returns: + dict. The project data. + """ + project_tag = tags.get_or_create_workfile_tag() + if project_tag is None: + return {} + + instance_data = tags.get_tag_data(project_tag) + return instance_data + + def _create_new_instance(self): + """Create a new workfile instance. + + Returns: + dict. The data of the instance to be created. + """ + project_name = self.create_context.get_current_project_name() + folder_path = self.create_context.get_current_folder_path() + task_name = self.create_context.get_current_task_name() + host_name = self.create_context.host_name + variant = self.default_variant + + folder_entity = self.create_context.get_current_folder_entity() + task_entity = self.create_context.get_current_task_entity() + + product_name = self.get_product_name( + project_name, + folder_entity, + task_entity, + variant, + host_name, + ) + + instance_data = { + "folderPath": folder_path, + "task": task_name, + "variant": variant, + "productName": product_name, + } + instance_data.update(self.get_dynamic_data( + variant, + task_name, + folder_entity, + project_name, + host_name, + False, + )) + + return instance_data + + def create(self, options=None): + """Auto-create an instance by default.""" + instance_data = self.load_instance_data() + if instance_data: + return + + self.log.info("Auto-creating workfile instance...") + data = self._create_new_instance() + current_instance = CreatedInstance( + self.product_type, data["productName"], data, self) + self._add_instance_to_context(current_instance) + + def collect_instances(self): + """Collect from timeline marker or create a new one.""" + data = self.load_instance_data() + if not data: + return + + instance = CreatedInstance( + self.product_type, data["productName"], data, self + ) + self._add_instance_to_context(instance) + + def update_instances(self, update_list): + """Store changes in project metadata so they can be recollected. + + Args: + update_list(List[UpdateData]): Gets list of tuples. Each item + contain changed instance and its changes. + """ + for created_inst, _ in update_list: + data = created_inst.data_to_store() + self.dump_instance_data(data) diff --git a/client/ayon_hiero/plugins/load/load_effects.py b/client/ayon_hiero/plugins/load/load_effects.py index d26026f..388ce84 100644 --- a/client/ayon_hiero/plugins/load/load_effects.py +++ b/client/ayon_hiero/plugins/load/load_effects.py @@ -176,7 +176,7 @@ def update(self, container, context): stitem.name(): stitem for stitem in phiero.flatten(active_track.subTrackItems()) } - container = phiero.get_track_openpype_data( + container = phiero.get_track_ayon_data( active_track, object_name ) @@ -288,7 +288,7 @@ def containerise( data_imprint = { object_name: { - "schema": "openpype:container-2.0", + "schema": "ayon:container-2.0", "id": AVALON_CONTAINER_ID, "name": str(name), "namespace": str(namespace), @@ -302,4 +302,4 @@ def containerise( data_imprint[object_name].update({k: v}) self.log.debug("_ data_imprint: {}".format(data_imprint)) - phiero.set_track_openpype_tag(track, data_imprint) + phiero.set_track_ayon_tag(track, data_imprint) diff --git a/client/ayon_hiero/plugins/publish/collect_audio.py b/client/ayon_hiero/plugins/publish/collect_audio.py new file mode 100644 index 0000000..0d28411 --- /dev/null +++ b/client/ayon_hiero/plugins/publish/collect_audio.py @@ -0,0 +1,34 @@ +import pyblish + + +class CollectAudio(pyblish.api.InstancePlugin): + """Collect new audio.""" + + order = pyblish.api.CollectorOrder - 0.48 + label = "Collect Audio" + hosts = ["hiero"] + families = ["audio"] + + def process(self, instance): + """ + Args: + instance (pyblish.Instance): The shot instance to update. + """ + # Retrieve instance data from parent instance shot instance. + parent_instance_id = instance.data["parent_instance_id"] + edit_shared_data = instance.context.data["editorialSharedData"] + instance.data.update( + edit_shared_data[parent_instance_id] + ) + + if instance.data.get("reviewTrack") is not None: + instance.data["reviewAudio"] = True + instance.data.pop("reviewTrack") + + clip_src = instance.data["otioClip"].source_range + clip_src_in = clip_src.start_time.to_frames() + clip_src_out = clip_src_in + clip_src.duration.to_frames() + instance.data.update({ + "clipInH": clip_src_in, + "clipOutH": clip_src_out + }) diff --git a/client/ayon_hiero/plugins/publish/collect_frame_tag_instances.py b/client/ayon_hiero/plugins/publish/collect_frame_tag_instances.py index 0e5d849..8945ba9 100644 --- a/client/ayon_hiero/plugins/publish/collect_frame_tag_instances.py +++ b/client/ayon_hiero/plugins/publish/collect_frame_tag_instances.py @@ -1,7 +1,7 @@ from pprint import pformat +import json import re import ast -import json import pyblish.api @@ -19,6 +19,7 @@ class CollectFrameTagInstances(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder label = "Collect Frames" hosts = ["hiero"] + families = ["clip"] def process(self, context): self._context = context @@ -37,10 +38,14 @@ def _get_tag_data(self, tag): data = {} # get tag metadata attribute - tag_data = tag.metadata() + tag_data = dict(tag.metadata()) + + if tag_data.get("tag.json_metadata"): + return json.loads(tag_data.get("tag.json_metadata")) # convert tag metadata to normal keys names and values to correct types - for k, v in dict(tag_data).items(): + # legacy + for k, v in tag_data.items(): key = k.replace("tag.", "") try: diff --git a/client/ayon_hiero/plugins/publish/collect_otio_timeline.py b/client/ayon_hiero/plugins/publish/collect_otio_timeline.py new file mode 100644 index 0000000..c845c25 --- /dev/null +++ b/client/ayon_hiero/plugins/publish/collect_otio_timeline.py @@ -0,0 +1,105 @@ +import pyblish.api + +from ayon_core.pipeline import registered_host + +from ayon_hiero.api import lib +from ayon_hiero.api.otio import hiero_export + +import hiero + + +class CollectOTIOTimeline(pyblish.api.ContextPlugin): + """Inject the otio timeline""" + + label = "Collect OTIO Timeline" + hosts = ["hiero"] + order = pyblish.api.CollectorOrder - 0.491 + + def process(self, context): + host = registered_host() + current_file = host.get_current_workfile() + + otio_timeline = hiero_export.create_otio_timeline() + + active_timeline = hiero.ui.activeSequence() + project = active_timeline.project() + fps = active_timeline.framerate().toFloat() + + all_tracks = active_timeline.videoTracks() + tracks_effect_items = self.collect_sub_track_items(all_tracks) + + context_data = { + "activeProject": project, + "activeTimeline": active_timeline, + "currentFile": current_file, + "otioTimeline": otio_timeline, + "colorspace": self.get_colorspace(project), + "fps": fps, + "tracksEffectItems": tracks_effect_items, + } + context.data.update(context_data) + + def get_colorspace(self, project): + # get workfile's colorspace properties + return { + "useOCIOEnvironmentOverride": project.useOCIOEnvironmentOverride(), + "lutSetting16Bit": project.lutSetting16Bit(), + "lutSetting8Bit": project.lutSetting8Bit(), + "lutSettingFloat": project.lutSettingFloat(), + "lutSettingLog": project.lutSettingLog(), + "lutSettingViewer": project.lutSettingViewer(), + "lutSettingWorkingSpace": project.lutSettingWorkingSpace(), + "lutUseOCIOForExport": project.lutUseOCIOForExport(), + "ocioConfigName": project.ocioConfigName(), + "ocioConfigPath": project.ocioConfigPath() + } + + @staticmethod + def collect_sub_track_items(tracks): + """ + Args: + tracks (list): All of the video tracks. + + Returns: + dict. Track index as key and list of subtracks + """ + # collect all subtrack items + sub_track_items = {} + for track in tracks: + effect_items = track.subTrackItems() + + # skip if no clips on track > need track with effect only + if not effect_items: + continue + + # skip all disabled tracks + if not track.isEnabled(): + continue + + track_index = track.trackIndex() + _sub_track_items = lib.flatten(effect_items) + + _sub_track_items = list(_sub_track_items) + # continue only if any subtrack items are collected + if not _sub_track_items: + continue + + enabled_sti = [] + # loop all found subtrack items and check if they are enabled + for _sti in _sub_track_items: + # checking if not enabled + if not _sti.isEnabled(): + continue + if isinstance(_sti, hiero.core.Annotation): + continue + # collect the subtrack item + enabled_sti.append(_sti) + + # continue only if any subtrack items are collected + if not enabled_sti: + continue + + # add collection of subtrackitems to dict + sub_track_items[track_index] = enabled_sti + + return sub_track_items diff --git a/client/ayon_hiero/plugins/publish/collect_plates.py b/client/ayon_hiero/plugins/publish/collect_plates.py new file mode 100644 index 0000000..12c3c13 --- /dev/null +++ b/client/ayon_hiero/plugins/publish/collect_plates.py @@ -0,0 +1,29 @@ +import pyblish + + +class CollectPlate(pyblish.api.InstancePlugin): + """Collect new plates.""" + + order = pyblish.api.CollectorOrder - 0.48 + label = "Collect Plate" + hosts = ["hiero"] + families = ["plate"] + + def process(self, instance): + """ + Args: + instance (pyblish.Instance): The shot instance to update. + """ + instance.data["families"].append("clip") + + # Retrieve instance data from parent instance shot instance. + parent_instance_id = instance.data["parent_instance_id"] + edit_shared_data = instance.context.data["editorialSharedData"] + + instance.data.update( + edit_shared_data[parent_instance_id] + ) + + track_item = instance.data["item"] + version_data = instance.data.setdefault("versionData", {}) + version_data["colorSpace"] = track_item.sourceMediaColourTransform() diff --git a/client/ayon_hiero/plugins/publish/collect_shots.py b/client/ayon_hiero/plugins/publish/collect_shots.py new file mode 100644 index 0000000..874a203 --- /dev/null +++ b/client/ayon_hiero/plugins/publish/collect_shots.py @@ -0,0 +1,167 @@ +import json +import pyblish + +from ayon_hiero.api import lib +from ayon_hiero.api.otio import utils + +import hiero + + +class CollectShot(pyblish.api.InstancePlugin): + """Collect new shots.""" + + order = pyblish.api.CollectorOrder - 0.49 + label = "Collect Shots" + hosts = ["hiero"] + families = ["shot"] + + SHARED_KEYS = ( + "annotations", + "folderPath", + "fps", + "handleStart", + "handleEnd", + "item", + "otioClip", + "resolutionWidth", + "resolutionHeight", + "pixelAspect", + "subtracks", + "tags", + ) + + @classmethod + def _inject_editorial_shared_data(cls, instance): + """ + Args: + instance (obj): The publishing instance. + """ + context = instance.context + instance_id = instance.data["instance_id"] + + # Inject folderPath and other creator_attributes to ensure + # new shots/hierarchy are properly handled. + creator_attributes = instance.data['creator_attributes'] + instance.data.update(creator_attributes) + + # Adjust handles: + # Explain + track_item = instance.data["item"] + instance.data.update({ + "handleStart": min( + instance.data["handleStart"], int(track_item.handleInLength())), + "handleEnd": min( + instance.data["handleEnd"], int(track_item.handleOutLength())), + }) + + # Inject/Distribute instance shot data as editorialSharedData + # to make it available for clip/plate/audio products + # in sub-collectors. + if not context.data.get("editorialSharedData"): + context.data["editorialSharedData"] = {} + + context.data["editorialSharedData"][instance_id] = { + key: value for key, value in instance.data.items() + if key in cls.SHARED_KEYS + } + + def process(self, instance): + """ + Args: + instance (pyblish.Instance): The shot instance to update. + """ + instance.data["integrate"] = False # no representation for shot + + # Adjust instance data from parent otio timeline. + otio_timeline = instance.context.data["otioTimeline"] + otio_clip, marker = utils.get_marker_from_clip_index( + otio_timeline, instance.data["clip_index"] + ) + if not otio_clip: + raise RuntimeError("Could not retrieve otioClip for shot %r", instance) + + # Compute fps from creator attribute. + if instance.data['creator_attributes']["fps"] == "from_selection": + instance.data['creator_attributes']["fps"] = instance.context.data["fps"] + + # Retrieve AyonData marker for associated clip. + instance.data["otioClip"] = otio_clip + creator_id = instance.data["creator_identifier"] + + marker_metadata = json.loads(marker.metadata["json_metadata"]) + inst_data = marker_metadata["hiero_sub_products"].get(creator_id, {}) + + # Overwrite settings with clip metadata is "sourceResolution" + overwrite_clip_metadata = inst_data.get("sourceResolution", False) + active_timeline = instance.context.data["activeTimeline"] + + # Adjust info from track_item on timeline + track_item = None + for video_track in active_timeline.videoTracks(): + for item in video_track.items(): + if item.guid() == instance.data["clip_index"]: + track_item = item + break + + instance.data.update({ + "annotations": self.clip_annotations(track_item.source()), + "item": track_item, + "subtracks": self.clip_subtrack(track_item), + "tags": lib.get_track_item_tags(track_item), + }) + + # Retrieve clip from active_timeline + if overwrite_clip_metadata: + source_clip = item.source() + item_format = source_clip.format() + + # Get resolution from active timeline + else: + item_format = active_timeline.format() + + instance.data.update( + { + "resolutionWidth": item_format.width(), + "resolutionHeight": item_format.height(), + "pixelAspect": item_format.pixelAspect() + } + ) + self._inject_editorial_shared_data(instance) + + @staticmethod + def clip_annotations(clip): + """ + Args: + clip (hiero.core.TrackItem): The clip to inspect. + + Returns: + list[hiero.core.Annotation]: Associated clips annotations. + """ + annotations = [] + subTrackItems = lib.flatten(clip.subTrackItems()) + annotations += [item for item in subTrackItems if isinstance( + item, hiero.core.Annotation)] + return annotations + + @staticmethod + def clip_subtrack(clip): + """ + Args: + clip (hiero.core.TrackItem): The clip to inspect. + + Returns: + list[hiero.core.SubTrackItem]: Associated clips SubTrackItem. + """ + subtracks = [] + subTrackItems = lib.flatten(clip.parent().subTrackItems()) + for item in subTrackItems: + if "TimeWarp" in item.name(): + continue + # avoid all annotation + if isinstance(item, hiero.core.Annotation): + continue + # avoid all disabled + if not item.isEnabled(): + continue + subtracks.append(item) + return subtracks diff --git a/client/ayon_hiero/plugins/publish/collect_workfile.py b/client/ayon_hiero/plugins/publish/collect_workfile.py new file mode 100644 index 0000000..b02f86d --- /dev/null +++ b/client/ayon_hiero/plugins/publish/collect_workfile.py @@ -0,0 +1,20 @@ +import pyblish.api + +import hiero + + +class CollectWorkfile(pyblish.api.ContextPlugin): + """Collect the current working file into context""" + + label = "Collect Workfile" + hosts = ["hiero"] + order = pyblish.api.CollectorOrder - 0.49 + + def process(self, instance): + + active_timeline = hiero.ui.activeSequence() + project = active_timeline.project() + + current_file = project.path() + + instance.data["currentFile"] = current_file diff --git a/client/ayon_hiero/plugins/publish/extract_clip_effects.py b/client/ayon_hiero/plugins/publish/extract_clip_effects.py index 7ee979c..e034809 100644 --- a/client/ayon_hiero/plugins/publish/extract_clip_effects.py +++ b/client/ayon_hiero/plugins/publish/extract_clip_effects.py @@ -94,7 +94,7 @@ def process(self, instance): def copy_linked_files(self, effect, dst_dir): for k, v in effect["node"].items(): - if k in "file" and v != '': + if k in "file" and isinstance(v, str) and v != '': base_name = os.path.basename(v) dst = os.path.join(dst_dir, base_name).replace("\\", "/") diff --git a/client/ayon_hiero/plugins/publish/extract_workfile.py b/client/ayon_hiero/plugins/publish/extract_workfile.py new file mode 100644 index 0000000..2080f11 --- /dev/null +++ b/client/ayon_hiero/plugins/publish/extract_workfile.py @@ -0,0 +1,85 @@ +import os +import pyblish.api + +from ayon_core.pipeline import publish + +import hiero +import tempfile + +from qtpy.QtGui import QPixmap + + +class ExtractWorkfile(publish.Extractor): + """ + Extractor export Hiero workfile representation + """ + + label = "Extract Workfile" + order = pyblish.api.ExtractorOrder + families = ["workfile"] + hosts = ["hiero"] + + def process(self, instance): + # create representation data + if "representations" not in instance.data: + instance.data["representations"] = [] + + # asset = instance.context.data["folderPath"] + # asset_name = asset.split("/")[-1] + + active_timeline = hiero.ui.activeSequence() + # project = active_timeline.project() + + # adding otio timeline to context + # otio_timeline = hiero_export.create_otio_timeline() + # otio_timeline = instance.data["otioTimeline"] + + # get workfile thumbnail paths + tmp_staging = tempfile.mkdtemp(prefix="pyblish_tmp_") + thumbnail_name = "workfile_thumbnail.png" + thumbnail_path = os.path.join(tmp_staging, thumbnail_name) + + # search for all windows with name of actual sequence + _windows = [w for w in hiero.ui.windowManager().windows() + if active_timeline.name() in w.windowTitle()] + + # export window to thumb path + QPixmap.grabWidget(_windows[-1]).save(thumbnail_path, 'png') + + # thumbnail + thumb_representation = { + 'files': thumbnail_name, + 'stagingDir': tmp_staging, + 'name': "thumbnail", + 'thumbnail': True, + 'ext': "png" + } + + name = instance.data["name"] + project = hiero.ui.activeProject() + staging_dir = self.staging_dir(instance) + + ext = ".hrox" + filename = name + ext + filepath = os.path.normpath( + os.path.join(staging_dir, filename)) + + # write out the workfile + path_previous = project.path() + project.saveAs(filepath) + project.setPath(path_previous) + + # create workfile representation + representation = { + 'name': ext.lstrip("."), + 'ext': ext.lstrip("."), + 'files': filename, + "stagingDir": staging_dir, + } + representations = instance.data.setdefault("representations", []) + representations.append(representation) + representations.append(thumb_representation) + + self.log.debug( + "Added hiero file representation: {}".format(representation) + ) diff --git a/client/ayon_hiero/plugins/publish/precollect_instances.py b/client/ayon_hiero/plugins/publish/precollect_instances.py deleted file mode 100644 index 8ec4512..0000000 --- a/client/ayon_hiero/plugins/publish/precollect_instances.py +++ /dev/null @@ -1,448 +0,0 @@ -import pyblish - -from ayon_core.pipeline import AYON_INSTANCE_ID, AVALON_INSTANCE_ID -from ayon_core.pipeline.editorial import is_overlapping_otio_ranges - -from ayon_hiero import api as phiero -from ayon_hiero.api.otio import hiero_export - -import hiero -# # developer reload modules -from pprint import pformat - - -class PrecollectInstances(pyblish.api.ContextPlugin): - """Collect all Track items selection.""" - - order = pyblish.api.CollectorOrder - 0.49 - label = "Precollect Instances" - hosts = ["hiero"] - - audio_track_items = [] - - def process(self, context): - self.otio_timeline = context.data["otioTimeline"] - timeline_selection = phiero.get_timeline_selection() - selected_timeline_items = phiero.get_track_items( - selection=timeline_selection, - check_tagged=True, - check_enabled=True - ) - - # only return enabled track items - if not selected_timeline_items: - selected_timeline_items = phiero.get_track_items( - check_enabled=True, check_tagged=True) - - self.log.info( - "Processing enabled track items: {}".format( - selected_timeline_items)) - - # add all tracks subtreck effect items to context - all_tracks = hiero.ui.activeSequence().videoTracks() - tracks_effect_items = self.collect_sub_track_items(all_tracks) - context.data["tracksEffectItems"] = tracks_effect_items - - # process all selected timeline track items - for track_item in selected_timeline_items: - data = {} - clip_name = track_item.name() - source_clip = track_item.source() - self.log.debug("clip_name: {}".format(clip_name)) - - # get openpype tag data - tag_data = phiero.get_trackitem_openpype_data(track_item) - self.log.debug("__ tag_data: {}".format(pformat(tag_data))) - - if not tag_data: - continue - - if tag_data.get("id") not in { - AYON_INSTANCE_ID, AVALON_INSTANCE_ID - }: - continue - - # get clips subtracks and annotations - annotations = self.clip_annotations(source_clip) - subtracks = self.clip_subtrack(track_item) - self.log.debug("Annotations: {}".format(annotations)) - self.log.debug(">> Subtracks: {}".format(subtracks)) - - # solve handles length - tag_data["handleStart"] = min( - tag_data["handleStart"], int(track_item.handleInLength())) - tag_data["handleEnd"] = min( - tag_data["handleEnd"], int(track_item.handleOutLength())) - - # add audio to families - with_audio = False - if tag_data.pop("audio"): - with_audio = True - - # add tag data to instance data - data.update({ - k: v for k, v in tag_data.items() - if k not in ("id", "applieswhole", "label") - }) - # Backward compatibility fix of 'entity_type' > 'folder_type' - if "parents" in data: - for parent in data["parents"]: - if "entity_type" in parent: - parent["folder_type"] = parent.pop("entity_type") - - folder_path, folder_name = self._get_folder_data(tag_data) - - families = [str(f) for f in tag_data["families"]] - - # TODO: remove backward compatibility - product_name = tag_data.get("productName") - if product_name is None: - # backward compatibility: subset -> productName - product_name = tag_data.get("subset") - - # backward compatibility: product_name should not be missing - if not product_name: - self.log.error( - "Product name is not defined for: {}".format(folder_path)) - - # TODO: remove backward compatibility - product_type = tag_data.get("productType") - if product_type is None: - # backward compatibility: family -> productType - product_type = tag_data.get("family") - - # backward compatibility: product_type should not be missing - if not product_type: - self.log.error( - "Product type is not defined for: {}".format(folder_path)) - - # form label - label = "{} -".format(folder_path) - if folder_name != clip_name: - label += " ({})".format(clip_name) - label += " {}".format(product_name) - - data.update({ - "name": "{}_{}".format(folder_path, product_name), - "label": label, - "productName": product_name, - "productType": product_type, - "folderPath": folder_path, - "asset_name": folder_name, - "item": track_item, - "families": families, - "publish": tag_data["publish"], - "fps": context.data["fps"], - - # clip's effect - "clipEffectItems": subtracks, - "clipAnnotations": annotations, - - # add all additional tags - "tags": phiero.get_track_item_tags(track_item), - "newHierarchyIntegration": True, - # Backwards compatible (Deprecated since 24/06/06) - "newAssetPublishing": True, - }) - - # otio clip data - otio_data = self.get_otio_clip_instance_data(track_item) or {} - self.log.debug("__ otio_data: {}".format(pformat(otio_data))) - data.update(otio_data) - self.log.debug("__ data: {}".format(pformat(data))) - - # add resolution - self.get_resolution_to_data(data, context) - - # create instance - instance = context.create_instance(**data) - - # add colorspace data - version_data = instance.data.setdefault("versionData", {}) - version_data["colorSpace"] = track_item.sourceMediaColourTransform() - - # create shot instance for shot attributes create/update - self.create_shot_instance(context, **data) - - self.log.info("Creating instance: {}".format(instance)) - self.log.info( - "_ instance.data: {}".format(pformat(instance.data))) - - if not with_audio: - continue - - # create audio product instance - self.create_audio_instance(context, **data) - - # add audioReview attribute to plate instance data - # if reviewTrack is on - if tag_data.get("reviewTrack") is not None: - instance.data["reviewAudio"] = True - - def get_resolution_to_data(self, data, context): - assert data.get("otioClip"), "Missing `otioClip` data" - - # solve source resolution option - if data.get("sourceResolution", None): - otio_clip_metadata = data[ - "otioClip"].media_reference.metadata - data.update({ - "resolutionWidth": otio_clip_metadata[ - "openpype.source.width"], - "resolutionHeight": otio_clip_metadata[ - "openpype.source.height"], - "pixelAspect": otio_clip_metadata[ - "openpype.source.pixelAspect"] - }) - else: - otio_tl_metadata = context.data["otioTimeline"].metadata - data.update({ - "resolutionWidth": otio_tl_metadata["openpype.timeline.width"], - "resolutionHeight": otio_tl_metadata[ - "openpype.timeline.height"], - "pixelAspect": otio_tl_metadata[ - "openpype.timeline.pixelAspect"] - }) - - def create_shot_instance(self, context, **data): - product_name = "shotMain" - master_layer = data.get("heroTrack") - hierarchy_data = data.get("hierarchyData") - item = data.get("item") - clip_name = item.name() - - if not master_layer: - return - - if not hierarchy_data: - return - - folder_path = data["folderPath"] - folder_name = data["asset_name"] - - product_type = "shot" - - # form label - label = "{} -".format(folder_path) - if folder_name != clip_name: - label += " ({}) ".format(clip_name) - label += " {}".format(product_name) - - data.update({ - "name": "{}_{}".format(folder_path, product_name), - "label": label, - "productName": product_name, - "productType": product_type, - "family": product_type, - "families": [product_type], - "integrate": False, - }) - - instance = context.create_instance(**data) - self.log.info("Creating instance: {}".format(instance)) - self.log.debug( - "_ instance.data: {}".format(pformat(instance.data))) - - def _get_folder_data(self, data): - folder_path = data.pop("folderPath", None) - - if data.get("asset_name"): - folder_name = data["asset_name"] - else: - folder_name = data["asset"] - - # backward compatibility for clip tags - # which are missing folderPath key - # TODO remove this in future versions - if not folder_path: - hierarchy_path = data["hierarchy"] - folder_path = "/{}/{}".format( - hierarchy_path, - folder_name - ) - - return folder_path, folder_name - - def create_audio_instance(self, context, **data): - product_name = "audioMain" - master_layer = data.get("heroTrack") - - if not master_layer: - return - - item = data.get("item") - clip_name = item.name() - - # test if any audio clips - if not self.test_any_audio(item): - return - - folder_path = data["folderPath"] - asset_name = data["asset_name"] - - product_type = "audio" - - # form label - label = "{} -".format(folder_path) - if asset_name != clip_name: - label += " ({}) ".format(clip_name) - label += " {}".format(product_name) - - data.update({ - "name": "{}_{}".format(folder_path, product_name), - "label": label, - "productName": product_name, - "productType": product_type, - "family": product_type, - "families": [product_type, "clip"] - }) - # remove review track attr if any - data.pop("reviewTrack") - - # create instance - instance = context.create_instance(**data) - self.log.info("Creating instance: {}".format(instance)) - self.log.debug( - "_ instance.data: {}".format(pformat(instance.data))) - - def test_any_audio(self, track_item): - # collect all audio tracks to class variable - if not self.audio_track_items: - for otio_clip in self.otio_timeline.each_clip(): - if otio_clip.parent().kind != "Audio": - continue - self.audio_track_items.append(otio_clip) - - # get track item timeline range - timeline_range = self.create_otio_time_range_from_timeline_item_data( - track_item) - - # loop through audio track items and search for overlapping clip - for otio_audio in self.audio_track_items: - parent_range = otio_audio.range_in_parent() - - # if any overaling clip found then return True - if is_overlapping_otio_ranges( - parent_range, timeline_range, strict=False): - return True - - def get_otio_clip_instance_data(self, track_item): - """ - Return otio objects for timeline, track and clip - - Args: - timeline_item_data (dict): timeline_item_data from list returned by - resolve.get_current_timeline_items() - otio_timeline (otio.schema.Timeline): otio object - - Returns: - dict: otio clip object - - """ - ti_track_name = track_item.parent().name() - timeline_range = self.create_otio_time_range_from_timeline_item_data( - track_item) - for otio_clip in self.otio_timeline.each_clip(): - track_name = otio_clip.parent().name - parent_range = otio_clip.range_in_parent() - if ti_track_name != track_name: - continue - if otio_clip.name != track_item.name(): - continue - self.log.debug("__ parent_range: {}".format(parent_range)) - self.log.debug("__ timeline_range: {}".format(timeline_range)) - if is_overlapping_otio_ranges( - parent_range, timeline_range, strict=True): - - # add pypedata marker to otio_clip metadata - for marker in otio_clip.markers: - if phiero.OPENPYPE_TAG_NAME in marker.name: - otio_clip.metadata.update(marker.metadata) - return {"otioClip": otio_clip} - - return None - - @staticmethod - def create_otio_time_range_from_timeline_item_data(track_item): - timeline = phiero.get_current_sequence() - frame_start = int(track_item.timelineIn()) - frame_duration = int(track_item.duration()) - fps = timeline.framerate().toFloat() - - return hiero_export.create_otio_time_range( - frame_start, frame_duration, fps) - - def collect_sub_track_items(self, tracks): - """ - Returns dictionary with track index as key and list of subtracks - """ - # collect all subtrack items - sub_track_items = {} - for track in tracks: - effect_items = track.subTrackItems() - - # skip if no clips on track > need track with effect only - if not effect_items: - continue - - # skip all disabled tracks - if not track.isEnabled(): - continue - - track_index = track.trackIndex() - _sub_track_items = phiero.flatten(effect_items) - - _sub_track_items = list(_sub_track_items) - # continue only if any subtrack items are collected - if not _sub_track_items: - continue - - enabled_sti = [] - # loop all found subtrack items and check if they are enabled - for _sti in _sub_track_items: - # checking if not enabled - if not _sti.isEnabled(): - continue - if isinstance(_sti, hiero.core.Annotation): - continue - # collect the subtrack item - enabled_sti.append(_sti) - - # continue only if any subtrack items are collected - if not enabled_sti: - continue - - # add collection of subtrackitems to dict - sub_track_items[track_index] = enabled_sti - - return sub_track_items - - @staticmethod - def clip_annotations(clip): - """ - Returns list of Clip's hiero.core.Annotation - """ - annotations = [] - subTrackItems = phiero.flatten(clip.subTrackItems()) - annotations += [item for item in subTrackItems if isinstance( - item, hiero.core.Annotation)] - return annotations - - @staticmethod - def clip_subtrack(clip): - """ - Returns list of Clip's hiero.core.SubTrackItem - """ - subtracks = [] - subTrackItems = phiero.flatten(clip.parent().subTrackItems()) - for item in subTrackItems: - if "TimeWarp" in item.name(): - continue - # avoid all annotation - if isinstance(item, hiero.core.Annotation): - continue - # avoid all disabled - if not item.isEnabled(): - continue - subtracks.append(item) - return subtracks diff --git a/client/ayon_hiero/plugins/publish/precollect_workfile.py b/client/ayon_hiero/plugins/publish/precollect_workfile.py deleted file mode 100644 index 1dd21b3..0000000 --- a/client/ayon_hiero/plugins/publish/precollect_workfile.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -import tempfile -from pprint import pformat - -import pyblish.api -from qtpy.QtGui import QPixmap - -import hiero.ui - -from ayon_hiero.api.otio import hiero_export - - -class PrecollectWorkfile(pyblish.api.ContextPlugin): - """Inject the current working file into context""" - - label = "Precollect Workfile" - order = pyblish.api.CollectorOrder - 0.491 - - def process(self, context): - folder_path = context.data["folderPath"] - folder_name = folder_path.split("/")[-1] - - active_timeline = hiero.ui.activeSequence() - project = active_timeline.project() - fps = active_timeline.framerate().toFloat() - - # adding otio timeline to context - otio_timeline = hiero_export.create_otio_timeline() - - # get workfile thumbnail paths - tmp_staging = tempfile.mkdtemp(prefix="pyblish_tmp_") - thumbnail_name = "workfile_thumbnail.png" - thumbnail_path = os.path.join(tmp_staging, thumbnail_name) - - # search for all windows with name of actual sequence - _windows = [w for w in hiero.ui.windowManager().windows() - if active_timeline.name() in w.windowTitle()] - - # export window to thumb path - QPixmap.grabWidget(_windows[-1]).save(thumbnail_path, 'png') - - # thumbnail - thumb_representation = { - 'files': thumbnail_name, - 'stagingDir': tmp_staging, - 'name': "thumbnail", - 'thumbnail': True, - 'ext': "png" - } - - # get workfile paths - current_file = project.path() - staging_dir, base_name = os.path.split(current_file) - - # creating workfile representation - workfile_representation = { - 'name': 'hrox', - 'ext': 'hrox', - 'files': base_name, - "stagingDir": staging_dir, - } - product_type = "workfile" - instance_data = { - "label": "{} - {}Main".format( - folder_path, product_type), - "name": "{}_{}".format(folder_name, product_type), - "folderPath": folder_path, - # TODO use 'get_product_name' - "productName": "{}{}Main".format( - folder_name, product_type.capitalize() - ), - "item": project, - "productType": product_type, - "family": product_type, - "families": [product_type], - "representations": [workfile_representation, thumb_representation] - } - - # create instance with workfile - instance = context.create_instance(**instance_data) - - # update context with main project attributes - context_data = { - "activeProject": project, - "activeTimeline": active_timeline, - "otioTimeline": otio_timeline, - "currentFile": current_file, - "colorspace": self.get_colorspace(project), - "fps": fps - } - self.log.debug("__ context_data: {}".format(pformat(context_data))) - context.data.update(context_data) - - self.log.info("Creating instance: {}".format(instance)) - self.log.debug("__ instance.data: {}".format(pformat(instance.data))) - self.log.debug("__ context_data: {}".format(pformat(context_data))) - - def get_colorspace(self, project): - # get workfile's colorspace properties - return { - "useOCIOEnvironmentOverride": project.useOCIOEnvironmentOverride(), - "lutSetting16Bit": project.lutSetting16Bit(), - "lutSetting8Bit": project.lutSetting8Bit(), - "lutSettingFloat": project.lutSettingFloat(), - "lutSettingLog": project.lutSettingLog(), - "lutSettingViewer": project.lutSettingViewer(), - "lutSettingWorkingSpace": project.lutSettingWorkingSpace(), - "lutUseOCIOForExport": project.lutUseOCIOForExport(), - "ocioConfigName": project.ocioConfigName(), - "ocioConfigPath": project.ocioConfigPath() - }