diff --git a/server_addon/traypublisher/client/ayon_traypublisher/__init__.py b/server_addon/traypublisher/client/ayon_traypublisher/__init__.py deleted file mode 100644 index 52a433b5e1..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .version import __version__ -from .addon import TrayPublishAddon - - -__all__ = ( - "__version__", - - "TrayPublishAddon", -) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/addon.py b/server_addon/traypublisher/client/ayon_traypublisher/addon.py deleted file mode 100644 index dd78a70b59..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/addon.py +++ /dev/null @@ -1,122 +0,0 @@ -import os - -from pathlib import Path -from ayon_core.lib import get_ayon_launcher_args -from ayon_core.lib.execute import run_detached_process -from ayon_core.addon import ( - click_wrap, - AYONAddon, - ITrayAction, - IHostAddon, -) - -from .version import __version__ - -TRAYPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) - - -class TrayPublishAddon(AYONAddon, IHostAddon, ITrayAction): - label = "Publisher" - name = "traypublisher" - version = __version__ - host_name = "traypublisher" - - def initialize(self, settings): - self.publish_paths = [ - os.path.join(TRAYPUBLISH_ROOT_DIR, "plugins", "publish") - ] - - def tray_init(self): - return - - def on_action_trigger(self): - self.run_traypublisher() - - def connect_with_addons(self, enabled_addons): - """Collect publish paths from other addons.""" - publish_paths = self.manager.collect_plugin_paths()["publish"] - self.publish_paths.extend(publish_paths) - - def run_traypublisher(self): - args = get_ayon_launcher_args( - "addon", self.name, "launch" - ) - run_detached_process(args) - - def cli(self, click_group): - click_group.add_command(cli_main.to_click_obj()) - - -@click_wrap.group( - TrayPublishAddon.name, - help="TrayPublisher related commands.") -def cli_main(): - pass - - -@cli_main.command() -def launch(): - """Launch TrayPublish tool UI.""" - - from ayon_traypublisher import ui - - ui.main() - - -@cli_main.command() -@click_wrap.option( - "--filepath", - help="Full path to CSV file with data", - type=str, - required=True -) -@click_wrap.option( - "--project", - help="Project name in which the context will be used", - type=str, - required=True -) -@click_wrap.option( - "--folder-path", - help="Asset name in which the context will be used", - type=str, - required=True -) -@click_wrap.option( - "--task", - help="Task name under Asset in which the context will be used", - type=str, - required=False -) -@click_wrap.option( - "--ignore-validators", - help="Option to ignore validators", - type=bool, - is_flag=True, - required=False -) -def ingestcsv( - filepath, - project, - folder_path, - task, - ignore_validators -): - """Ingest CSV file into project. - - This command will ingest CSV file into project. CSV file must be in - specific format. See documentation for more information. - """ - from .csv_publish import csvpublish - - # use Path to check if csv_filepath exists - if not Path(filepath).exists(): - raise FileNotFoundError(f"File {filepath} does not exist.") - - csvpublish( - filepath, - project, - folder_path, - task, - ignore_validators - ) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/api/__init__.py b/server_addon/traypublisher/client/ayon_traypublisher/api/__init__.py deleted file mode 100644 index 4e7284b09a..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/api/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .pipeline import ( - TrayPublisherHost, -) - - -__all__ = ( - "TrayPublisherHost", -) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/api/editorial.py b/server_addon/traypublisher/client/ayon_traypublisher/api/editorial.py deleted file mode 100644 index c71dae336c..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/api/editorial.py +++ /dev/null @@ -1,363 +0,0 @@ -import re -from copy import deepcopy - -import ayon_api - -from ayon_core.pipeline.create import CreatorError - - -class ShotMetadataSolver: - """ Solving hierarchical metadata - - Used during editorial publishing. Works with input - clip name and settings defining python formatable - template. Settings also define searching patterns - and its token keys used for formatting in templates. - """ - - NO_DECOR_PATERN = re.compile(r"\{([a-z]*?)\}") - - def __init__(self, logger): - self.clip_name_tokenizer = [] - self.shot_rename = { - "enabled": False, - "shot_rename_template": "", - } - self.shot_hierarchy = { - "enabled": False, - "parents": [], - "parents_path": "", - } - self.shot_add_tasks = [] - self.log = logger - - def update_data( - self, - clip_name_tokenizer, - shot_rename, - shot_hierarchy, - shot_add_tasks - ): - self.clip_name_tokenizer = clip_name_tokenizer - self.shot_rename = shot_rename - self.shot_hierarchy = shot_hierarchy - self.shot_add_tasks = shot_add_tasks - - def _rename_template(self, data): - """Shot renaming function - - Args: - data (dict): formatting data - - Raises: - CreatorError: If missing keys - - Returns: - str: formatted new name - """ - shot_rename_template = self.shot_rename[ - "shot_rename_template"] - try: - # format to new shot name - return shot_rename_template.format(**data) - except KeyError as _error: - raise CreatorError(( - "Make sure all keys in settings are correct:: \n\n" - f"From template string {shot_rename_template} > " - f"`{_error}` has no equivalent in \n" - f"{list(data.keys())} input formatting keys!" - )) - - def _generate_tokens(self, clip_name, source_data): - """Token generator - - Settings defines token pairs key and regex expression. - - Args: - clip_name (str): name of clip in editorial - source_data (dict): data for formatting - - Raises: - CreatorError: if missing key - - Returns: - dict: updated source_data - """ - output_data = deepcopy(source_data["anatomy_data"]) - output_data["clip_name"] = clip_name - - if not self.clip_name_tokenizer: - return output_data - - parent_name = source_data["selected_folder_entity"]["name"] - - search_text = parent_name + clip_name - - for clip_name_item in self.clip_name_tokenizer: - token_key = clip_name_item["name"] - pattern = clip_name_item["regex"] - p = re.compile(pattern) - match = p.findall(search_text) - if not match: - raise CreatorError(( - "Make sure regex expression works with your data: \n\n" - f"'{token_key}' with regex '{pattern}' in your settings\n" - "can't find any match in your clip name " - f"'{search_text}'!\n\nLook to: " - "'project_settings/traypublisher/editorial_creators" - "/editorial_simple/clip_name_tokenizer'\n" - "at your project settings..." - )) - - # QUESTION:how to refactor `match[-1]` to some better way? - output_data[token_key] = match[-1] - - return output_data - - def _create_parents_from_settings(self, parents, data): - """formatting parent components. - - Args: - parents (list): list of dict parent components - data (dict): formatting data - - Raises: - CreatorError: missing formatting key - CreatorError: missing token key - KeyError: missing parent token - - Returns: - list: list of dict of parent components - """ - # fill the parents parts from presets - shot_hierarchy = deepcopy(self.shot_hierarchy) - hierarchy_parents = shot_hierarchy["parents"] - - # fill parent keys data template from anatomy data - try: - _parent_tokens_formatting_data = { - parent_token["name"]: parent_token["value"].format(**data) - for parent_token in hierarchy_parents - } - except KeyError as _error: - raise CreatorError(( - "Make sure all keys in settings are correct : \n" - f"`{_error}` has no equivalent in \n{list(data.keys())}" - )) - - _parent_tokens_type = { - parent_token["name"]: parent_token["parent_type"] - for parent_token in hierarchy_parents - } - for _index, _parent in enumerate( - shot_hierarchy["parents_path"].split("/") - ): - # format parent token with value which is formatted - try: - parent_name = _parent.format( - **_parent_tokens_formatting_data) - except KeyError as _error: - raise CreatorError(( - "Make sure all keys in settings are correct:\n\n" - f"`{_error}` from template string" - f" {shot_hierarchy['parents_path']}," - f" has no equivalent in" - f"\n{list(_parent_tokens_formatting_data.keys())} parents" - )) - - parent_token_name = ( - self.NO_DECOR_PATERN.findall(_parent).pop()) - - if not parent_token_name: - raise KeyError( - f"Parent token is not found in: `{_parent}`") - - # find parent type - parent_token_type = _parent_tokens_type[parent_token_name] - - # in case selected context is set to the same folder - # TODO keep index with 'parents' - name check is not enough - if ( - _index == 0 - and parents[-1]["entity_name"] == parent_name - ): - continue - - # in case first parent is project then start parents from start - if ( - _index == 0 - and parent_token_type.lower() == "project" - ): - project_parent = parents[0] - parents = [project_parent] - continue - - parents.append({ - "entity_type": "folder", - "folder_type": parent_token_type.lower(), - "entity_name": parent_name - }) - - return parents - - def _create_hierarchy_path(self, parents): - """Converting hierarchy path from parents - - Args: - parents (list): list of dict parent components - - Returns: - str: hierarchy path - """ - return "/".join( - [ - p["entity_name"] for p in parents - if p["entity_type"] != "project" - ] - ) if parents else "" - - def _get_parents_from_selected_folder( - self, - project_entity, - folder_entity, - ): - """Returning parents from context on selected folder. - - Context defined in Traypublisher project tree. - - Args: - project_entity (dict[str, Any]): Project entity. - folder_entity (dict[str, Any]): Selected folder entity. - - Returns: - list: list of dict parent components - """ - - project_name = project_entity["name"] - path_entries = folder_entity["path"].split("/") - subpaths = [] - subpath_items = [] - for name in path_entries: - subpath_items.append(name) - if name: - subpaths.append("/".join(subpath_items)) - # Remove last name because we already have folder entity - subpaths.pop(-1) - - folder_entity_by_path = {} - if subpaths: - folder_entity_by_path = { - parent_folder["path"]: parent_folder - for parent_folder in ayon_api.get_folders( - project_name, folder_paths=subpaths - ) - } - folders_hierarchy = [ - folder_entity_by_path[folder_path] - for folder_path in subpaths - ] - folders_hierarchy.append(folder_entity) - - # add current selection context hierarchy - output = [{ - "entity_type": "project", - "entity_name": project_name, - }] - for entity in folders_hierarchy: - output.append({ - "entity_type": "folder", - "folder_type": entity["folderType"], - "entity_name": entity["name"] - }) - return output - - def _generate_tasks_from_settings(self, project_entity): - """Convert settings inputs to task data. - - Args: - project_entity (dict): Project entity. - - Raises: - KeyError: Missing task type in project doc - - Returns: - dict: tasks data - """ - tasks_to_add = {} - - project_task_types = project_entity["taskTypes"] - task_type_names = { - task_type["name"] - for task_type in project_task_types - } - for task_item in self.shot_add_tasks: - task_name = task_item["name"] - task_type = task_item["task_type"] - - # check if task type in project task types - if task_type not in task_type_names: - raise KeyError( - "Missing task type `{}` for `{}` is not" - " existing in `{}``".format( - task_type, - task_name, - list(task_type_names) - ) - ) - tasks_to_add[task_name] = {"type": task_type} - - return tasks_to_add - - def generate_data(self, clip_name, source_data): - """Metadata generator. - - Converts input data to hierarchy mentadata. - - Args: - clip_name (str): clip name - source_data (dict): formatting data - - Returns: - (str, dict): shot name and hierarchy data - """ - - tasks = {} - folder_entity = source_data["selected_folder_entity"] - project_entity = source_data["project_entity"] - - # match clip to shot name at start - shot_name = clip_name - - # parse all tokens and generate formatting data - formatting_data = self._generate_tokens(shot_name, source_data) - - # generate parents from selected folder - parents = self._get_parents_from_selected_folder( - project_entity, folder_entity - ) - - if self.shot_rename["enabled"]: - shot_name = self._rename_template(formatting_data) - self.log.info(f"Renamed shot name: {shot_name}") - - if self.shot_hierarchy["enabled"]: - parents = self._create_parents_from_settings( - parents, formatting_data) - - if self.shot_add_tasks: - tasks = self._generate_tasks_from_settings( - project_entity) - - # generate hierarchy path from parents - hierarchy_path = self._create_hierarchy_path(parents) - if hierarchy_path: - folder_path = f"/{hierarchy_path}/{shot_name}" - else: - folder_path = f"/{shot_name}" - - return shot_name, { - "hierarchy": hierarchy_path, - "folderPath": folder_path, - "parents": parents, - "tasks": tasks - } diff --git a/server_addon/traypublisher/client/ayon_traypublisher/api/pipeline.py b/server_addon/traypublisher/client/ayon_traypublisher/api/pipeline.py deleted file mode 100644 index f4526ddf4b..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/api/pipeline.py +++ /dev/null @@ -1,179 +0,0 @@ -import os -import json -import tempfile -import atexit - -import pyblish.api - -from ayon_core.pipeline import ( - register_creator_plugin_path, -) -from ayon_core.host import HostBase, IPublishHost - - -ROOT_DIR = os.path.dirname(os.path.dirname( - os.path.abspath(__file__) -)) -PUBLISH_PATH = os.path.join(ROOT_DIR, "plugins", "publish") -CREATE_PATH = os.path.join(ROOT_DIR, "plugins", "create") - - -class TrayPublisherHost(HostBase, IPublishHost): - name = "traypublisher" - - def install(self): - os.environ["AYON_HOST_NAME"] = self.name - - pyblish.api.register_host("traypublisher") - pyblish.api.register_plugin_path(PUBLISH_PATH) - register_creator_plugin_path(CREATE_PATH) - - def get_context_title(self): - return HostContext.get_project_name() - - def get_context_data(self): - return HostContext.get_context_data() - - def update_context_data(self, data, changes): - HostContext.save_context_data(data) - - def set_project_name(self, project_name): - # TODO Deregister project specific plugins and register new project - # plugins - os.environ["AYON_PROJECT_NAME"] = project_name - HostContext.set_project_name(project_name) - - -class HostContext: - _context_json_path = None - - @staticmethod - def _on_exit(): - if ( - HostContext._context_json_path - and os.path.exists(HostContext._context_json_path) - ): - os.remove(HostContext._context_json_path) - - @classmethod - def get_context_json_path(cls): - if cls._context_json_path is None: - output_file = tempfile.NamedTemporaryFile( - mode="w", prefix="traypub_", suffix=".json" - ) - output_file.close() - cls._context_json_path = output_file.name - atexit.register(HostContext._on_exit) - print(cls._context_json_path) - return cls._context_json_path - - @classmethod - def _get_data(cls, group=None): - json_path = cls.get_context_json_path() - data = {} - if not os.path.exists(json_path): - with open(json_path, "w") as json_stream: - json.dump(data, json_stream) - else: - with open(json_path, "r") as json_stream: - content = json_stream.read() - if content: - data = json.loads(content) - if group is None: - return data - return data.get(group) - - @classmethod - def _save_data(cls, group, new_data): - json_path = cls.get_context_json_path() - data = cls._get_data() - data[group] = new_data - with open(json_path, "w") as json_stream: - json.dump(data, json_stream) - - @classmethod - def add_instance(cls, instance): - instances = cls.get_instances() - instances.append(instance) - cls.save_instances(instances) - - @classmethod - def get_instances(cls): - return cls._get_data("instances") or [] - - @classmethod - def save_instances(cls, instances): - cls._save_data("instances", instances) - - @classmethod - def get_context_data(cls): - return cls._get_data("context") or {} - - @classmethod - def save_context_data(cls, data): - cls._save_data("context", data) - - @classmethod - def get_project_name(cls): - return cls._get_data("project_name") - - @classmethod - def set_project_name(cls, project_name): - cls._save_data("project_name", project_name) - - @classmethod - def get_data_to_store(cls): - return { - "project_name": cls.get_project_name(), - "instances": cls.get_instances(), - "context": cls.get_context_data(), - } - - -def list_instances(): - return HostContext.get_instances() - - -def update_instances(update_list): - updated_instances = {} - for instance, _changes in update_list: - updated_instances[instance.id] = instance.data_to_store() - - instances = HostContext.get_instances() - for instance_data in instances: - instance_id = instance_data["instance_id"] - if instance_id in updated_instances: - new_instance_data = updated_instances[instance_id] - old_keys = set(instance_data.keys()) - new_keys = set(new_instance_data.keys()) - instance_data.update(new_instance_data) - for key in (old_keys - new_keys): - instance_data.pop(key) - - HostContext.save_instances(instances) - - -def remove_instances(instances): - if not isinstance(instances, (tuple, list)): - instances = [instances] - - current_instances = HostContext.get_instances() - for instance in instances: - instance_id = instance.data["instance_id"] - found_idx = None - for idx, _instance in enumerate(current_instances): - if instance_id == _instance["instance_id"]: - found_idx = idx - break - - if found_idx is not None: - current_instances.pop(found_idx) - HostContext.save_instances(current_instances) - - -def get_context_data(): - return HostContext.get_context_data() - - -def update_context_data(data, changes): - HostContext.save_context_data(data) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/api/plugin.py b/server_addon/traypublisher/client/ayon_traypublisher/api/plugin.py deleted file mode 100644 index 973eb65b11..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/api/plugin.py +++ /dev/null @@ -1,337 +0,0 @@ -import ayon_api - -from ayon_core.lib.attribute_definitions import ( - FileDef, - BoolDef, - NumberDef, - UISeparatorDef, -) -from ayon_core.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS -from ayon_core.pipeline.create import ( - Creator, - HiddenCreator, - CreatedInstance, - cache_and_get_instances, - PRE_CREATE_THUMBNAIL_KEY, -) -from .pipeline import ( - list_instances, - update_instances, - remove_instances, - HostContext, -) - -REVIEW_EXTENSIONS = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS) -SHARED_DATA_KEY = "ayon.traypublisher.instances" - - -class HiddenTrayPublishCreator(HiddenCreator): - host_name = "traypublisher" - settings_category = "traypublisher" - - def collect_instances(self): - instances_by_identifier = cache_and_get_instances( - self, SHARED_DATA_KEY, list_instances - ) - for instance_data in instances_by_identifier[self.identifier]: - instance = CreatedInstance.from_existing(instance_data, self) - self._add_instance_to_context(instance) - - def update_instances(self, update_list): - update_instances(update_list) - - def remove_instances(self, instances): - remove_instances(instances) - for instance in instances: - self._remove_instance_from_context(instance) - - def _store_new_instance(self, new_instance): - """Tray publisher specific method to store instance. - - Instance is stored into "workfile" of traypublisher and also add it - to CreateContext. - - Args: - new_instance (CreatedInstance): Instance that should be stored. - """ - - # Host implementation of storing metadata about instance - HostContext.add_instance(new_instance.data_to_store()) - # Add instance to current context - self._add_instance_to_context(new_instance) - - -class TrayPublishCreator(Creator): - create_allow_context_change = True - host_name = "traypublisher" - settings_category = "traypublisher" - - def collect_instances(self): - instances_by_identifier = cache_and_get_instances( - self, SHARED_DATA_KEY, list_instances - ) - for instance_data in instances_by_identifier[self.identifier]: - instance = CreatedInstance.from_existing(instance_data, self) - self._add_instance_to_context(instance) - - def update_instances(self, update_list): - update_instances(update_list) - - def remove_instances(self, instances): - remove_instances(instances) - for instance in instances: - self._remove_instance_from_context(instance) - - def _store_new_instance(self, new_instance): - """Tray publisher specific method to store instance. - - Instance is stored into "workfile" of traypublisher and also add it - to CreateContext. - - Args: - new_instance (CreatedInstance): Instance that should be stored. - """ - - # Host implementation of storing metadata about instance - HostContext.add_instance(new_instance.data_to_store()) - new_instance.mark_as_stored() - - # Add instance to current context - self._add_instance_to_context(new_instance) - - -class SettingsCreator(TrayPublishCreator): - create_allow_context_change = True - create_allow_thumbnail = True - allow_version_control = False - - extensions = [] - - def create(self, product_name, data, pre_create_data): - # Pass precreate data to creator attributes - thumbnail_path = pre_create_data.pop(PRE_CREATE_THUMBNAIL_KEY, None) - - # Fill 'version_to_use' if version control is enabled - if self.allow_version_control: - folder_path = data["folderPath"] - product_entities_by_folder_path = self._prepare_next_versions( - [folder_path], [product_name]) - version = product_entities_by_folder_path[folder_path].get( - product_name - ) - pre_create_data["version_to_use"] = version - data["_previous_last_version"] = version - - data["creator_attributes"] = pre_create_data - data["settings_creator"] = True - - # Create new instance - new_instance = CreatedInstance( - self.product_type, product_name, data, self - ) - - self._store_new_instance(new_instance) - - if thumbnail_path: - self.set_instance_thumbnail_path(new_instance.id, thumbnail_path) - - def _prepare_next_versions(self, folder_paths, product_names): - """Prepare next versions for given folder and product names. - - Todos: - Expect combination of product names by folder path to avoid - unnecessary server calls for unused products. - - Args: - folder_paths (Iterable[str]): Folder paths. - product_names (Iterable[str]): Product names. - - Returns: - dict[str, dict[str, int]]: Last versions by fodler path - and product names. - """ - - # Prepare all versions for all combinations to '1' - # TODO use 'ayon_core.pipeline.version_start' logic - product_entities_by_folder_path = { - folder_path: { - product_name: 1 - for product_name in product_names - } - for folder_path in folder_paths - } - if not folder_paths or not product_names: - return product_entities_by_folder_path - - folder_entities = ayon_api.get_folders( - self.project_name, - folder_paths=folder_paths, - fields={"id", "path"} - ) - folder_paths_by_id = { - folder_entity["id"]: folder_entity["path"] - for folder_entity in folder_entities - } - product_entities = list(ayon_api.get_products( - self.project_name, - folder_ids=folder_paths_by_id.keys(), - product_names=product_names, - fields={"id", "name", "folderId"} - )) - - product_ids = {p["id"] for p in product_entities} - last_versions = ayon_api.get_last_versions( - self.project_name, - product_ids, - fields={"version", "productId"}) - - for product_entity in product_entities: - product_id = product_entity["id"] - product_name = product_entity["name"] - folder_id = product_entity["folderId"] - folder_path = folder_paths_by_id[folder_id] - last_version = last_versions.get(product_id) - version = 0 - if last_version is not None: - version = last_version["version"] - product_entities_by_folder_path[folder_path][product_name] += ( - version - ) - return product_entities_by_folder_path - - def _fill_next_versions(self, instances_data): - """Fill next version for instances. - - Instances have also stored previous next version to be able to - recognize if user did enter different version. If version was - not changed by user, or user set it to '0' the next version will be - updated by current database state. - """ - - filtered_instance_data = [] - for instance in instances_data: - previous_last_version = instance.get("_previous_last_version") - creator_attributes = instance["creator_attributes"] - use_next_version = creator_attributes.get( - "use_next_version", True) - version = creator_attributes.get("version_to_use", 0) - if ( - use_next_version - or version == 0 - or version == previous_last_version - ): - filtered_instance_data.append(instance) - - folder_paths = { - instance["folderPath"] - for instance in filtered_instance_data - } - product_names = { - instance["productName"] - for instance in filtered_instance_data} - product_entities_by_folder_path = self._prepare_next_versions( - folder_paths, product_names - ) - for instance in filtered_instance_data: - folder_path = instance["folderPath"] - product_name = instance["productName"] - version = product_entities_by_folder_path[folder_path][product_name] - instance["creator_attributes"]["version_to_use"] = version - instance["_previous_last_version"] = version - - def collect_instances(self): - """Collect instances from host. - - Overriden to be able to manage version control attributes. If version - control is disabled, the attributes will be removed from instances, - and next versions are filled if is version control enabled. - """ - - instances_by_identifier = cache_and_get_instances( - self, SHARED_DATA_KEY, list_instances - ) - instances = instances_by_identifier[self.identifier] - if not instances: - return - - if self.allow_version_control: - self._fill_next_versions(instances) - - for instance_data in instances: - # Make sure that there are not data related to version control - # if plugin does not support it - if not self.allow_version_control: - instance_data.pop("_previous_last_version", None) - creator_attributes = instance_data["creator_attributes"] - creator_attributes.pop("version_to_use", None) - creator_attributes.pop("use_next_version", None) - - instance = CreatedInstance.from_existing(instance_data, self) - self._add_instance_to_context(instance) - - def get_instance_attr_defs(self): - defs = self.get_pre_create_attr_defs() - if self.allow_version_control: - defs += [ - UISeparatorDef(), - BoolDef( - "use_next_version", - default=True, - label="Use next version", - ), - NumberDef( - "version_to_use", - default=1, - minimum=0, - maximum=999, - label="Version to use", - ) - ] - return defs - - def get_pre_create_attr_defs(self): - # Use same attributes as for instance attributes - return [ - FileDef( - "representation_files", - folders=False, - extensions=self.extensions, - allow_sequences=self.allow_sequences, - single_item=not self.allow_multiple_items, - label="Representations", - ), - FileDef( - "reviewable", - folders=False, - extensions=REVIEW_EXTENSIONS, - allow_sequences=True, - single_item=True, - label="Reviewable representations", - extensions_label="Single reviewable item" - ) - ] - - @classmethod - def from_settings(cls, item_data): - identifier = item_data["identifier"] - product_type = item_data["product_type"] - if not identifier: - identifier = "settings_{}".format(product_type) - return type( - "{}{}".format(cls.__name__, identifier), - (cls, ), - { - "product_type": product_type, - "identifier": identifier, - "label": item_data["label"].strip(), - "icon": item_data["icon"], - "description": item_data["description"], - "detailed_description": item_data["detailed_description"], - "extensions": item_data["extensions"], - "allow_sequences": item_data["allow_sequences"], - "allow_multiple_items": item_data["allow_multiple_items"], - "allow_version_control": item_data.get( - "allow_version_control", False), - "default_variants": item_data["default_variants"], - } - ) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/batch_parsing.py b/server_addon/traypublisher/client/ayon_traypublisher/batch_parsing.py deleted file mode 100644 index fa3c8d5b9a..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/batch_parsing.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Functions to parse asset names, versions from file names""" -import os -import re - -import ayon_api - -from ayon_core.lib import Logger - - -def get_folder_entity_from_filename( - project_name, - source_filename, - version_regex, - all_selected_folder_ids=None -): - """Try to parse out folder name from file name provided. - - Artists might provide various file name formats. - Currently handled: - - chair.mov - - chair_v001.mov - - my_chair_to_upload.mov - """ - version = None - folder_name = os.path.splitext(source_filename)[0] - # Always first check if source filename is directly folder - # (eg. 'chair.mov') - matching_folder_entity = get_folder_by_name_case_not_sensitive( - project_name, folder_name, all_selected_folder_ids) - - if matching_folder_entity is None: - # name contains also a version - matching_folder_entity, version = ( - parse_with_version( - project_name, - folder_name, - version_regex, - all_selected_folder_ids - ) - ) - - if matching_folder_entity is None: - matching_folder_entity = parse_containing( - project_name, - folder_name, - all_selected_folder_ids - ) - - return matching_folder_entity, version - - -def parse_with_version( - project_name, - folder_name, - version_regex, - all_selected_folder_ids=None, - log=None -): - """Try to parse folder name from a file name containing version too - - Eg. 'chair_v001.mov' >> 'chair', 1 - """ - if not log: - log = Logger.get_logger(__name__) - log.debug( - ("Folder entity by \"{}\" was not found, trying version regex.". - format(folder_name))) - - matching_folder_entity = version_number = None - - regex_result = version_regex.findall(folder_name) - if regex_result: - _folder_name, _version_number = regex_result[0] - matching_folder_entity = get_folder_by_name_case_not_sensitive( - project_name, - _folder_name, - all_selected_folder_ids=all_selected_folder_ids - ) - if matching_folder_entity: - version_number = int(_version_number) - - return matching_folder_entity, version_number - - -def parse_containing(project_name, folder_name, all_selected_folder_ids=None): - """Look if file name contains any existing folder name""" - for folder_entity in ayon_api.get_folders( - project_name, - folder_ids=all_selected_folder_ids, - fields={"id", "name"} - ): - if folder_entity["name"].lower() in folder_name.lower(): - return ayon_api.get_folder_by_id( - project_name, - folder_entity["id"] - ) - - -def get_folder_by_name_case_not_sensitive( - project_name, - folder_name, - all_selected_folder_ids=None, - log=None -): - """Handle more cases in file names""" - if not log: - log = Logger.get_logger(__name__) - folder_name = re.compile(folder_name, re.IGNORECASE) - - folder_entities = list(ayon_api.get_folders( - project_name, - folder_ids=all_selected_folder_ids, - folder_names=[folder_name] - )) - - if len(folder_entities) > 1: - log.warning("Too many records found for {}".format( - folder_name)) - return None - - if folder_entities: - return folder_entities.pop() diff --git a/server_addon/traypublisher/client/ayon_traypublisher/csv_publish.py b/server_addon/traypublisher/client/ayon_traypublisher/csv_publish.py deleted file mode 100644 index b7906c5706..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/csv_publish.py +++ /dev/null @@ -1,84 +0,0 @@ -import pyblish.api -import pyblish.util - -from ayon_api import get_folder_by_path, get_task_by_name -from ayon_core.lib.attribute_definitions import FileDefItem -from ayon_core.pipeline import install_host -from ayon_core.pipeline.create import CreateContext - -from ayon_traypublisher.api import TrayPublisherHost - - -def csvpublish( - filepath, - project_name, - folder_path, - task_name=None, - ignore_validators=False -): - """Publish CSV file. - - Args: - filepath (str): Path to CSV file. - project_name (str): Project name. - folder_path (str): Folder path. - task_name (Optional[str]): Task name. - ignore_validators (Optional[bool]): Option to ignore validators. - """ - - # initialization of host - host = TrayPublisherHost() - install_host(host) - - # setting host context into project - host.set_project_name(project_name) - - # form precreate data with field values - file_field = FileDefItem.from_paths([filepath], False).pop().to_dict() - precreate_data = { - "csv_filepath_data": file_field, - } - - # create context initialization - create_context = CreateContext(host, headless=True) - folder_entity = get_folder_by_path( - project_name, - folder_path=folder_path, - ) - - if not folder_entity: - ValueError( - f"Folder path '{folder_path}' doesn't " - f"exists at project '{project_name}'." - ) - - task_entity = get_task_by_name( - project_name, - folder_entity["id"], - task_name, - ) - - if not task_entity: - ValueError( - f"Task name '{task_name}' doesn't " - f"exists at folder '{folder_path}'." - ) - - create_context.create( - "io.ayon.creators.traypublisher.csv_ingest", - "Main", - folder_entity=folder_entity, - task_entity=task_entity, - pre_create_data=precreate_data, - ) - - # publishing context initialization - pyblish_context = pyblish.api.Context() - pyblish_context.data["create_context"] = create_context - - # redefine targets (skip 'local' to disable validators) - if ignore_validators: - targets = ["default", "ingest"] - - # publishing - pyblish.util.publish(context=pyblish_context, targets=targets) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_colorspace_look.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_colorspace_look.py deleted file mode 100644 index 901bd758ba..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_colorspace_look.py +++ /dev/null @@ -1,176 +0,0 @@ -# -*- coding: utf-8 -*- -"""Creator of colorspace look files. - -This creator is used to publish colorspace look files thanks to -production type `ociolook`. All files are published as representation. -""" -from pathlib import Path -import ayon_api - -from ayon_core.lib.attribute_definitions import ( - FileDef, EnumDef, TextDef, UISeparatorDef -) -from ayon_core.pipeline import ( - CreatedInstance, - CreatorError -) -from ayon_core.pipeline import colorspace -from ayon_traypublisher.api.plugin import TrayPublishCreator - - -class CreateColorspaceLook(TrayPublishCreator): - """Creates colorspace look files.""" - - identifier = "io.ayon.creators.traypublisher.colorspace_look" - label = "Colorspace Look" - product_type = "ociolook" - description = "Publishes color space look file." - extensions = [".cc", ".cube", ".3dl", ".spi1d", ".spi3d", ".csp", ".lut"] - enabled = False - - colorspace_items = [ - (None, "Not set") - ] - colorspace_attr_show = False - config_items = None - config_data = None - - def get_detail_description(self): - return """# Colorspace Look - -This creator publishes color space look file (LUT). - """ - - def get_icon(self): - return "mdi.format-color-fill" - - def create(self, product_name, instance_data, pre_create_data): - repr_file = pre_create_data.get("luts_file") - if not repr_file: - raise CreatorError("No files specified") - - files = repr_file.get("filenames") - if not files: - # this should never happen - raise CreatorError("Missing files from representation") - - folder_path = instance_data["folderPath"] - task_name = instance_data["task"] - folder_entity = ayon_api.get_folder_by_path( - self.project_name, folder_path) - - task_entity = None - if task_name: - task_entity = ayon_api.get_task_by_name( - self.project_name, folder_entity["id"], task_name - ) - - product_name = self.get_product_name( - project_name=self.project_name, - folder_entity=folder_entity, - task_entity=task_entity, - variant=instance_data["variant"], - ) - - instance_data["creator_attributes"] = { - "abs_lut_path": ( - Path(repr_file["directory"]) / files[0]).as_posix() - } - - # Create new instance - new_instance = CreatedInstance(self.product_type, product_name, - instance_data, self) - new_instance.transient_data["config_items"] = self.config_items - new_instance.transient_data["config_data"] = self.config_data - - self._store_new_instance(new_instance) - - def collect_instances(self): - super().collect_instances() - for instance in self.create_context.instances: - if instance.creator_identifier == self.identifier: - instance.transient_data["config_items"] = self.config_items - instance.transient_data["config_data"] = self.config_data - - def get_instance_attr_defs(self): - return [ - EnumDef( - "working_colorspace", - self.colorspace_items, - default="Not set", - label="Working Colorspace", - ), - UISeparatorDef( - label="Advanced1" - ), - TextDef( - "abs_lut_path", - label="LUT Path", - ), - EnumDef( - "input_colorspace", - self.colorspace_items, - default="Not set", - label="Input Colorspace", - ), - EnumDef( - "direction", - [ - (None, "Not set"), - ("forward", "Forward"), - ("inverse", "Inverse") - ], - default="Not set", - label="Direction" - ), - EnumDef( - "interpolation", - [ - (None, "Not set"), - ("linear", "Linear"), - ("tetrahedral", "Tetrahedral"), - ("best", "Best"), - ("nearest", "Nearest") - ], - default="Not set", - label="Interpolation" - ), - EnumDef( - "output_colorspace", - self.colorspace_items, - default="Not set", - label="Output Colorspace", - ), - ] - - def get_pre_create_attr_defs(self): - return [ - FileDef( - "luts_file", - folders=False, - extensions=self.extensions, - allow_sequences=False, - single_item=True, - label="Look Files", - ) - ] - - def apply_settings(self, project_settings): - config_data = colorspace.get_current_context_imageio_config_preset( - project_settings=project_settings - ) - if not config_data: - self.enabled = False - return - - filepath = config_data["path"] - config_items = colorspace.get_ocio_config_colorspaces(filepath) - labeled_colorspaces = colorspace.get_colorspaces_enumerator_items( - config_items, - include_aliases=True, - include_roles=True - ) - self.config_items = config_items - self.config_data = config_data - self.colorspace_items.extend(labeled_colorspaces) - self.enabled = True diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py deleted file mode 100644 index fd4dedd48e..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py +++ /dev/null @@ -1,812 +0,0 @@ -import os -import re -import csv -import collections -from io import StringIO -from copy import deepcopy, copy -from typing import Optional, List, Set, Dict, Union, Any - -import clique -import ayon_api - -from ayon_core.pipeline.create import get_product_name -from ayon_core.pipeline import CreatedInstance -from ayon_core.lib import FileDef, BoolDef -from ayon_core.lib.transcoding import ( - VIDEO_EXTENSIONS, IMAGE_EXTENSIONS -) -from ayon_core.pipeline.create import CreatorError -from ayon_traypublisher.api.plugin import TrayPublishCreator - - -def _get_row_value_with_validation( - columns_config: Dict[str, Any], - column_name: str, - row_data: Dict[str, Any], -): - """Get row value with validation""" - - # get column data from column config - column_data = None - for column in columns_config["columns"]: - if column["name"] == column_name: - column_data = column - break - - if not column_data: - raise CreatorError( - f"Column '{column_name}' not found in column config." - ) - - # get column value from row - column_value = row_data.get(column_name) - column_required = column_data["required_column"] - - # check if column value is not empty string and column is required - if column_value == "" and column_required: - raise CreatorError( - f"Value in column '{column_name}' is required." - ) - - # get column type - column_type = column_data["type"] - # get column validation regex - column_validation = column_data["validation_pattern"] - # get column default value - column_default = column_data["default"] - - if column_type in ["number", "decimal"] and column_default == 0: - column_default = None - - # check if column value is not empty string - if column_value == "": - # set default value if column value is empty string - column_value = column_default - - # set column value to correct type following column type - if column_type == "number" and column_value is not None: - column_value = int(column_value) - elif column_type == "decimal" and column_value is not None: - column_value = float(column_value) - elif column_type == "bool": - column_value = column_value in ["true", "True"] - - # check if column value matches validation regex - if ( - column_value is not None and - not re.match(str(column_validation), str(column_value)) - ): - raise CreatorError( - f"Column '{column_name}' value '{column_value}'" - f" does not match validation regex '{column_validation}'" - f"\nRow data: {row_data}" - f"\nColumn data: {column_data}" - ) - - return column_value - - -class RepreItem: - def __init__( - self, - name, - filepath, - frame_start, - frame_end, - handle_start, - handle_end, - fps, - thumbnail_path, - colorspace, - comment, - slate_exists, - tags, - ): - self.name = name - self.filepath = filepath - self.frame_start = frame_start - self.frame_end = frame_end - self.handle_start = handle_start - self.handle_end = handle_end - self.fps = fps - self.thumbnail_path = thumbnail_path - self.colorspace = colorspace - self.comment = comment - self.slate_exists = slate_exists - self.tags = tags - - @classmethod - def from_csv_row(cls, columns_config, repre_config, row): - kwargs = { - dst_key: _get_row_value_with_validation( - columns_config, column_name, row - ) - for dst_key, column_name in ( - # Representation information - ("filepath", "File Path"), - ("frame_start", "Frame Start"), - ("frame_end", "Frame End"), - ("handle_start", "Handle Start"), - ("handle_end", "Handle End"), - ("fps", "FPS"), - - # Optional representation information - ("thumbnail_path", "Version Thumbnail"), - ("colorspace", "Representation Colorspace"), - ("comment", "Version Comment"), - ("name", "Representation"), - ("slate_exists", "Slate Exists"), - ("repre_tags", "Representation Tags"), - ) - } - - # Should the 'int' and 'float' conversion happen? - # - looks like '_get_row_value_with_validation' is already handling it - for key in {"frame_start", "frame_end", "handle_start", "handle_end"}: - kwargs[key] = int(kwargs[key]) - - kwargs["fps"] = float(kwargs["fps"]) - - # Convert tags value to list - tags_list = copy(repre_config["default_tags"]) - repre_tags: Optional[str] = kwargs.pop("repre_tags") - if repre_tags: - tags_list = [] - tags_delimiter = repre_config["tags_delimiter"] - # strip spaces from repre_tags - if tags_delimiter in repre_tags: - tags = repre_tags.split(tags_delimiter) - for _tag in tags: - tags_list.append(_tag.strip().lower()) - else: - tags_list.append(repre_tags) - kwargs["tags"] = tags_list - return cls(**kwargs) - - -class ProductItem: - def __init__( - self, - folder_path: str, - task_name: str, - version: int, - variant: str, - product_type: str, - task_type: Optional[str] = None, - ): - self.folder_path = folder_path - self.task_name = task_name - self.task_type = task_type - self.version = version - self.variant = variant - self.product_type = product_type - self.repre_items: List[RepreItem] = [] - self._unique_name = None - self._pre_product_name = None - - @property - def unique_name(self) -> str: - if self._unique_name is None: - self._unique_name = "/".join([ - self.folder_path, - self.task_name, - f"{self.variant}{self.product_type}{self.version}".replace( - " ", "" - ).lower() - ]) - return self._unique_name - - @property - def instance_name(self): - if self._pre_product_name is None: - self._pre_product_name = ( - f"{self.task_name}{self.variant}" - f"{self.product_type}{self.version}" - ).replace(" ", "").lower() - return self._pre_product_name - - def add_repre_item(self, repre_item: RepreItem): - self.repre_items.append(repre_item) - - @classmethod - def from_csv_row(cls, columns_config, row): - kwargs = { - dst_key: _get_row_value_with_validation( - columns_config, column_name, row - ) - for dst_key, column_name in ( - # Context information - ("folder_path", "Folder Path"), - ("task_name", "Task Name"), - ("version", "Version"), - ("variant", "Variant"), - ("product_type", "Product Type"), - ) - } - return cls(**kwargs) - - -class IngestCSV(TrayPublishCreator): - """CSV ingest creator class""" - - icon = "fa.file" - - label = "CSV Ingest" - product_type = "csv_ingest_file" - identifier = "io.ayon.creators.traypublisher.csv_ingest" - - default_variants = ["Main"] - - description = "Ingest products' data from CSV file" - detailed_description = """ -Ingest products' data from CSV file following column and representation -configuration in project settings. -""" - - # Position in the list of creators. - order = 10 - - # settings for this creator - columns_config = {} - representations_config = {} - - def get_instance_attr_defs(self): - return [ - BoolDef( - "add_review_family", - default=True, - label="Review" - ) - ] - - def get_pre_create_attr_defs(self): - """Creating pre-create attributes at creator plugin. - - Returns: - list: list of attribute object instances - """ - # Use same attributes as for instance attributes - return [ - FileDef( - "csv_filepath_data", - folders=False, - extensions=[".csv"], - allow_sequences=False, - single_item=True, - label="CSV File", - ), - ] - - def create( - self, - product_name: str, - instance_data: Dict[str, Any], - pre_create_data: Dict[str, Any] - ): - """Create product from each row found in the CSV. - - Args: - product_name (str): The subset name. - instance_data (dict): The instance data. - pre_create_data (dict): - """ - - csv_filepath_data = pre_create_data.get("csv_filepath_data", {}) - - csv_dir = csv_filepath_data.get("directory", "") - if not os.path.exists(csv_dir): - raise CreatorError( - f"Directory '{csv_dir}' does not exist." - ) - filename = csv_filepath_data.get("filenames", []) - self._process_csv_file( - product_name, instance_data, csv_dir, filename[0] - ) - - def _pass_data_to_csv_instance( - self, - instance_data: Dict[str, Any], - staging_dir: str, - filename: str - ): - """Pass CSV representation file to instance data""" - - representation = { - "name": "csv", - "ext": "csv", - "files": filename, - "stagingDir": staging_dir, - "stagingDir_persistent": True, - } - - instance_data.update({ - "label": f"CSV: {filename}", - "representations": [representation], - "stagingDir": staging_dir, - "stagingDir_persistent": True, - }) - - def _process_csv_file( - self, - product_name: str, - instance_data: Dict[str, Any], - csv_dir: str, - filename: str - ): - """Process CSV file. - - Args: - product_name (str): The subset name. - instance_data (dict): The instance data. - csv_dir (str): The csv directory. - filename (str): The filename. - - """ - # create new instance from the csv file via self function - self._pass_data_to_csv_instance( - instance_data, - csv_dir, - filename - ) - - csv_instance = CreatedInstance( - self.product_type, product_name, instance_data, self - ) - - csv_instance["csvFileData"] = { - "filename": filename, - "staging_dir": csv_dir, - } - - # create instances from csv data via self function - instances = self._create_instances_from_csv_data(csv_dir, filename) - for instance in instances: - self._store_new_instance(instance) - self._store_new_instance(csv_instance) - - def _resolve_repre_path( - self, csv_dir: str, filepath: Union[str, None] - ) -> Union[str, None]: - if not filepath: - return filepath - - # Validate only existence of file directory as filename - # may contain frame specific char (e.g. '%04d' or '####'). - filedir, filename = os.path.split(filepath) - if not filedir or filedir == ".": - # If filedir is empty or "." then use same directory as - # csv path - filepath = os.path.join(csv_dir, filepath) - - elif not os.path.exists(filedir): - # If filepath does not exist, first try to find it in the - # same directory as the csv file is, but keep original - # value otherwise. - new_filedir = os.path.join(csv_dir, filedir) - if os.path.exists(new_filedir): - filepath = os.path.join(new_filedir, filename) - - return filepath - - def _get_data_from_csv( - self, csv_dir: str, filename: str - ) -> Dict[str, ProductItem]: - """Generate instances from the csv file""" - # get current project name and code from context.data - project_name = self.create_context.get_current_project_name() - csv_path = os.path.join(csv_dir, filename) - - # make sure csv file contains columns from following list - required_columns = [ - column["name"] - for column in self.columns_config["columns"] - if column["required_column"] - ] - - # read csv file - with open(csv_path, "r") as csv_file: - csv_content = csv_file.read() - - # read csv file with DictReader - csv_reader = csv.DictReader( - StringIO(csv_content), - delimiter=self.columns_config["csv_delimiter"] - ) - - # fix fieldnames - # sometimes someone can keep extra space at the start or end of - # the column name - all_columns = [ - " ".join(column.rsplit()) - for column in csv_reader.fieldnames - ] - - # return back fixed fieldnames - csv_reader.fieldnames = all_columns - - # check if csv file contains all required columns - if any(column not in all_columns for column in required_columns): - raise CreatorError( - f"Missing required columns: {required_columns}" - ) - - product_items_by_name: Dict[str, ProductItem] = {} - for row in csv_reader: - _product_item: ProductItem = ProductItem.from_csv_row( - self.columns_config, row - ) - unique_name = _product_item.unique_name - if unique_name not in product_items_by_name: - product_items_by_name[unique_name] = _product_item - product_item: ProductItem = product_items_by_name[unique_name] - product_item.add_repre_item( - RepreItem.from_csv_row( - self.columns_config, - self.representations_config, - row - ) - ) - - folder_paths: Set[str] = { - product_item.folder_path - for product_item in product_items_by_name.values() - } - folder_ids_by_path: Dict[str, str] = { - folder_entity["path"]: folder_entity["id"] - for folder_entity in ayon_api.get_folders( - project_name, folder_paths=folder_paths, fields={"id", "path"} - ) - } - missing_paths: Set[str] = folder_paths - set(folder_ids_by_path.keys()) - if missing_paths: - ending = "" if len(missing_paths) == 1 else "s" - joined_paths = "\n".join(sorted(missing_paths)) - raise CreatorError( - f"Folder{ending} not found.\n{joined_paths}" - ) - - task_names: Set[str] = { - product_item.task_name - for product_item in product_items_by_name.values() - } - task_entities_by_folder_id = collections.defaultdict(list) - for task_entity in ayon_api.get_tasks( - project_name, - folder_ids=set(folder_ids_by_path.values()), - task_names=task_names, - fields={"folderId", "name", "taskType"} - ): - folder_id = task_entity["folderId"] - task_entities_by_folder_id[folder_id].append(task_entity) - - missing_tasks: Set[str] = set() - for product_item in product_items_by_name.values(): - folder_path = product_item.folder_path - task_name = product_item.task_name - folder_id = folder_ids_by_path[folder_path] - task_entities = task_entities_by_folder_id[folder_id] - task_entity = next( - ( - task_entity - for task_entity in task_entities - if task_entity["name"] == task_name - ), - None - ) - if task_entity is None: - missing_tasks.add("/".join([folder_path, task_name])) - else: - product_item.task_type = task_entity["taskType"] - - if missing_tasks: - ending = "" if len(missing_tasks) == 1 else "s" - joined_paths = "\n".join(sorted(missing_tasks)) - raise CreatorError( - f"Task{ending} not found.\n{joined_paths}" - ) - - for product_item in product_items_by_name.values(): - repre_paths: Set[str] = set() - duplicated_paths: Set[str] = set() - for repre_item in product_item.repre_items: - # Resolve relative paths in csv file - repre_item.filepath = self._resolve_repre_path( - csv_dir, repre_item.filepath - ) - repre_item.thumbnail_path = self._resolve_repre_path( - csv_dir, repre_item.thumbnail_path - ) - - filepath = repre_item.filepath - if filepath in repre_paths: - duplicated_paths.add(filepath) - repre_paths.add(filepath) - - if duplicated_paths: - ending = "" if len(duplicated_paths) == 1 else "s" - joined_names = "\n".join(sorted(duplicated_paths)) - raise CreatorError( - f"Duplicate filename{ending} in csv file.\n{joined_names}" - ) - - return product_items_by_name - - def _add_thumbnail_repre( - self, - thumbnails: Set[str], - instance: CreatedInstance, - repre_item: RepreItem, - multiple_thumbnails: bool, - ) -> Union[str, None]: - """Add thumbnail to instance. - - Add thumbnail as representation and set 'thumbnailPath' if is not set - yet. - - Args: - thumbnails (Set[str]): Set of all thumbnail paths that should - create representation. - instance (CreatedInstance): Instance from create plugin. - repre_item (RepreItem): Representation item. - multiple_thumbnails (bool): There are multiple representations - with thumbnail. - - Returns: - Uniom[str, None]: Explicit output name for thumbnail - representation. - - """ - if not thumbnails: - return None - - thumbnail_path = repre_item.thumbnail_path - if not thumbnail_path or thumbnail_path not in thumbnails: - return None - - thumbnails.remove(thumbnail_path) - - thumb_dir, thumb_file = os.path.split(thumbnail_path) - thumb_basename, thumb_ext = os.path.splitext(thumb_file) - - # NOTE 'explicit_output_name' and custom repre name was set only - # when 'multiple_thumbnails' is True and 'review' tag is present. - # That was changed to set 'explicit_output_name' is set when - # 'multiple_thumbnails' is True. - # is_reviewable = "review" in repre_item.tags - - repre_name = "thumbnail" - explicit_output_name = None - if multiple_thumbnails: - repre_name = f"thumbnail_{thumb_basename}" - explicit_output_name = repre_item.name - - thumbnail_repre_data = { - "name": repre_name, - "ext": thumb_ext.lstrip("."), - "files": thumb_file, - "stagingDir": thumb_dir, - "stagingDir_persistent": True, - "tags": ["thumbnail", "delete"], - } - if explicit_output_name: - thumbnail_repre_data["outputName"] = explicit_output_name - - instance["prepared_data_for_repres"].append({ - "type": "thumbnail", - "colorspace": None, - "representation": thumbnail_repre_data, - }) - # also add thumbnailPath for ayon to integrate - if not instance.get("thumbnailPath"): - instance["thumbnailPath"] = thumbnail_path - - return explicit_output_name - - def _add_representation( - self, - instance: CreatedInstance, - repre_item: RepreItem, - explicit_output_name: Optional[str] = None - ): - """Get representation data - - Args: - repre_item (RepreItem): Representation item based on csv row. - explicit_output_name (Optional[str]): Explicit output name. - For grouping purposes with reviewable components. - - """ - # get extension of file - basename: str = os.path.basename(repre_item.filepath) - extension: str = os.path.splitext(basename)[-1].lower() - - # validate filepath is having correct extension based on output - repre_config_data: Union[Dict[str, Any], None] = None - for repre in self.representations_config["representations"]: - if repre["name"] == repre_item.name: - repre_config_data = repre - break - - if not repre_config_data: - raise CreatorError( - f"Representation '{repre_item.name}' not found " - "in config representation data." - ) - - validate_extensions: List[str] = repre_config_data["extensions"] - if extension not in validate_extensions: - raise CreatorError( - f"File extension '{extension}' not valid for " - f"output '{validate_extensions}'." - ) - - is_sequence: bool = extension in IMAGE_EXTENSIONS - # convert ### string in file name to %03d - # this is for correct frame range validation - # example: file.###.exr -> file.%03d.exr - if "#" in basename: - padding = len(basename.split("#")) - 1 - basename = basename.replace("#" * padding, f"%0{padding}d") - is_sequence = True - - # make absolute path to file - dirname: str = os.path.dirname(repre_item.filepath) - - # check if dirname exists - if not os.path.isdir(dirname): - raise CreatorError( - f"Directory '{dirname}' does not exist." - ) - - frame_start: Union[int, None] = None - frame_end: Union[int, None] = None - files: Union[str, List[str]] = basename - if is_sequence: - # collect all data from dirname - cols, _ = clique.assemble(list(os.listdir(dirname))) - if not cols: - raise CreatorError( - f"No collections found in directory '{dirname}'." - ) - - col = cols[0] - files = list(col) - frame_start = min(col.indexes) - frame_end = max(col.indexes) - - tags: List[str] = deepcopy(repre_item.tags) - # if slate in repre_data is True then remove one frame from start - if repre_item.slate_exists: - tags.append("has_slate") - - # get representation data - representation_data: Dict[str, Any] = { - "name": repre_item.name, - "ext": extension[1:], - "files": files, - "stagingDir": dirname, - "stagingDir_persistent": True, - "tags": tags, - } - if extension in VIDEO_EXTENSIONS: - representation_data.update({ - "fps": repre_item.fps, - "outputName": repre_item.name, - }) - - if explicit_output_name: - representation_data["outputName"] = explicit_output_name - - if frame_start: - representation_data["frameStart"] = frame_start - if frame_end: - representation_data["frameEnd"] = frame_end - - instance["prepared_data_for_repres"].append({ - "type": "media", - "colorspace": repre_item.colorspace, - "representation": representation_data, - }) - - def _prepare_representations( - self, product_item: ProductItem, instance: CreatedInstance - ): - # Collect thumbnail paths from all representation items - # to check if multiple thumbnails are present. - # Once representation is created for certain thumbnail it is removed - # from the set. - thumbnails: Set[str] = { - repre_item.thumbnail_path - for repre_item in product_item.repre_items - if repre_item.thumbnail_path - } - multiple_thumbnails: bool = len(thumbnails) > 1 - - for repre_item in product_item.repre_items: - explicit_output_name = self._add_thumbnail_repre( - thumbnails, - instance, - repre_item, - multiple_thumbnails, - ) - - # get representation data - self._add_representation( - instance, - repre_item, - explicit_output_name - ) - - def _create_instances_from_csv_data(self, csv_dir: str, filename: str): - """Create instances from csv data""" - # from special function get all data from csv file and convert them - # to new instances - product_items_by_name: Dict[str, ProductItem] = ( - self._get_data_from_csv(csv_dir, filename) - ) - - instances = [] - project_name: str = self.create_context.get_current_project_name() - for product_item in product_items_by_name.values(): - folder_path: str = product_item.folder_path - version: int = product_item.version - product_name: str = get_product_name( - project_name, - product_item.task_name, - product_item.task_type, - self.host_name, - product_item.product_type, - product_item.variant - ) - label: str = f"{folder_path}_{product_name}_v{version:>03}" - - repre_items: List[RepreItem] = product_item.repre_items - first_repre_item: RepreItem = repre_items[0] - version_comment: Union[str, None] = next( - ( - repre_item.comment - for repre_item in repre_items - if repre_item.comment - ), - None - ) - slate_exists: bool = any( - repre_item.slate_exists - for repre_item in repre_items - ) - - families: List[str] = ["csv_ingest"] - if slate_exists: - # adding slate to families mainly for loaders to be able - # to filter out slates - families.append("slate") - - instance_data = { - "name": product_item.instance_name, - "folderPath": folder_path, - "families": families, - "label": label, - "task": product_item.task_name, - "variant": product_item.variant, - "source": "csv", - "frameStart": first_repre_item.frame_start, - "frameEnd": first_repre_item.frame_end, - "handleStart": first_repre_item.handle_start, - "handleEnd": first_repre_item.handle_end, - "fps": first_repre_item.fps, - "version": version, - "comment": version_comment, - "prepared_data_for_repres": [] - } - - # create new instance - new_instance: CreatedInstance = CreatedInstance( - product_item.product_type, - product_name, - instance_data, - self - ) - self._prepare_representations(product_item, new_instance) - instances.append(new_instance) - - return instances diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_editorial.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_editorial.py deleted file mode 100644 index b013ed6864..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_editorial.py +++ /dev/null @@ -1,847 +0,0 @@ -import os -from copy import deepcopy - -import ayon_api -import opentimelineio as otio - -from ayon_traypublisher.api.plugin import ( - TrayPublishCreator, - HiddenTrayPublishCreator -) -from ayon_traypublisher.api.editorial import ( - ShotMetadataSolver -) -from ayon_core.pipeline import CreatedInstance -from ayon_core.lib import ( - get_ffprobe_data, - convert_ffprobe_fps_value, - - FileDef, - TextDef, - NumberDef, - EnumDef, - BoolDef, - UISeparatorDef, - UILabelDef -) - - -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( - "workfile_start_frame", - default=1001, - label="Workfile start frame" - ), - NumberDef( - "handle_start", - default=0, - label="Handle start" - ), - NumberDef( - "handle_end", - default=0, - label="Handle end" - ) -] - - -class EditorialClipInstanceCreatorBase(HiddenTrayPublishCreator): - """Wrapper class for clip product type creators.""" - host_name = "traypublisher" - - def create(self, instance_data, source_data=None): - product_name = instance_data["productName"] - - # Create new instance - new_instance = CreatedInstance( - self.product_type, product_name, instance_data, self - ) - - self._store_new_instance(new_instance) - - return new_instance - - def get_instance_attr_defs(self): - return [ - BoolDef( - "add_review_family", - default=True, - label="Review" - ) - ] - - -class EditorialShotInstanceCreator(EditorialClipInstanceCreatorBase): - """Shot product type class - - The shot metadata instance carrier. - """ - identifier = "editorial_shot" - product_type = "shot" - label = "Editorial Shot" - - def get_instance_attr_defs(self): - instance_attributes = [ - TextDef( - "folderPath", - label="Folder path" - ) - ] - instance_attributes.extend(CLIP_ATTR_DEFS) - return instance_attributes - - -class EditorialPlateInstanceCreator(EditorialClipInstanceCreatorBase): - """Plate product type class - - Plate representation instance. - """ - identifier = "editorial_plate" - product_type = "plate" - label = "Editorial Plate" - - -class EditorialAudioInstanceCreator(EditorialClipInstanceCreatorBase): - """Audio product type class - - Audio representation instance. - """ - identifier = "editorial_audio" - product_type = "audio" - label = "Editorial Audio" - - -class EditorialReviewInstanceCreator(EditorialClipInstanceCreatorBase): - """Review product type class - - Review representation instance. - """ - identifier = "editorial_review" - product_type = "review" - label = "Editorial Review" - - -class EditorialSimpleCreator(TrayPublishCreator): - """Editorial creator class - - Simple workflow creator. This creator only disecting input - video file into clip chunks and then converts each to - defined format defined Settings for each product preset. - - Args: - TrayPublishCreator (Creator): Tray publisher plugin class - """ - - label = "Editorial Simple" - product_type = "editorial" - identifier = "editorial_simple" - default_variants = [ - "main" - ] - description = "Editorial files to generate shots." - detailed_description = """ -Supporting publishing new shots to project -or updating already created. Publishing will create OTIO file. -""" - icon = "fa.file" - product_type_presets = [] - - def __init__(self, *args, **kwargs): - self._shot_metadata_solver = ShotMetadataSolver(self.log) - super(EditorialSimpleCreator, self).__init__(*args, **kwargs) - - def apply_settings(self, project_settings): - editorial_creators = deepcopy( - project_settings["traypublisher"]["editorial_creators"] - ) - creator_settings = editorial_creators.get(self.identifier) - - self._shot_metadata_solver.update_data( - creator_settings["clip_name_tokenizer"], - creator_settings["shot_rename"], - creator_settings["shot_hierarchy"], - creator_settings["shot_add_tasks"] - ) - self.product_type_presets = creator_settings["product_type_presets"] - default_variants = creator_settings.get("default_variants") - if default_variants: - self.default_variants = default_variants - - def create(self, product_name, instance_data, pre_create_data): - allowed_product_type_presets = self._get_allowed_product_type_presets( - pre_create_data) - - product_types = { - item["product_type"] - for item in self.product_type_presets - } - clip_instance_properties = { - k: v - for k, v in pre_create_data.items() - if k != "sequence_filepath_data" - if k not in product_types - } - - folder_path = instance_data["folderPath"] - folder_entity = ayon_api.get_folder_by_path( - self.project_name, folder_path - ) - - if pre_create_data["fps"] == "from_selection": - # get 'fps' from folder attributes - fps = folder_entity["attrib"]["fps"] - else: - fps = float(pre_create_data["fps"]) - - instance_data.update({ - "fps": fps - }) - - # get path of sequence - sequence_path_data = pre_create_data["sequence_filepath_data"] - media_path_data = pre_create_data["media_filepaths_data"] - - sequence_paths = self._get_path_from_file_data( - sequence_path_data, multi=True) - media_path = self._get_path_from_file_data(media_path_data) - - first_otio_timeline = None - for seq_path in sequence_paths: - # get otio timeline - otio_timeline = self._create_otio_timeline( - seq_path, fps) - - # Create all clip instances - clip_instance_properties.update({ - "fps": fps, - "variant": instance_data["variant"] - }) - - # create clip instances - self._get_clip_instances( - folder_entity, - otio_timeline, - media_path, - clip_instance_properties, - allowed_product_type_presets, - os.path.basename(seq_path), - first_otio_timeline, - ) - - if not first_otio_timeline: - # assign otio timeline for multi file to layer - first_otio_timeline = otio_timeline - - # create otio editorial instance - self._create_otio_instance( - product_name, - instance_data, - seq_path, - media_path, - first_otio_timeline - ) - - def _create_otio_instance( - self, - product_name, - data, - sequence_path, - media_path, - otio_timeline - ): - """Otio instance creating function - - Args: - product_name (str): Product name. - data (dict): instance data - sequence_path (str): path to sequence file - media_path (str): path to media file - otio_timeline (otio.Timeline): otio timeline object - """ - # Pass precreate data to creator attributes - data.update({ - "sequenceFilePath": sequence_path, - "editorialSourcePath": media_path, - "otioTimeline": otio.adapters.write_to_string(otio_timeline) - }) - new_instance = CreatedInstance( - self.product_type, product_name, data, self - ) - self._store_new_instance(new_instance) - - def _create_otio_timeline(self, sequence_path, fps): - """Creating otio timeline from sequence path - - Args: - sequence_path (str): path to sequence file - fps (float): frame per second - - Returns: - otio.Timeline: otio timeline object - """ - # get editorial sequence file into otio timeline object - extension = os.path.splitext(sequence_path)[1] - - kwargs = {} - if extension == ".edl": - # EDL has no frame rate embedded so needs explicit - # frame rate else 24 is assumed. - kwargs["rate"] = fps - kwargs["ignore_timecode_mismatch"] = True - - return otio.adapters.read_from_file(sequence_path, **kwargs) - - def _get_path_from_file_data(self, file_path_data, multi=False): - """Converting creator path data to single path string - - Args: - file_path_data (FileDefItem): creator path data inputs - multi (bool): switch to multiple files mode - - Raises: - FileExistsError: in case nothing had been set - - Returns: - str: path string - """ - return_path_list = [] - - - if isinstance(file_path_data, list): - return_path_list = [ - os.path.join(f["directory"], f["filenames"][0]) - for f in file_path_data - ] - - if not return_path_list: - raise FileExistsError( - f"File path was not added: {file_path_data}") - - return return_path_list if multi else return_path_list[0] - - def _get_clip_instances( - self, - folder_entity, - otio_timeline, - media_path, - instance_data, - product_type_presets, - sequence_file_name, - first_otio_timeline=None - ): - """Helping function for creating clip instance - - Args: - folder_entity (dict[str, Any]): Folder entity. - otio_timeline (otio.Timeline): otio timeline object - media_path (str): media file path string - instance_data (dict): clip instance data - product_type_presets (list): list of dict settings product presets - """ - - tracks = [ - track for track in otio_timeline.each_child( - descended_from_type=otio.schema.Track) - if track.kind == "Video" - ] - - # media data for audio stream and reference solving - media_data = self._get_media_source_metadata(media_path) - - for track in tracks: - # set track name - track.name = f"{sequence_file_name} - {otio_timeline.name}" - - try: - track_start_frame = ( - abs(track.source_range.start_time.value) - ) - track_start_frame -= self.timeline_frame_start - except AttributeError: - track_start_frame = 0 - - for otio_clip in track.each_child(): - if not self._validate_clip_for_processing(otio_clip): - continue - - # get available frames info to clip data - self._create_otio_reference(otio_clip, media_path, media_data) - - # convert timeline range to source range - self._restore_otio_source_range(otio_clip) - - base_instance_data = self._get_base_instance_data( - otio_clip, - instance_data, - track_start_frame, - folder_entity - ) - - parenting_data = { - "instance_label": None, - "instance_id": None - } - - for product_type_preset in product_type_presets: - # exclude audio product type if no audio stream - if ( - product_type_preset["product_type"] == "audio" - and not media_data.get("audio") - ): - continue - - self._make_product_instance( - otio_clip, - product_type_preset, - deepcopy(base_instance_data), - parenting_data - ) - - # add track to first otioTimeline if it is in input args - if first_otio_timeline: - first_otio_timeline.tracks.append(deepcopy(track)) - - def _restore_otio_source_range(self, otio_clip): - """Infusing source range. - - Otio clip is missing proper source clip range so - here we add them from from parent timeline frame range. - - Args: - otio_clip (otio.Clip): otio clip object - """ - otio_clip.source_range = otio_clip.range_in_parent() - - def _create_otio_reference( - self, - otio_clip, - media_path, - media_data - ): - """Creating otio reference at otio clip. - - Args: - otio_clip (otio.Clip): otio clip object - media_path (str): media file path string - media_data (dict): media metadata - """ - start_frame = media_data["start_frame"] - frame_duration = media_data["duration"] - fps = media_data["fps"] - - available_range = otio.opentime.TimeRange( - start_time=otio.opentime.RationalTime( - start_frame, fps), - duration=otio.opentime.RationalTime( - frame_duration, fps) - ) - # in case old OTIO or video file create `ExternalReference` - media_reference = otio.schema.ExternalReference( - target_url=media_path, - available_range=available_range - ) - otio_clip.media_reference = media_reference - - def _get_media_source_metadata(self, path): - """Get all available metadata from file - - Args: - path (str): media file path string - - Raises: - AssertionError: ffprobe couldn't read metadata - - Returns: - dict: media file metadata - """ - return_data = {} - - try: - media_data = get_ffprobe_data( - path, self.log - ) - - # get video stream data - video_streams = [] - audio_streams = [] - for stream in media_data["streams"]: - codec_type = stream.get("codec_type") - if codec_type == "audio": - audio_streams.append(stream) - - elif codec_type == "video": - video_streams.append(stream) - - if not video_streams: - raise ValueError( - "Could not find video stream in source file." - ) - - video_stream = video_streams[0] - return_data = { - "video": True, - "start_frame": 0, - "duration": int(video_stream["nb_frames"]), - "fps": float( - convert_ffprobe_fps_value( - video_stream["r_frame_rate"] - ) - ) - } - - # get audio streams data - if audio_streams: - return_data["audio"] = True - - except Exception as exc: - raise AssertionError(( - "FFprobe couldn't read information about input file: " - f"\"{path}\". Error message: {exc}" - )) - - return return_data - - def _make_product_instance( - self, - otio_clip, - product_type_preset, - instance_data, - parenting_data - ): - """Making product instance from input preset - - Args: - otio_clip (otio.Clip): otio clip object - product_type_preset (dict): single product type preset - instance_data (dict): instance data - parenting_data (dict): shot instance parent data - - Returns: - CreatedInstance: creator instance object - """ - product_type = product_type_preset["product_type"] - label = self._make_product_naming( - product_type_preset, - instance_data - ) - instance_data["label"] = label - - # add file extension filter only if it is not shot product type - if product_type == "shot": - instance_data["otioClip"] = ( - otio.adapters.write_to_string(otio_clip)) - c_instance = self.create_context.creators[ - "editorial_shot"].create( - instance_data) - parenting_data.update({ - "instance_label": label, - "instance_id": c_instance.data["instance_id"] - }) - else: - # add review family if defined - instance_data.update({ - "outputFileType": product_type_preset["output_file_type"], - "parent_instance_id": parenting_data["instance_id"], - "creator_attributes": { - "parent_instance": parenting_data["instance_label"], - "add_review_family": product_type_preset.get("review") - } - }) - - creator_identifier = f"editorial_{product_type}" - editorial_clip_creator = self.create_context.creators[ - creator_identifier] - c_instance = editorial_clip_creator.create( - instance_data) - - return c_instance - - def _make_product_naming(self, product_type_preset, instance_data): - """Product name maker - - Args: - product_type_preset (dict): single preset item - instance_data (dict): instance data - - Returns: - str: label string - """ - folder_path = instance_data["creator_attributes"]["folderPath"] - - variant_name = instance_data["variant"] - product_type = product_type_preset["product_type"] - - # get variant name from preset or from inheritance - _variant_name = product_type_preset.get("variant") or variant_name - - # product name - product_name = "{}{}".format( - product_type, _variant_name.capitalize() - ) - label = "{} {}".format( - folder_path, - product_name - ) - - instance_data.update({ - "label": label, - "variant": _variant_name, - "productType": product_type, - "productName": product_name, - }) - - return label - - def _get_base_instance_data( - self, - otio_clip, - instance_data, - track_start_frame, - folder_entity, - ): - """Factoring basic set of instance data. - - Args: - otio_clip (otio.Clip): otio clip object - instance_data (dict): precreate instance data - track_start_frame (int): track start frame - - Returns: - dict: instance data - - """ - parent_folder_path = folder_entity["path"] - parent_folder_name = parent_folder_path.rsplit("/", 1)[-1] - - # get clip instance properties - handle_start = instance_data["handle_start"] - handle_end = instance_data["handle_end"] - timeline_offset = instance_data["timeline_offset"] - workfile_start_frame = instance_data["workfile_start_frame"] - fps = instance_data["fps"] - variant_name = instance_data["variant"] - - # basic unique folder name - clip_name = os.path.splitext(otio_clip.name)[0] - project_entity = ayon_api.get_project(self.project_name) - - shot_name, shot_metadata = self._shot_metadata_solver.generate_data( - clip_name, - { - "anatomy_data": { - "project": { - "name": self.project_name, - "code": project_entity["code"] - }, - "parent": parent_folder_name, - "app": self.host_name - }, - "selected_folder_entity": folder_entity, - "project_entity": project_entity - } - ) - - timing_data = self._get_timing_data( - otio_clip, - timeline_offset, - track_start_frame, - workfile_start_frame - ) - - # create creator attributes - creator_attributes = { - - "workfile_start_frame": workfile_start_frame, - "fps": fps, - "handle_start": int(handle_start), - "handle_end": int(handle_end) - } - # add timing data - creator_attributes.update(timing_data) - - # create base instance data - base_instance_data = { - "shotName": shot_name, - "variant": variant_name, - "task": None, - "newHierarchyIntegration": True, - # Backwards compatible (Deprecated since 24/06/06) - "newAssetPublishing": True, - "trackStartFrame": track_start_frame, - "timelineOffset": timeline_offset, - - # creator_attributes - "creator_attributes": creator_attributes - } - # update base instance data with context data - # and also update creator attributes with context data - creator_attributes["folderPath"] = shot_metadata.pop("folderPath") - base_instance_data["folderPath"] = parent_folder_path - - # add creator attributes to shared instance data - base_instance_data["creator_attributes"] = creator_attributes - # add hierarchy shot metadata - base_instance_data.update(shot_metadata) - - return base_instance_data - - def _get_timing_data( - self, - otio_clip, - timeline_offset, - track_start_frame, - workfile_start_frame - ): - """Returning available timing data - - Args: - otio_clip (otio.Clip): otio clip object - timeline_offset (int): offset value - track_start_frame (int): starting frame input - workfile_start_frame (int): start frame for shot's workfiles - - Returns: - dict: timing metadata - """ - # frame ranges data - clip_in = otio_clip.range_in_parent().start_time.value - clip_in += track_start_frame - clip_out = otio_clip.range_in_parent().end_time_inclusive().value - clip_out += track_start_frame - - # add offset in case there is any - if timeline_offset: - clip_in += timeline_offset - clip_out += timeline_offset - - clip_duration = otio_clip.duration().value - source_in = otio_clip.trimmed_range().start_time.value - source_out = source_in + clip_duration - - # define starting frame for future shot - frame_start = ( - clip_in if workfile_start_frame is None - else workfile_start_frame - ) - frame_end = frame_start + (clip_duration - 1) - - return { - "frameStart": int(frame_start), - "frameEnd": int(frame_end), - "clipIn": int(clip_in), - "clipOut": int(clip_out), - "clipDuration": int(otio_clip.duration().value), - "sourceIn": int(source_in), - "sourceOut": int(source_out) - } - - def _get_allowed_product_type_presets(self, pre_create_data): - """Filter out allowed product type presets. - - Args: - pre_create_data (dict): precreate attributes inputs - - Returns: - list: lit of dict with preset items - """ - return [ - {"product_type": "shot"}, - *[ - preset - for preset in self.product_type_presets - if pre_create_data[preset["product_type"]] - ] - ] - - def _validate_clip_for_processing(self, otio_clip): - """Validate otio clip attributes - - Args: - otio_clip (otio.Clip): otio clip object - - Returns: - bool: True if all passing conditions - """ - if otio_clip.name is None: - return False - - if isinstance(otio_clip, otio.schema.Gap): - return False - - # skip all generators like black empty - if isinstance( - otio_clip.media_reference, - otio.schema.GeneratorReference): - return False - - # Transitions are ignored, because Clips have the full frame - # range. - if isinstance(otio_clip, otio.schema.Transition): - return False - - return True - - def get_pre_create_attr_defs(self): - """Creating pre-create attributes at creator plugin. - - Returns: - list: list of attribute object instances - """ - # Use same attributes as for instance attrobites - attr_defs = [ - FileDef( - "sequence_filepath_data", - folders=False, - extensions=[ - ".edl", - ".xml", - ".aaf", - ".fcpxml" - ], - allow_sequences=False, - single_item=False, - label="Sequence file", - ), - FileDef( - "media_filepaths_data", - folders=False, - extensions=[ - ".mov", - ".mp4", - ".wav" - ], - allow_sequences=False, - single_item=False, - label="Media files", - ), - # TODO: perhaps better would be timecode and fps input - NumberDef( - "timeline_offset", - default=0, - label="Timeline offset" - ), - UISeparatorDef(), - UILabelDef("Clip instance attributes"), - UISeparatorDef() - ] - # add variants swithers - attr_defs.extend( - BoolDef(item["product_type"], label=item["product_type"]) - for item in self.product_type_presets - ) - attr_defs.append(UISeparatorDef()) - - attr_defs.extend(CLIP_ATTR_DEFS) - return attr_defs diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_editorial_package.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_editorial_package.py deleted file mode 100644 index bc003c0601..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_editorial_package.py +++ /dev/null @@ -1,96 +0,0 @@ -from pathlib import Path - -from ayon_core.pipeline import ( - CreatedInstance, -) - -from ayon_core.lib.attribute_definitions import ( - FileDef, - BoolDef, - TextDef, -) -from ayon_traypublisher.api.plugin import TrayPublishCreator - - -class EditorialPackageCreator(TrayPublishCreator): - """Creates instance for OTIO file from published folder. - - Folder contains OTIO file and exported .mov files. Process should publish - whole folder as single `editorial_pkg` product type and (possibly) convert - .mov files into different format and copy them into `publish` `resources` - subfolder. - """ - identifier = "editorial_pkg" - label = "Editorial package" - product_type = "editorial_pkg" - description = "Publish folder with OTIO file and resources" - - # Position batch creator after simple creators - order = 120 - - conversion_enabled = False - - def apply_settings(self, project_settings): - self.conversion_enabled = ( - project_settings["traypublisher"] - ["publish"] - ["ExtractEditorialPckgConversion"] - ["conversion_enabled"] - ) - - def get_icon(self): - return "fa.folder" - - def create(self, product_name, instance_data, pre_create_data): - folder_path = pre_create_data.get("folder_path") - if not folder_path: - return - - instance_data["creator_attributes"] = { - "folder_path": (Path(folder_path["directory"]) / - Path(folder_path["filenames"][0])).as_posix(), - "conversion_enabled": pre_create_data["conversion_enabled"] - } - - # Create new instance - new_instance = CreatedInstance(self.product_type, product_name, - instance_data, self) - self._store_new_instance(new_instance) - - def get_pre_create_attr_defs(self): - # Use same attributes as for instance attributes - return [ - FileDef( - "folder_path", - folders=True, - single_item=True, - extensions=[], - allow_sequences=False, - label="Folder path" - ), - BoolDef("conversion_enabled", - tooltip="Convert to output defined in Settings.", - default=self.conversion_enabled, - label="Convert resources"), - ] - - def get_instance_attr_defs(self): - return [ - TextDef( - "folder_path", - label="Folder path", - disabled=True - ), - BoolDef("conversion_enabled", - tooltip="Convert to output defined in Settings.", - label="Convert resources"), - ] - - def get_detail_description(self): - return """# Publish folder with OTIO file and video clips - - Folder contains OTIO file and exported .mov files. Process should - publish whole folder as single `editorial_pkg` product type and - (possibly) convert .mov files into different format and copy them into - `publish` `resources` subfolder. - """ diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_from_settings.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_from_settings.py deleted file mode 100644 index 13cf92ab10..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_from_settings.py +++ /dev/null @@ -1,22 +0,0 @@ -import os -from ayon_core.lib import Logger -from ayon_core.settings import get_project_settings - -log = Logger.get_logger(__name__) - - -def initialize(): - from ayon_traypublisher.api.plugin import SettingsCreator - - project_name = os.environ["AYON_PROJECT_NAME"] - project_settings = get_project_settings(project_name) - - simple_creators = project_settings["traypublisher"]["simple_creators"] - - global_variables = globals() - for item in simple_creators: - dynamic_plugin = SettingsCreator.from_settings(item) - global_variables[dynamic_plugin.__name__] = dynamic_plugin - - -initialize() diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_movie_batch.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_movie_batch.py deleted file mode 100644 index 77b9b0df0a..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_movie_batch.py +++ /dev/null @@ -1,170 +0,0 @@ -import copy -import os -import re -import collections - -import ayon_api - -from ayon_core.lib import ( - FileDef, - BoolDef, -) -from ayon_core.pipeline import ( - CreatedInstance, -) -from ayon_core.pipeline.create import ( - get_product_name, - TaskNotSetError, -) - -from ayon_traypublisher.api.plugin import TrayPublishCreator -from ayon_traypublisher.batch_parsing import ( - get_folder_entity_from_filename -) - - -class BatchMovieCreator(TrayPublishCreator): - """Creates instances from movie file(s). - - Intended for .mov files, but should work for any video file. - Doesn't handle image sequences though. - """ - identifier = "render_movie_batch" - label = "Batch Movies" - product_type = "render" - description = "Publish batch of video files" - - create_allow_context_change = False - version_regex = re.compile(r"^(.+)_v([0-9]+)$") - # Position batch creator after simple creators - order = 110 - - def apply_settings(self, project_settings): - creator_settings = ( - project_settings["traypublisher"]["create"]["BatchMovieCreator"] - ) - self.default_variants = creator_settings["default_variants"] - self.default_tasks = creator_settings["default_tasks"] - self.extensions = creator_settings["extensions"] - - def get_icon(self): - return "fa.file" - - def create(self, product_name, data, pre_create_data): - file_paths = pre_create_data.get("filepath") - if not file_paths: - return - - data_by_folder_id = collections.defaultdict(list) - for file_info in file_paths: - instance_data = copy.deepcopy(data) - file_name = file_info["filenames"][0] - filepath = os.path.join(file_info["directory"], file_name) - instance_data["creator_attributes"] = {"filepath": filepath} - - folder_entity, version = get_folder_entity_from_filename( - self.project_name, file_name, self.version_regex) - data_by_folder_id[folder_entity["id"]].append( - (instance_data, folder_entity) - ) - - all_task_entities = ayon_api.get_tasks( - self.project_name, task_ids=set(data_by_folder_id.keys()) - ) - task_entity_by_folder_id = collections.defaultdict(dict) - for task_entity in all_task_entities: - folder_id = task_entity["folderId"] - task_name = task_entity["name"].lower() - task_entity_by_folder_id[folder_id][task_name] = task_entity - - for ( - folder_id, (instance_data, folder_entity) - ) in data_by_folder_id.items(): - task_entities_by_name = task_entity_by_folder_id[folder_id] - task_name = None - task_entity = None - for default_task_name in self.default_tasks: - _name = default_task_name.lower() - if _name in task_entities_by_name: - task_name = task_entity["name"] - task_entity = task_entities_by_name[_name] - break - - product_name = self._get_product_name( - self.project_name, task_entity, data["variant"] - ) - - instance_data["folderPath"] = folder_entity["path"] - instance_data["task"] = task_name - - # Create new instance - new_instance = CreatedInstance(self.product_type, product_name, - instance_data, self) - self._store_new_instance(new_instance) - - def _get_product_name(self, project_name, task_entity, variant): - """Create product name according to standard template process""" - host_name = self.create_context.host_name - task_name = task_type = None - if task_entity: - task_name = task_entity["name"] - task_type = task_entity["taskType"] - try: - product_name = get_product_name( - project_name, - task_name, - task_type, - host_name, - self.product_type, - variant, - ) - except TaskNotSetError: - # Create instance with fake task - # - instance will be marked as invalid so it can't be published - # but user have ability to change it - # NOTE: This expect that there is not task 'Undefined' on folder - dumb_value = "Undefined" - product_name = get_product_name( - project_name, - dumb_value, - dumb_value, - host_name, - self.product_type, - variant, - ) - - return product_name - - def get_instance_attr_defs(self): - return [ - BoolDef( - "add_review_family", - default=True, - label="Review" - ) - ] - - def get_pre_create_attr_defs(self): - # Use same attributes as for instance attributes - return [ - FileDef( - "filepath", - folders=False, - single_item=False, - extensions=self.extensions, - allow_sequences=False, - label="Filepath" - ), - BoolDef( - "add_review_family", - default=True, - label="Review" - ) - ] - - def get_detail_description(self): - return """# Publish batch of .mov to multiple folders. - - File names must then contain only folder name, or folder name + version. - (eg. 'chair.mov', 'chair_v001.mov', not really safe `my_chair_v001.mov` - """ diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_online.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_online.py deleted file mode 100644 index 135a11c0c6..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_online.py +++ /dev/null @@ -1,116 +0,0 @@ -# -*- coding: utf-8 -*- -"""Creator of online files. - -Online file retain their original name and use it as product name. To -avoid conflicts, this creator checks if product with this name already -exists under selected folder. -""" -from pathlib import Path - -# import ayon_api - -from ayon_core.lib.attribute_definitions import FileDef, BoolDef -from ayon_core.pipeline import ( - CreatedInstance, - CreatorError -) -from ayon_traypublisher.api.plugin import TrayPublishCreator - - -class OnlineCreator(TrayPublishCreator): - """Creates instance from file and retains its original name.""" - - identifier = "io.ayon.creators.traypublisher.online" - label = "Online" - product_type = "online" - description = "Publish file retaining its original file name" - extensions = [".mov", ".mp4", ".mxf", ".m4v", ".mpg", ".exr", - ".dpx", ".tif", ".png", ".jpg"] - - def get_detail_description(self): - return """# Create file retaining its original file name. - - This will publish files using template helping to retain original - file name and that file name is used as product name. - - Bz default it tries to guard against multiple publishes of the same - file.""" - - def get_icon(self): - return "fa.file" - - def create(self, product_name, instance_data, pre_create_data): - repr_file = pre_create_data.get("representation_file") - if not repr_file: - raise CreatorError("No files specified") - - files = repr_file.get("filenames") - if not files: - # this should never happen - raise CreatorError("Missing files from representation") - - origin_basename = Path(files[0]).stem - - # disable check for existing product with the same name - """ - folder_entity = ayon_api.get_folder_by_path( - self.project_name, instance_data["folderPath"], fields={"id"}) - - if ayon_api.get_product_by_name( - self.project_name, origin_basename, folder_entity["id"], - fields={"id"}): - raise CreatorError(f"product with {origin_basename} already " - "exists in selected folder") - """ - - instance_data["originalBasename"] = origin_basename - product_name = origin_basename - - instance_data["creator_attributes"] = { - "path": (Path(repr_file["directory"]) / files[0]).as_posix() - } - - # Create new instance - new_instance = CreatedInstance(self.product_type, product_name, - instance_data, self) - self._store_new_instance(new_instance) - - def get_instance_attr_defs(self): - return [ - BoolDef( - "add_review_family", - default=True, - label="Review" - ) - ] - - def get_pre_create_attr_defs(self): - return [ - FileDef( - "representation_file", - folders=False, - extensions=self.extensions, - allow_sequences=True, - single_item=True, - label="Representation", - ), - BoolDef( - "add_review_family", - default=True, - label="Review" - ) - ] - - def get_product_name( - self, - project_name, - folder_entity, - task_entity, - variant, - host_name=None, - instance=None - ): - if instance is None: - return "{originalBasename}" - - return instance.data["productName"] diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_app_name.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_app_name.py deleted file mode 100644 index e38d10e70f..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_app_name.py +++ /dev/null @@ -1,13 +0,0 @@ -import pyblish.api - - -class CollectTrayPublisherAppName(pyblish.api.ContextPlugin): - """Collect app name and label.""" - - label = "Collect App Name/Label" - order = pyblish.api.CollectorOrder - 0.5 - hosts = ["traypublisher"] - - def process(self, context): - context.data["appName"] = "tray publisher" - context.data["appLabel"] = "Tray publisher" diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_clip_instances.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_clip_instances.py deleted file mode 100644 index bdf7c05f3d..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_clip_instances.py +++ /dev/null @@ -1,36 +0,0 @@ -from pprint import pformat -import pyblish.api - - -class CollectClipInstance(pyblish.api.InstancePlugin): - """Collect clip instances and resolve its parent""" - - label = "Collect Clip Instances" - order = pyblish.api.CollectorOrder - 0.081 - - hosts = ["traypublisher"] - families = ["plate", "review", "audio"] - - def process(self, instance): - creator_identifier = instance.data["creator_identifier"] - if creator_identifier not in [ - "editorial_plate", - "editorial_audio", - "editorial_review" - ]: - return - - instance.data["families"].append("clip") - - 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 "editorialSourcePath" in instance.context.data.keys(): - instance.data["editorialSourcePath"] = ( - instance.context.data["editorialSourcePath"]) - instance.data["families"].append("trimming") - - self.log.debug(pformat(instance.data)) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_colorspace_look.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_colorspace_look.py deleted file mode 100644 index b194e9bf00..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_colorspace_look.py +++ /dev/null @@ -1,86 +0,0 @@ -import os -from pprint import pformat -import pyblish.api -from ayon_core.pipeline import publish -from ayon_core.pipeline import colorspace - - -class CollectColorspaceLook(pyblish.api.InstancePlugin, - publish.AYONPyblishPluginMixin): - """Collect OCIO colorspace look from LUT file - """ - - label = "Collect Colorspace Look" - order = pyblish.api.CollectorOrder - hosts = ["traypublisher"] - families = ["ociolook"] - - def process(self, instance): - creator_attrs = instance.data["creator_attributes"] - - lut_repre_name = "LUTfile" - file_url = creator_attrs["abs_lut_path"] - file_name = os.path.basename(file_url) - base_name, ext = os.path.splitext(file_name) - - # set output name with base_name which was cleared - # of all symbols and all parts were capitalized - output_name = (base_name.replace("_", " ") - .replace(".", " ") - .replace("-", " ") - .title() - .replace(" ", "")) - - # get config items - config_items = instance.data["transientData"]["config_items"] - config_data = instance.data["transientData"]["config_data"] - - # get colorspace items - converted_color_data = {} - for colorspace_key in [ - "working_colorspace", - "input_colorspace", - "output_colorspace" - ]: - if creator_attrs[colorspace_key]: - color_data = colorspace.convert_colorspace_enumerator_item( - creator_attrs[colorspace_key], config_items) - converted_color_data[colorspace_key] = color_data - else: - converted_color_data[colorspace_key] = None - - # add colorspace to config data - if converted_color_data["working_colorspace"]: - config_data["colorspace"] = ( - converted_color_data["working_colorspace"]["name"] - ) - - # create lut representation data - lut_repre = { - "name": lut_repre_name, - "output": output_name, - "ext": ext.lstrip("."), - "files": file_name, - "stagingDir": os.path.dirname(file_url), - "tags": [] - } - instance.data.update({ - "representations": [lut_repre], - "source": file_url, - "ocioLookWorkingSpace": converted_color_data["working_colorspace"], - "ocioLookItems": [ - { - "name": lut_repre_name, - "ext": ext.lstrip("."), - "input_colorspace": converted_color_data[ - "input_colorspace"], - "output_colorspace": converted_color_data[ - "output_colorspace"], - "direction": creator_attrs["direction"], - "interpolation": creator_attrs["interpolation"], - "config_data": config_data - } - ], - }) - - self.log.debug(pformat(instance.data)) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_csv_ingest_instance_data.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_csv_ingest_instance_data.py deleted file mode 100644 index 33536d0854..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_csv_ingest_instance_data.py +++ /dev/null @@ -1,47 +0,0 @@ -from pprint import pformat -import pyblish.api -from ayon_core.pipeline import publish - - -class CollectCSVIngestInstancesData( - pyblish.api.InstancePlugin, - publish.AYONPyblishPluginMixin, - publish.ColormanagedPyblishPluginMixin -): - """Collect CSV Ingest data from instance. - """ - - label = "Collect CSV Ingest instances data" - order = pyblish.api.CollectorOrder + 0.1 - hosts = ["traypublisher"] - families = ["csv_ingest"] - - def process(self, instance): - - # expecting [(colorspace, repre_data), ...] - prepared_repres_data_items = instance.data[ - "prepared_data_for_repres"] - - for prep_repre_data in prepared_repres_data_items: - type = prep_repre_data["type"] - colorspace = prep_repre_data["colorspace"] - repre_data = prep_repre_data["representation"] - - # thumbnails should be skipped - if type == "media": - # colorspace name is passed from CSV column - self.set_representation_colorspace( - repre_data, instance.context, colorspace - ) - elif type == "media" and colorspace is None: - # TODO: implement colorspace file rules file parsing - self.log.warning( - "Colorspace is not defined in csv for following" - f" representation: {pformat(repre_data)}" - ) - pass - elif type == "thumbnail": - # thumbnails should be skipped - pass - - instance.data["representations"].append(repre_data) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_instances.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_instances.py deleted file mode 100644 index e181d0abe5..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_instances.py +++ /dev/null @@ -1,48 +0,0 @@ -import os -from pprint import pformat -import pyblish.api -import opentimelineio as otio - - -class CollectEditorialInstance(pyblish.api.InstancePlugin): - """Collect data for instances created by settings creators.""" - - label = "Collect Editorial Instances" - order = pyblish.api.CollectorOrder - 0.1 - - hosts = ["traypublisher"] - families = ["editorial"] - - def process(self, instance): - - if "families" not in instance.data: - instance.data["families"] = [] - - if "representations" not in instance.data: - instance.data["representations"] = [] - - fpath = instance.data["sequenceFilePath"] - otio_timeline_string = instance.data.pop("otioTimeline") - otio_timeline = otio.adapters.read_from_string( - otio_timeline_string) - - instance.context.data["otioTimeline"] = otio_timeline - instance.context.data["editorialSourcePath"] = ( - instance.data["editorialSourcePath"]) - - self.log.info(fpath) - - instance.data["stagingDir"] = os.path.dirname(fpath) - - _, ext = os.path.splitext(fpath) - - instance.data["representations"].append({ - "ext": ext[1:], - "name": ext[1:], - "stagingDir": instance.data["stagingDir"], - "files": os.path.basename(fpath) - }) - - self.log.debug("Created Editorial Instance {}".format( - pformat(instance.data) - )) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_package.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_package.py deleted file mode 100644 index fb7d5cd5a1..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_package.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Produces instance.data["editorial_pkg"] data used during integration. - -Requires: - instance.data["creator_attributes"]["path"] - from creator - -Provides: - instance -> editorial_pkg (dict): - folder_path (str) - otio_path (str) - from dragged folder - resource_paths (list) - -""" -import os - -import pyblish.api - -from ayon_core.lib.transcoding import VIDEO_EXTENSIONS - - -class CollectEditorialPackage(pyblish.api.InstancePlugin): - """Collects path to OTIO file and resources""" - - label = "Collect Editorial Package" - order = pyblish.api.CollectorOrder - 0.1 - - hosts = ["traypublisher"] - families = ["editorial_pkg"] - - def process(self, instance): - folder_path = instance.data["creator_attributes"]["folder_path"] - if not folder_path or not os.path.exists(folder_path): - self.log.info(( - "Instance doesn't contain collected existing folder path." - )) - return - - instance.data["editorial_pkg"] = {} - instance.data["editorial_pkg"]["folder_path"] = folder_path - - otio_path, resource_paths = ( - self._get_otio_and_resource_paths(folder_path)) - - instance.data["editorial_pkg"]["otio_path"] = otio_path - instance.data["editorial_pkg"]["resource_paths"] = resource_paths - - def _get_otio_and_resource_paths(self, folder_path): - otio_path = None - resource_paths = [] - - file_names = os.listdir(folder_path) - for filename in file_names: - _, ext = os.path.splitext(filename) - file_path = os.path.join(folder_path, filename) - if ext == ".otio": - otio_path = file_path - elif ext in VIDEO_EXTENSIONS: - resource_paths.append(file_path) - return otio_path, resource_paths diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_reviewable.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_reviewable.py deleted file mode 100644 index 4af4fb94e9..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_reviewable.py +++ /dev/null @@ -1,30 +0,0 @@ -import pyblish.api - - -class CollectEditorialReviewable(pyblish.api.InstancePlugin): - """ Collect review input from user. - - Adds the input to instance data. - """ - - label = "Collect Editorial Reviewable" - order = pyblish.api.CollectorOrder - - families = ["plate", "review", "audio"] - hosts = ["traypublisher"] - - def process(self, instance): - creator_identifier = instance.data["creator_identifier"] - if creator_identifier not in [ - "editorial_plate", - "editorial_audio", - "editorial_review" - ]: - return - - creator_attributes = instance.data["creator_attributes"] - - if creator_attributes["add_review_family"]: - instance.data["families"].append("review") - - self.log.debug("instance.data {}".format(instance.data)) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_explicit_colorspace.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_explicit_colorspace.py deleted file mode 100644 index 12f7557c98..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_explicit_colorspace.py +++ /dev/null @@ -1,101 +0,0 @@ -import pyblish.api -from ayon_core.lib import EnumDef -from ayon_core.pipeline import colorspace -from ayon_core.pipeline import publish -from ayon_core.pipeline.publish import KnownPublishError - - -class CollectColorspace(pyblish.api.InstancePlugin, - publish.AYONPyblishPluginMixin, - publish.ColormanagedPyblishPluginMixin): - """Collect explicit user defined representation colorspaces""" - - label = "Choose representation colorspace" - order = pyblish.api.CollectorOrder + 0.49 - hosts = ["traypublisher"] - families = ["render", "plate", "reference", "image", "online"] - enabled = False - - default_colorspace_items = [ - (None, "Don't override") - ] - colorspace_items = list(default_colorspace_items) - colorspace_attr_show = False - config_items = None - - def process(self, instance): - values = self.get_attr_values_from_data(instance.data) - colorspace_value = values.get("colorspace", None) - if colorspace_value is None: - return - - color_data = colorspace.convert_colorspace_enumerator_item( - colorspace_value, self.config_items) - - colorspace_name = self._colorspace_name_by_type(color_data) - self.log.debug("Explicit colorspace name: {}".format(colorspace_name)) - - context = instance.context - for repre in instance.data.get("representations", {}): - self.set_representation_colorspace( - representation=repre, - context=context, - colorspace=colorspace_name - ) - - def _colorspace_name_by_type(self, colorspace_data): - """ - Returns colorspace name by type - - Arguments: - colorspace_data (dict): colorspace data - - Returns: - str: colorspace name - """ - if colorspace_data["type"] == "colorspaces": - return colorspace_data["name"] - elif colorspace_data["type"] == "roles": - return colorspace_data["colorspace"] - else: - raise KnownPublishError( - ( - "Collecting of colorspace failed. used config is missing " - "colorspace type: '{}' . Please contact your pipeline TD." - ).format(colorspace_data['type']) - ) - - @classmethod - def apply_settings(cls, project_settings): - config_data = colorspace.get_current_context_imageio_config_preset( - project_settings=project_settings - ) - - enabled = False - colorspace_items = list(cls.default_colorspace_items) - config_items = None - if config_data: - enabled = True - filepath = config_data["path"] - config_items = colorspace.get_ocio_config_colorspaces(filepath) - labeled_colorspaces = colorspace.get_colorspaces_enumerator_items( - config_items, - include_aliases=True, - include_roles=True - ) - colorspace_items.extend(labeled_colorspaces) - - cls.config_items = config_items - cls.colorspace_items = colorspace_items - cls.enabled = enabled - - @classmethod - def get_attribute_defs(cls): - return [ - EnumDef( - "colorspace", - cls.colorspace_items, - default="Don't override", - label="Override Colorspace" - ) - ] diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_frame_data_from_folder_entity.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_frame_data_from_folder_entity.py deleted file mode 100644 index 2e564a2e4e..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_frame_data_from_folder_entity.py +++ /dev/null @@ -1,51 +0,0 @@ -import pyblish.api - - -class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin): - """Collect Frame Data From 'folderEntity' found in context. - - Frame range data will only be collected if the keys - are not yet collected for the instance. - """ - - order = pyblish.api.CollectorOrder + 0.491 - label = "Collect Missing Frame Data From Folder" - families = [ - "plate", - "pointcache", - "vdbcache", - "online", - "render", - ] - hosts = ["traypublisher"] - - def process(self, instance): - missing_keys = [] - for key in ( - "fps", - "frameStart", - "frameEnd", - "handleStart", - "handleEnd", - ): - if key not in instance.data: - missing_keys.append(key) - - # Skip the logic if all keys are already collected. - # NOTE: In editorial is not 'folderEntity' filled, so it would crash - # even if we don't need it. - if not missing_keys: - return - - keys_set = [] - folder_attributes = instance.data["folderEntity"]["attrib"] - for key in missing_keys: - if key in folder_attributes: - instance.data[key] = folder_attributes[key] - keys_set.append(key) - - if keys_set: - self.log.debug( - f"Frame range data {keys_set} " - "has been collected from folder entity." - ) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_movie_batch.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_movie_batch.py deleted file mode 100644 index c4f2915ef8..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_movie_batch.py +++ /dev/null @@ -1,48 +0,0 @@ -import os - -import pyblish.api -from ayon_core.pipeline import AYONPyblishPluginMixin - - -class CollectMovieBatch( - pyblish.api.InstancePlugin, AYONPyblishPluginMixin -): - """Collect file url for batch movies and create representation. - - Adds review on instance and to repre.tags based on value of toggle button - on creator. - """ - - label = "Collect Movie Batch Files" - order = pyblish.api.CollectorOrder - - hosts = ["traypublisher"] - - def process(self, instance): - if instance.data.get("creator_identifier") != "render_movie_batch": - return - - creator_attributes = instance.data["creator_attributes"] - - file_url = creator_attributes["filepath"] - file_name = os.path.basename(file_url) - _, ext = os.path.splitext(file_name) - - repre = { - "name": ext[1:], - "ext": ext[1:], - "files": file_name, - "stagingDir": os.path.dirname(file_url), - "tags": [] - } - instance.data["representations"].append(repre) - - if creator_attributes["add_review_family"]: - repre["tags"].append("review") - instance.data["families"].append("review") - if not instance.data.get("thumbnailSource"): - instance.data["thumbnailSource"] = file_url - - instance.data["source"] = file_url - - self.log.debug("instance.data {}".format(instance.data)) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_online_file.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_online_file.py deleted file mode 100644 index 05b00e9516..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_online_file.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -import pyblish.api -from pathlib import Path - - -class CollectOnlineFile(pyblish.api.InstancePlugin): - """Collect online file and retain its file name.""" - label = "Collect Online File" - order = pyblish.api.CollectorOrder - families = ["online"] - hosts = ["traypublisher"] - - def process(self, instance): - file = Path(instance.data["creator_attributes"]["path"]) - review = instance.data["creator_attributes"]["add_review_family"] - instance.data["review"] = review - if "review" not in instance.data["families"]: - instance.data["families"].append("review") - self.log.info(f"Adding review: {review}") - - instance.data["representations"].append( - { - "name": file.suffix.lstrip("."), - "ext": file.suffix.lstrip("."), - "files": file.name, - "stagingDir": file.parent.as_posix(), - "tags": ["review"] if review else [] - } - ) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_review_frames.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_review_frames.py deleted file mode 100644 index 7eceda968a..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_review_frames.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -import pyblish.api - - -class CollectReviewInfo(pyblish.api.InstancePlugin): - """Collect data required for review instances. - - ExtractReview plugin requires frame start/end, fps on instance data which - are missing on instances from TrayPublishes. - - Warning: - This is temporary solution to "make it work". Contains removed changes - from https://github.com/ynput/OpenPype/pull/4383 reduced only for - review instances. - """ - - label = "Collect Review Info" - order = pyblish.api.CollectorOrder + 0.491 - families = ["review"] - hosts = ["traypublisher"] - - def process(self, instance): - folder_entity = instance.data.get("folderEntity") - if instance.data.get("frameStart") is not None or not folder_entity: - self.log.debug("Missing required data on instance") - return - - folder_attributes = folder_entity["attrib"] - # Store collected data for logging - collected_data = {} - for key in ( - "fps", - "frameStart", - "frameEnd", - "handleStart", - "handleEnd", - ): - if key in instance.data or key not in folder_attributes: - continue - value = folder_attributes[key] - collected_data[key] = value - instance.data[key] = value - self.log.debug("Collected data: {}".format(str(collected_data))) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_sequence_frame_data.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_sequence_frame_data.py deleted file mode 100644 index c2894e15ad..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_sequence_frame_data.py +++ /dev/null @@ -1,82 +0,0 @@ -import pyblish.api -import clique - -from ayon_core.pipeline import OptionalPyblishPluginMixin - - -class CollectSequenceFrameData( - pyblish.api.InstancePlugin, - OptionalPyblishPluginMixin -): - """Collect Original Sequence Frame Data - - If the representation includes files with frame numbers, - then set `frameStart` and `frameEnd` for the instance to the - start and end frame respectively - """ - - order = pyblish.api.CollectorOrder + 0.4905 - label = "Collect Original Sequence Frame Data" - families = ["plate", "pointcache", - "vdbcache", "online", - "render"] - hosts = ["traypublisher"] - optional = True - - def process(self, instance): - if not self.is_active(instance.data): - return - - # editorial would fail since they might not be in database yet - new_hierarchy = ( - instance.data.get("newHierarchyIntegration") - # Backwards compatible (Deprecated since 24/06/06) - or instance.data.get("newAssetPublishing") - ) - if new_hierarchy: - self.log.debug("Instance is creating new folders. Skipping.") - return - - frame_data = self.get_frame_data_from_repre_sequence(instance) - - if not frame_data: - # if no dict data skip collecting the frame range data - return - - for key, value in frame_data.items(): - instance.data[key] = value - self.log.debug(f"Collected Frame range data '{key}':{value} ") - - def get_frame_data_from_repre_sequence(self, instance): - repres = instance.data.get("representations") - folder_attributes = instance.data["folderEntity"]["attrib"] - - if repres: - first_repre = repres[0] - if "ext" not in first_repre: - self.log.warning("Cannot find file extension" - " in representation data") - return - - files = first_repre["files"] - if not isinstance(files, list): - files = [files] - - collections, _ = clique.assemble(files) - if not collections: - # No sequences detected and we can't retrieve - # frame range - self.log.debug( - "No sequences detected in the representation data." - " Skipping collecting frame range data.") - return - collection = collections[0] - repres_frames = list(collection.indexes) - - return { - "frameStart": repres_frames[0], - "frameEnd": repres_frames[-1], - "handleStart": 0, - "handleEnd": 0, - "fps": folder_attributes["fps"] - } diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_shot_instances.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_shot_instances.py deleted file mode 100644 index 5a2f5cbc20..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_shot_instances.py +++ /dev/null @@ -1,209 +0,0 @@ -from pprint import pformat -import pyblish.api -import opentimelineio as otio - - -class CollectShotInstance(pyblish.api.InstancePlugin): - """ Collect shot instances - - Resolving its user inputs from creator attributes - to instance data. - """ - - label = "Collect Shot Instances" - order = pyblish.api.CollectorOrder - 0.09 - - hosts = ["traypublisher"] - families = ["shot"] - - SHARED_KEYS = [ - "folderPath", - "fps", - "handleStart", - "handleEnd", - "frameStart", - "frameEnd", - "clipIn", - "clipOut", - "clipDuration", - "sourceIn", - "sourceOut", - "otioClip", - "workfileFrameStart" - ] - - def process(self, instance): - creator_identifier = instance.data["creator_identifier"] - if "editorial" not in creator_identifier: - return - - # get otio clip object - otio_clip = self._get_otio_clip(instance) - instance.data["otioClip"] = otio_clip - - # first solve the inputs from creator attr - data = self._solve_inputs_to_data(instance) - instance.data.update(data) - - # distribute all shared keys to clips instances - self._distribute_shared_data(instance) - self._solve_hierarchy_context(instance) - - self.log.debug(pformat(instance.data)) - - def _get_otio_clip(self, instance): - """ Converts otio string data. - - Convert them to proper otio object - and finds its equivalent at otio timeline. - This process is a hack to support also - resolving parent range. - - Args: - instance (obj): publishing instance - - Returns: - otio.Clip: otio clip object - """ - context = instance.context - # convert otio clip from string to object - otio_clip_string = instance.data.pop("otioClip") - otio_clip = otio.adapters.read_from_string( - otio_clip_string) - - otio_timeline = context.data["otioTimeline"] - - clips = [ - clip for clip in otio_timeline.each_child( - descended_from_type=otio.schema.Clip) - if clip.name == otio_clip.name - if clip.parent().kind == "Video" - ] - - otio_clip = clips.pop() - - return otio_clip - - def _distribute_shared_data(self, instance): - """ Distribute all defined keys. - - All data are shared between all related - instances in context. - - Args: - instance (obj): publishing instance - """ - context = instance.context - - instance_id = instance.data["instance_id"] - - if not context.data.get("editorialSharedData"): - context.data["editorialSharedData"] = {} - - context.data["editorialSharedData"][instance_id] = { - _k: _v for _k, _v in instance.data.items() - if _k in self.SHARED_KEYS - } - - def _solve_inputs_to_data(self, instance): - """ Resolve all user inputs into instance data. - - Args: - instance (obj): publishing instance - - Returns: - dict: instance data updating data - """ - _cr_attrs = instance.data["creator_attributes"] - workfile_start_frame = _cr_attrs["workfile_start_frame"] - frame_start = _cr_attrs["frameStart"] - frame_end = _cr_attrs["frameEnd"] - frame_dur = frame_end - frame_start - - return { - "fps": float(_cr_attrs["fps"]), - "handleStart": _cr_attrs["handle_start"], - "handleEnd": _cr_attrs["handle_end"], - "frameStart": workfile_start_frame, - "frameEnd": workfile_start_frame + frame_dur, - "clipIn": _cr_attrs["clipIn"], - "clipOut": _cr_attrs["clipOut"], - "clipDuration": _cr_attrs["clipDuration"], - "sourceIn": _cr_attrs["sourceIn"], - "sourceOut": _cr_attrs["sourceOut"], - "workfileFrameStart": workfile_start_frame, - "folderPath": _cr_attrs["folderPath"], - } - - def _solve_hierarchy_context(self, instance): - """ Adding hierarchy data to context shared data. - - Args: - instance (obj): publishing instance - """ - context = instance.context - - final_context = ( - context.data["hierarchyContext"] - if context.data.get("hierarchyContext") - else {} - ) - - # get handles - handle_start = int(instance.data["handleStart"]) - handle_end = int(instance.data["handleEnd"]) - - in_info = { - "entity_type": "folder", - "folder_type": "Shot", - "attributes": { - "handleStart": handle_start, - "handleEnd": handle_end, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], - "clipIn": instance.data["clipIn"], - "clipOut": instance.data["clipOut"], - "fps": instance.data["fps"] - }, - "tasks": instance.data["tasks"] - } - - parents = instance.data.get('parents', []) - - folder_name = instance.data["folderPath"].split("/")[-1] - actual = {folder_name: in_info} - - for parent in reversed(parents): - parent_name = parent["entity_name"] - parent_info = { - "entity_type": parent["entity_type"], - "children": actual, - } - if parent_info["entity_type"] == "folder": - parent_info["folder_type"] = parent["folder_type"] - actual = {parent_name: parent_info} - - final_context = self._update_dict(final_context, actual) - - # adding hierarchy context to instance - context.data["hierarchyContext"] = final_context - - def _update_dict(self, ex_dict, new_dict): - """ Recursion function - - Updating nested data with another nested data. - - Args: - ex_dict (dict): nested data - new_dict (dict): nested data - - Returns: - dict: updated nested data - """ - for key in ex_dict: - if key in new_dict and isinstance(ex_dict[key], dict): - new_dict[key] = self._update_dict(ex_dict[key], new_dict[key]) - elif not ex_dict.get(key) or not new_dict.get(key): - new_dict[key] = ex_dict[key] - - return new_dict diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_simple_instances.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_simple_instances.py deleted file mode 100644 index d6e35f4d75..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_simple_instances.py +++ /dev/null @@ -1,272 +0,0 @@ -import os -import tempfile -from pathlib import Path - -import clique -import pyblish.api - - -class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): - """Collect data for instances created by settings creators. - - Plugin create representations for simple instances based - on 'representation_files' attribute stored on instance data. - - There is also possibility to have reviewable representation which can be - stored under 'reviewable' attribute stored on instance data. If there was - already created representation with the same files as 'reviewable' contains - - Representations can be marked for review and in that case is also added - 'review' family to instance families. For review can be marked only one - representation so **first** representation that has extension available - in '_review_extensions' is used for review. - - For instance 'source' is used path from last representation created - from 'representation_files'. - - Set staging directory on instance. That is probably never used because - each created representation has it's own staging dir. - """ - - label = "Collect Settings Simple Instances" - order = pyblish.api.CollectorOrder - 0.49 - - hosts = ["traypublisher"] - - def process(self, instance): - if not instance.data.get("settings_creator"): - return - - instance_label = instance.data["name"] - # Create instance's staging dir in temp - tmp_folder = tempfile.mkdtemp(prefix="traypublisher_") - instance.data["stagingDir"] = tmp_folder - instance.context.data["cleanupFullPaths"].append(tmp_folder) - - self.log.debug(( - "Created temp staging directory for instance {}. {}" - ).format(instance_label, tmp_folder)) - - self._fill_version(instance, instance_label) - - # Store filepaths for validation of their existence - source_filepaths = [] - # Make sure there are no representations with same name - repre_names_counter = {} - # Store created names for logging - repre_names = [] - # Store set of filepaths per each representation - representation_files_mapping = [] - source = self._create_main_representations( - instance, - source_filepaths, - repre_names_counter, - repre_names, - representation_files_mapping - ) - - self._create_review_representation( - instance, - source_filepaths, - repre_names_counter, - repre_names, - representation_files_mapping - ) - source_filepaths = list(set(source_filepaths)) - instance.data["source"] = source - instance.data["sourceFilepaths"] = source_filepaths - - # NOTE: Missing filepaths should not cause crashes (at least not here) - # - if filepaths are required they should crash on validation - if source_filepaths: - # NOTE: Original basename is not handling sequences - # - we should maybe not fill the key when sequence is used? - origin_basename = Path(source_filepaths[0]).stem - instance.data["originalBasename"] = origin_basename - - self.log.debug( - ( - "Created Simple Settings instance \"{}\"" - " with {} representations: {}" - ).format( - instance_label, - len(instance.data["representations"]), - ", ".join(repre_names) - ) - ) - - def _fill_version(self, instance, instance_label): - """Fill instance version under which will be instance integrated. - - Instance must have set 'use_next_version' to 'False' - and 'version_to_use' to version to use. - - Args: - instance (pyblish.api.Instance): Instance to fill version for. - instance_label (str): Label of instance to fill version for. - """ - - creator_attributes = instance.data["creator_attributes"] - use_next_version = creator_attributes.get("use_next_version", True) - # If 'version_to_use' is '0' it means that next version should be used - version_to_use = creator_attributes.get("version_to_use", 0) - if use_next_version or not version_to_use: - return - instance.data["version"] = version_to_use - self.log.debug( - "Version for instance \"{}\" was set to \"{}\"".format( - instance_label, version_to_use)) - - def _create_main_representations( - self, - instance, - source_filepaths, - repre_names_counter, - repre_names, - representation_files_mapping - ): - creator_attributes = instance.data["creator_attributes"] - filepath_items = creator_attributes["representation_files"] - if not isinstance(filepath_items, list): - filepath_items = [filepath_items] - - source = None - for filepath_item in filepath_items: - # Skip if filepath item does not have filenames - if not filepath_item["filenames"]: - continue - - filepaths = { - os.path.join(filepath_item["directory"], filename) - for filename in filepath_item["filenames"] - } - source_filepaths.extend(filepaths) - - source = self._calculate_source(filepaths) - representation = self._create_representation_data( - filepath_item, repre_names_counter, repre_names - ) - instance.data["representations"].append(representation) - representation_files_mapping.append( - (filepaths, representation, source) - ) - return source - - def _create_review_representation( - self, - instance, - source_filepaths, - repre_names_counter, - repre_names, - representation_files_mapping - ): - # Skip review representation creation if there are no representations - # created for "main" part - # - review representation must not be created in that case so - # validation can care about it - if not representation_files_mapping: - self.log.warning(( - "There are missing source representations." - " Creation of review representation was skipped." - )) - return - - creator_attributes = instance.data["creator_attributes"] - review_file_item = creator_attributes["reviewable"] - filenames = review_file_item.get("filenames") - if not filenames: - self.log.debug(( - "Filepath for review is not defined." - " Skipping review representation creation." - )) - return - - item_dir = review_file_item["directory"] - first_filepath = os.path.join(item_dir, filenames[0]) - - filepaths = { - os.path.join(item_dir, filename) - for filename in filenames - } - source_filepaths.extend(filepaths) - # First try to find out representation with same filepaths - # so it's not needed to create new representation just for review - review_representation = None - # Review path (only for logging) - review_path = None - for item in representation_files_mapping: - _filepaths, representation, repre_path = item - if _filepaths == filepaths: - review_representation = representation - review_path = repre_path - break - - if review_representation is None: - self.log.debug("Creating new review representation") - review_path = self._calculate_source(filepaths) - review_representation = self._create_representation_data( - review_file_item, repre_names_counter, repre_names - ) - instance.data["representations"].append(review_representation) - - if "review" not in instance.data["families"]: - instance.data["families"].append("review") - - if not instance.data.get("thumbnailSource"): - instance.data["thumbnailSource"] = first_filepath - - review_representation["tags"].append("review") - - # Adding "review" to representation name since it can clash with main - # representation if they share the same extension. - review_representation["outputName"] = "review" - - self.log.debug("Representation {} was marked for review. {}".format( - review_representation["name"], review_path - )) - - def _create_representation_data( - self, filepath_item, repre_names_counter, repre_names - ): - """Create new representation data based on file item. - - Args: - filepath_item (Dict[str, Any]): Item with information about - representation paths. - repre_names_counter (Dict[str, int]): Store count of representation - names. - repre_names (List[str]): All used representation names. For - logging purposes. - - Returns: - Dict: Prepared base representation data. - """ - - filenames = filepath_item["filenames"] - _, ext = os.path.splitext(filenames[0]) - if len(filenames) == 1: - filenames = filenames[0] - - repre_name = repre_ext = ext[1:] - if repre_name not in repre_names_counter: - repre_names_counter[repre_name] = 2 - else: - counter = repre_names_counter[repre_name] - repre_names_counter[repre_name] += 1 - repre_name = "{}_{}".format(repre_name, counter) - repre_names.append(repre_name) - return { - "ext": repre_ext, - "name": repre_name, - "stagingDir": filepath_item["directory"], - "files": filenames, - "tags": [] - } - - def _calculate_source(self, filepaths): - cols, rems = clique.assemble(filepaths) - if cols: - source = cols[0].format("{head}{padding}{tail}") - elif rems: - source = rems[0] - return source diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_source.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_source.py deleted file mode 100644 index 6ff22be13a..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_source.py +++ /dev/null @@ -1,24 +0,0 @@ -import pyblish.api - - -class CollectSource(pyblish.api.ContextPlugin): - """Collecting instances from traypublisher host.""" - - label = "Collect source" - order = pyblish.api.CollectorOrder - 0.49 - hosts = ["traypublisher"] - - def process(self, context): - # get json paths from os and load them - source_name = "traypublisher" - for instance in context: - source = instance.data.get("source") - if not source: - instance.data["source"] = source_name - self.log.info(( - "Source of instance \"{}\" is changed to \"{}\"" - ).format(instance.data["name"], source_name)) - else: - self.log.info(( - "Source of instance \"{}\" was already set to \"{}\"" - ).format(instance.data["name"], source)) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_colorspace_look.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_colorspace_look.py deleted file mode 100644 index f7be322e74..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_colorspace_look.py +++ /dev/null @@ -1,45 +0,0 @@ -import os -import json -import pyblish.api -from ayon_core.pipeline import publish - - -class ExtractColorspaceLook(publish.Extractor, - publish.AYONPyblishPluginMixin): - """Extract OCIO colorspace look from LUT file - """ - - label = "Extract Colorspace Look" - order = pyblish.api.ExtractorOrder - hosts = ["traypublisher"] - families = ["ociolook"] - - def process(self, instance): - ociolook_items = instance.data["ocioLookItems"] - ociolook_working_color = instance.data["ocioLookWorkingSpace"] - staging_dir = self.staging_dir(instance) - - # create ociolook file attributes - ociolook_file_name = "ocioLookFile.json" - ociolook_file_content = { - "version": 1, - "data": { - "ocioLookItems": ociolook_items, - "ocioLookWorkingSpace": ociolook_working_color - } - } - - # write ociolook content into json file saved in staging dir - file_url = os.path.join(staging_dir, ociolook_file_name) - with open(file_url, "w") as f_: - json.dump(ociolook_file_content, f_, indent=4) - - # create lut representation data - ociolook_repre = { - "name": "ocioLookFile", - "ext": "json", - "files": ociolook_file_name, - "stagingDir": staging_dir, - "tags": [] - } - instance.data["representations"].append(ociolook_repre) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_csv_file.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_csv_file.py deleted file mode 100644 index 4bdf7c0493..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_csv_file.py +++ /dev/null @@ -1,31 +0,0 @@ -import pyblish.api - -from ayon_core.pipeline import publish - - -class ExtractCSVFile(publish.Extractor): - """ - Extractor export CSV file - """ - - label = "Extract CSV file" - order = pyblish.api.ExtractorOrder - 0.45 - families = ["csv_ingest_file"] - hosts = ["traypublisher"] - - def process(self, instance): - - csv_file_data = instance.data["csvFileData"] - - representation_csv = { - 'name': "csv_data", - 'ext': "csv", - 'files': csv_file_data["filename"], - "stagingDir": csv_file_data["staging_dir"], - "stagingDir_persistent": True - } - - instance.data["representations"].append(representation_csv) - - self.log.info("Added CSV file representation: {}".format( - representation_csv)) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_editorial_pckg.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_editorial_pckg.py deleted file mode 100644 index 3e391b5f6e..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_editorial_pckg.py +++ /dev/null @@ -1,232 +0,0 @@ -import copy -import os.path -import subprocess - -import opentimelineio - -import pyblish.api - -from ayon_core.lib import get_ffmpeg_tool_args, run_subprocess -from ayon_core.pipeline import publish - - -class ExtractEditorialPckgConversion(publish.Extractor): - """Replaces movie paths in otio file with publish rootless - - Prepares movie resources for integration (adds them to `transfers`). - Converts .mov files according to output definition. - """ - - label = "Extract Editorial Package" - order = pyblish.api.ExtractorOrder - 0.45 - hosts = ["traypublisher"] - families = ["editorial_pkg"] - - def process(self, instance): - editorial_pkg_data = instance.data.get("editorial_pkg") - - otio_path = editorial_pkg_data["otio_path"] - otio_basename = os.path.basename(otio_path) - staging_dir = self.staging_dir(instance) - - editorial_pkg_repre = { - 'name': "editorial_pkg", - 'ext': "otio", - 'files': otio_basename, - "stagingDir": staging_dir, - } - otio_staging_path = os.path.join(staging_dir, otio_basename) - - instance.data["representations"].append(editorial_pkg_repre) - - publish_resource_folder = self._get_publish_resource_folder(instance) - resource_paths = editorial_pkg_data["resource_paths"] - transfers = self._get_transfers(resource_paths, - publish_resource_folder) - - project_settings = instance.context.data["project_settings"] - output_def = (project_settings["traypublisher"] - ["publish"] - ["ExtractEditorialPckgConversion"] - ["output"]) - - conversion_enabled = (instance.data["creator_attributes"] - ["conversion_enabled"]) - - if conversion_enabled and output_def["ext"]: - transfers = self._convert_resources(output_def, transfers) - - instance.data["transfers"] = transfers - - source_to_rootless = self._get_resource_path_mapping(instance, - transfers) - - otio_data = editorial_pkg_data["otio_data"] - otio_data = self._replace_target_urls(otio_data, source_to_rootless) - - opentimelineio.adapters.write_to_file(otio_data, otio_staging_path) - - self.log.info("Added Editorial Package representation: {}".format( - editorial_pkg_repre)) - - def _get_publish_resource_folder(self, instance): - """Calculates publish folder and create it.""" - publish_path = self._get_published_path(instance) - publish_folder = os.path.dirname(publish_path) - publish_resource_folder = os.path.join(publish_folder, "resources") - - if not os.path.exists(publish_resource_folder): - os.makedirs(publish_resource_folder, exist_ok=True) - return publish_resource_folder - - def _get_resource_path_mapping(self, instance, transfers): - """Returns dict of {source_mov_path: rootless_published_path}.""" - replace_paths = {} - anatomy = instance.context.data["anatomy"] - for source, destination in transfers: - rootless_path = self._get_rootless(anatomy, destination) - source_file_name = os.path.basename(source) - replace_paths[source_file_name] = rootless_path - return replace_paths - - def _get_transfers(self, resource_paths, publish_resource_folder): - """Returns list of tuples (source, destination) with movie paths.""" - transfers = [] - for res_path in resource_paths: - res_basename = os.path.basename(res_path) - pub_res_path = os.path.join(publish_resource_folder, res_basename) - transfers.append((res_path, pub_res_path)) - return transfers - - def _replace_target_urls(self, otio_data, replace_paths): - """Replace original movie paths with published rootless ones.""" - for track in otio_data.tracks: - for clip in track: - # Check if the clip has a media reference - if clip.media_reference is not None: - # Access the target_url from the media reference - target_url = clip.media_reference.target_url - if not target_url: - continue - file_name = os.path.basename(target_url) - replace_path = replace_paths.get(file_name) - if replace_path: - clip.media_reference.target_url = replace_path - if clip.name == file_name: - clip.name = os.path.basename(replace_path) - - return otio_data - - def _get_rootless(self, anatomy, path): - """Try to find rootless {root[work]} path from `path`""" - success, rootless_path = anatomy.find_root_template_from_path( - path) - if not success: - # `rootless_path` is not set to `output_dir` if none of roots match - self.log.warning( - f"Could not find root path for remapping '{path}'." - ) - rootless_path = path - - return rootless_path - - def _get_published_path(self, instance): - """Calculates expected `publish` folder""" - # determine published path from Anatomy. - template_data = instance.data.get("anatomyData") - rep = instance.data["representations"][0] - template_data["representation"] = rep.get("name") - template_data["ext"] = rep.get("ext") - template_data["comment"] = None - - anatomy = instance.context.data["anatomy"] - template_data["root"] = anatomy.roots - template = anatomy.get_template_item("publish", "default", "path") - template_filled = template.format_strict(template_data) - return os.path.normpath(template_filled) - - def _convert_resources(self, output_def, transfers): - """Converts all resource files to configured format.""" - out_extension = output_def["ext"] - if not out_extension: - self.log.warning("No output extension configured in " - "ayon+settings://traypublisher/publish/ExtractEditorialPckgConversion") # noqa - return transfers - - final_transfers = [] - out_def_ffmpeg_args = output_def["ffmpeg_args"] - ffmpeg_input_args = [ - value.strip() - for value in out_def_ffmpeg_args["input"] - if value.strip() - ] - ffmpeg_video_filters = [ - value.strip() - for value in out_def_ffmpeg_args["video_filters"] - if value.strip() - ] - ffmpeg_audio_filters = [ - value.strip() - for value in out_def_ffmpeg_args["audio_filters"] - if value.strip() - ] - ffmpeg_output_args = [ - value.strip() - for value in out_def_ffmpeg_args["output"] - if value.strip() - ] - ffmpeg_input_args = self._split_ffmpeg_args(ffmpeg_input_args) - - generic_args = [ - subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg")) - ] - generic_args.extend(ffmpeg_input_args) - if ffmpeg_video_filters: - generic_args.append("-filter:v") - generic_args.append( - "\"{}\"".format(",".join(ffmpeg_video_filters))) - - if ffmpeg_audio_filters: - generic_args.append("-filter:a") - generic_args.append( - "\"{}\"".format(",".join(ffmpeg_audio_filters))) - - for source, destination in transfers: - base_name = os.path.basename(destination) - file_name, ext = os.path.splitext(base_name) - dest_path = os.path.join(os.path.dirname(destination), - f"{file_name}.{out_extension}") - final_transfers.append((source, dest_path)) - - all_args = copy.deepcopy(generic_args) - all_args.append(f"-i \"{source}\"") - all_args.extend(ffmpeg_output_args) # order matters - all_args.append(f"\"{dest_path}\"") - subprcs_cmd = " ".join(all_args) - - # run subprocess - self.log.debug("Executing: {}".format(subprcs_cmd)) - run_subprocess(subprcs_cmd, shell=True, logger=self.log) - return final_transfers - - def _split_ffmpeg_args(self, in_args): - """Makes sure all entered arguments are separated in individual items. - - Split each argument string with " -" to identify if string contains - one or more arguments. - """ - splitted_args = [] - for arg in in_args: - sub_args = arg.split(" -") - if len(sub_args) == 1: - if arg and arg not in splitted_args: - splitted_args.append(arg) - continue - - for idx, arg in enumerate(sub_args): - if idx != 0: - arg = "-" + arg - - if arg and arg not in splitted_args: - splitted_args.append(arg) - return splitted_args diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_trim_video_audio.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_trim_video_audio.py deleted file mode 100644 index 78e2aec972..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_trim_video_audio.py +++ /dev/null @@ -1,126 +0,0 @@ -import os -from pprint import pformat - -import pyblish.api - -from ayon_core.lib import ( - get_ffmpeg_tool_args, - run_subprocess, -) -from ayon_core.pipeline import publish - - -class ExtractTrimVideoAudio(publish.Extractor): - """Trim with ffmpeg "mov" and "wav" files.""" - - # must be before `ExtractThumbnailSP` - order = pyblish.api.ExtractorOrder - 0.01 - label = "Extract Trim Video/Audio" - hosts = ["traypublisher"] - families = ["clip", "trimming"] - - # make sure it is enabled only if at least both families are available - match = pyblish.api.Subset - - # presets - - def process(self, instance): - representation = instance.data.get("representations") - self.log.debug(f"_ representation: {representation}") - - if not representation: - instance.data["representations"] = list() - - # get ffmpet path - ffmpeg_tool_args = get_ffmpeg_tool_args("ffmpeg") - - # get staging dir - staging_dir = self.staging_dir(instance) - self.log.debug("Staging dir set to: `{}`".format(staging_dir)) - - # Generate mov file. - fps = instance.data["fps"] - video_file_path = instance.data["editorialSourcePath"] - extensions = instance.data.get("extensions", ["mov"]) - output_file_type = instance.data.get("outputFileType") - reviewable = "review" in instance.data["families"] - - frame_start = int(instance.data["frameStart"]) - frame_end = int(instance.data["frameEnd"]) - handle_start = instance.data["handleStart"] - handle_end = instance.data["handleEnd"] - - clip_start_h = float(instance.data["clipInH"]) - _dur = instance.data["clipDuration"] - handle_dur = (handle_start + handle_end) - clip_dur_h = float(_dur + handle_dur) - - if output_file_type: - extensions = [output_file_type] - - for ext in extensions: - self.log.debug("Processing ext: `{}`".format(ext)) - - if not ext.startswith("."): - ext = "." + ext - - clip_trimed_path = os.path.join( - staging_dir, instance.data["name"] + ext) - - if ext == ".wav": - # offset time as ffmpeg is having bug - clip_start_h += 0.5 - # remove "review" from families - instance.data["families"] = [ - fml for fml in instance.data["families"] - if "trimming" not in fml - ] - - ffmpeg_args = ffmpeg_tool_args + [ - "-ss", str(clip_start_h / fps), - "-i", video_file_path, - "-t", str(clip_dur_h / fps) - ] - if ext in [".mov", ".mp4"]: - ffmpeg_args.extend([ - "-crf", "18", - "-pix_fmt", "yuv420p" - ]) - elif ext in ".wav": - ffmpeg_args.extend([ - "-vn", - "-acodec", "pcm_s16le", - "-ar", "48000", - "-ac", "2" - ]) - - # add output path - ffmpeg_args.append(clip_trimed_path) - - joined_args = " ".join(ffmpeg_args) - self.log.debug(f"Processing: {joined_args}") - run_subprocess( - ffmpeg_args, logger=self.log - ) - - repre = { - "name": ext[1:], - "ext": ext[1:], - "files": os.path.basename(clip_trimed_path), - "stagingDir": staging_dir, - "frameStart": frame_start, - "frameEnd": frame_end, - "frameStartFtrack": frame_start - handle_start, - "frameEndFtrack": frame_end + handle_end, - "fps": fps, - "tags": [] - } - - if ext in [".mov", ".mp4"] and reviewable: - repre.update({ - "thumbnail": True, - "tags": ["review", "ftrackreview", "delete"]}) - - instance.data["representations"].append(repre) - - self.log.debug(f"Instance data: {pformat(instance.data)}") diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/help/validate_existing_version.xml b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/help/validate_existing_version.xml deleted file mode 100644 index 89997b4c8c..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/help/validate_existing_version.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - -Version already exists - -## Version already exists - -Version {version} you have set on instance '{product_name}' under '{folder_path}' already exists. This validation is enabled by default to prevent accidental override of existing versions. - -### How to repair? -- Click on 'Repair' action -> this will change version to next available. -- Disable validation on the instance if you are sure you want to override the version. -- Reset publishing and manually change the version number. - - - diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/help/validate_frame_ranges.xml b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/help/validate_frame_ranges.xml deleted file mode 100644 index 933df1c7c5..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/help/validate_frame_ranges.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - -Invalid frame range - -## Invalid frame range - -Expected duration or '{duration}' frames set in database, workfile contains only '{found}' frames. - -### How to repair? - -Modify configuration in the database or tweak frame range in the workfile. - - - \ No newline at end of file diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_colorspace.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_colorspace.py deleted file mode 100644 index 9f629f78dd..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_colorspace.py +++ /dev/null @@ -1,66 +0,0 @@ -import pyblish.api - -from ayon_core.pipeline import ( - publish, - PublishValidationError -) - -from ayon_core.pipeline.colorspace import ( - get_ocio_config_colorspaces -) - - -class ValidateColorspace(pyblish.api.InstancePlugin, - publish.AYONPyblishPluginMixin, - publish.ColormanagedPyblishPluginMixin): - """Validate representation colorspaces""" - - label = "Validate representation colorspace" - order = pyblish.api.ValidatorOrder - hosts = ["traypublisher"] - families = ["render", "plate", "reference", "image", "online"] - - def process(self, instance): - - config_colorspaces = {} # cache of colorspaces per config path - for repre in instance.data.get("representations", {}): - - colorspace_data = repre.get("colorspaceData", {}) - if not colorspace_data: - # Nothing to validate - continue - - config_path = colorspace_data["config"]["path"] - if config_path not in config_colorspaces: - colorspaces = get_ocio_config_colorspaces(config_path) - if not colorspaces.get("colorspaces"): - message = ( - f"OCIO config '{config_path}' does not contain any " - "colorspaces. This is an error in the OCIO config. " - "Contact your pipeline TD.", - ) - raise PublishValidationError( - title="Colorspace validation", - message=message, - description=message - ) - config_colorspaces[config_path] = set( - colorspaces["colorspaces"]) - - colorspace = colorspace_data["colorspace"] - self.log.debug( - f"Validating representation '{repre['name']}' " - f"colorspace '{colorspace}'" - ) - if colorspace not in config_colorspaces[config_path]: - message = ( - f"Representation '{repre['name']}' colorspace " - f"'{colorspace}' does not exist in OCIO config: " - f"{config_path}" - ) - - raise PublishValidationError( - title="Representation colorspace", - message=message, - description=message - ) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_colorspace_look.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_colorspace_look.py deleted file mode 100644 index 91c7632376..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_colorspace_look.py +++ /dev/null @@ -1,89 +0,0 @@ -import pyblish.api - -from ayon_core.pipeline import ( - publish, - PublishValidationError -) - - -class ValidateColorspaceLook(pyblish.api.InstancePlugin, - publish.AYONPyblishPluginMixin): - """Validate colorspace look attributes""" - - label = "Validate colorspace look attributes" - order = pyblish.api.ValidatorOrder - hosts = ["traypublisher"] - families = ["ociolook"] - - def process(self, instance): - create_context = instance.context.data["create_context"] - created_instance = create_context.get_instance_by_id( - instance.data["instance_id"]) - creator_defs = created_instance.creator_attribute_defs - - ociolook_working_color = instance.data.get("ocioLookWorkingSpace") - ociolook_items = instance.data.get("ocioLookItems", []) - - creator_defs_by_key = {_def.key: _def.label for _def in creator_defs} - - not_set_keys = {} - if not ociolook_working_color: - not_set_keys["working_colorspace"] = creator_defs_by_key[ - "working_colorspace"] - - for ociolook_item in ociolook_items: - item_not_set_keys = self.validate_colorspace_set_attrs( - ociolook_item, creator_defs_by_key) - if item_not_set_keys: - not_set_keys[ociolook_item["name"]] = item_not_set_keys - - if not_set_keys: - message = ( - "Colorspace look attributes are not set: \n" - ) - for key, value in not_set_keys.items(): - if isinstance(value, list): - values_string = "\n\t- ".join(value) - message += f"\n\t{key}:\n\t- {values_string}" - else: - message += f"\n\t{value}" - - raise PublishValidationError( - title="Colorspace Look attributes", - message=message, - description=message - ) - - def validate_colorspace_set_attrs( - self, - ociolook_item, - creator_defs_by_key - ): - """Validate colorspace look attributes""" - - self.log.debug(f"Validate colorspace look attributes: {ociolook_item}") - - check_keys = [ - "input_colorspace", - "output_colorspace", - "direction", - "interpolation" - ] - - not_set_keys = [] - for key in check_keys: - if ociolook_item[key]: - # key is set and it is correct - continue - - def_label = creator_defs_by_key.get(key) - - if not def_label: - # raise since key is not recognized by creator defs - raise KeyError( - f"Colorspace look attribute '{key}' is not " - f"recognized by creator attributes: {creator_defs_by_key}" - ) - not_set_keys.append(def_label) - - return not_set_keys diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_editorial_package.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_editorial_package.py deleted file mode 100644 index 02793516e2..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_editorial_package.py +++ /dev/null @@ -1,79 +0,0 @@ -import os -import opentimelineio -from opentimelineio.exceptions import UnsupportedSchemaError - - -import pyblish.api -from ayon_core.pipeline import PublishValidationError - - -class ValidateEditorialPackage(pyblish.api.InstancePlugin): - """Checks that published folder contains all resources from otio - - Currently checks only by file names and expects flat structure. - It ignores path to resources in otio file as folder might be dragged in and - published from different location than it was created. - """ - - label = "Validate Editorial Package" - order = pyblish.api.ValidatorOrder - 0.49 - - hosts = ["traypublisher"] - families = ["editorial_pkg"] - - def process(self, instance): - editorial_pkg_data = instance.data.get("editorial_pkg") - if not editorial_pkg_data: - raise PublishValidationError("Editorial package not collected") - - folder_path = editorial_pkg_data["folder_path"] - - otio_path = editorial_pkg_data["otio_path"] - if not otio_path: - raise PublishValidationError( - f"Folder {folder_path} missing otio file") - - resource_paths = editorial_pkg_data["resource_paths"] - - resource_file_names = {os.path.basename(path) - for path in resource_paths} - - try: - otio_data = opentimelineio.adapters.read_from_file(otio_path) - except UnsupportedSchemaError as e: - raise PublishValidationError( - f"Unsupported schema in otio file '{otio_path}'." - "Version of your OpenTimelineIO library is too old." - "Please update it to the latest version." - f"Current version is '{opentimelineio.__version__}', " - "but required is at least 0.16.0." - ) from e - - target_urls = self._get_all_target_urls(otio_data) - missing_files = set() - for target_url in target_urls: - target_basename = os.path.basename(target_url) - if target_basename not in resource_file_names: - missing_files.add(target_basename) - - if missing_files: - raise PublishValidationError( - f"Otio file contains missing files `{missing_files}`.\n\n" - f"Please add them to `{folder_path}` and republish.") - - instance.data["editorial_pkg"]["otio_data"] = otio_data - - def _get_all_target_urls(self, otio_data): - target_urls = [] - - # Iterate through tracks, clips, or other elements - for track in otio_data.tracks: - for clip in track: - # Check if the clip has a media reference - if clip.media_reference is not None: - # Access the target_url from the media reference - target_url = clip.media_reference.target_url - if target_url: - target_urls.append(target_url) - - return target_urls diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_existing_version.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_existing_version.py deleted file mode 100644 index 0b4f8e16c1..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_existing_version.py +++ /dev/null @@ -1,58 +0,0 @@ -import pyblish.api - -from ayon_core.pipeline.publish import ( - ValidateContentsOrder, - PublishXmlValidationError, - OptionalPyblishPluginMixin, - RepairAction, -) - - -class ValidateExistingVersion( - OptionalPyblishPluginMixin, - pyblish.api.InstancePlugin -): - label = "Validate Existing Version" - order = ValidateContentsOrder - - hosts = ["traypublisher"] - targets = ["local"] - - actions = [RepairAction] - - settings_category = "traypublisher" - optional = True - - def process(self, instance): - if not self.is_active(instance.data): - return - - version = instance.data.get("version") - if version is None: - return - - last_version = instance.data.get("latestVersion") - if last_version is None or last_version < version: - return - - product_name = instance.data["productName"] - msg = "Version {} already exists for product {}.".format( - version, product_name) - - formatting_data = { - "product_name": product_name, - "folder_path": instance.data["folderPath"], - "version": version - } - raise PublishXmlValidationError( - self, msg, formatting_data=formatting_data) - - @classmethod - def repair(cls, instance): - create_context = instance.context.data["create_context"] - created_instance = create_context.get_instance_by_id( - instance.data["instance_id"]) - creator_attributes = created_instance["creator_attributes"] - # Disable version override - creator_attributes["use_next_version"] = True - create_context.save_changes() diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_filepaths.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_filepaths.py deleted file mode 100644 index c673b1977b..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_filepaths.py +++ /dev/null @@ -1,68 +0,0 @@ -import os -import pyblish.api -from ayon_core.pipeline import PublishValidationError - - -class ValidateFilePath(pyblish.api.InstancePlugin): - """Validate existence of source filepaths on instance. - - Plugins looks into key 'sourceFilepaths' and validate if paths there - actually exist on disk. - - Also validate if the key is filled but is empty. In that case also - crashes so do not fill the key if unfilled value should not cause error. - - This is primarily created for Simple Creator instances. - """ - - label = "Validate Filepaths" - order = pyblish.api.ValidatorOrder - 0.49 - - hosts = ["traypublisher"] - - def process(self, instance): - if "sourceFilepaths" not in instance.data: - self.log.info(( - "Skipped validation of source filepaths existence." - " Instance does not have collected 'sourceFilepaths'" - )) - return - - product_type = instance.data["productType"] - label = instance.data["name"] - filepaths = instance.data["sourceFilepaths"] - if not filepaths: - raise PublishValidationError( - ( - "Source filepaths of '{}' instance \"{}\" are not filled" - ).format(product_type, label), - "File not filled", - ( - "## Files were not filled" - "\nThis mean that you didn't enter any files into required" - " file input." - "\n- Please refresh publishing and check instance" - " {}" - ).format(label) - ) - - not_found_files = [ - filepath - for filepath in filepaths - if not os.path.exists(filepath) - ] - if not_found_files: - joined_paths = "\n".join([ - "- {}".format(filepath) - for filepath in not_found_files - ]) - raise PublishValidationError( - ( - "Filepath of '{}' instance \"{}\" does not exist:\n{}" - ).format(product_type, label, joined_paths), - "File not found", - ( - "## Files were not found\nFiles\n{}" - "\n\nCheck if the path is still available." - ).format(joined_paths) - ) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_frame_ranges.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_frame_ranges.py deleted file mode 100644 index 42127f4a5f..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_frame_ranges.py +++ /dev/null @@ -1,86 +0,0 @@ -import re - -import pyblish.api - -from ayon_core.pipeline.publish import ( - ValidateContentsOrder, - PublishXmlValidationError, - OptionalPyblishPluginMixin, -) - - -class ValidateFrameRange(OptionalPyblishPluginMixin, - pyblish.api.InstancePlugin): - """Validating frame range of rendered files against state in DB.""" - - label = "Validate Frame Range" - hosts = ["traypublisher"] - families = ["render", "plate"] - targets = ["local"] - - order = ValidateContentsOrder - - optional = True - # published data might be sequence (.mov, .mp4) in that counting files - # doesn't make sense - check_extensions = ["exr", "dpx", "jpg", "jpeg", "png", "tiff", "tga", - "gif", "svg"] - skip_timelines_check = [] # skip for specific task names (regex) - - def process(self, instance): - # Skip the instance if is not active by data on the instance - if not self.is_active(instance.data): - return - - # editorial would fail since they might not be in database yet - new_hierarchy = ( - instance.data.get("newHierarchyIntegration") - # Backwards compatible (Deprecated since 24/06/06) - or instance.data.get("newAssetPublishing") - ) - if new_hierarchy: - self.log.debug("Instance is creating new folder. Skipping.") - return - - if (self.skip_timelines_check and - any(re.search(pattern, instance.data["task"]) - for pattern in self.skip_timelines_check)): - self.log.info("Skipping for {} task".format(instance.data["task"])) - - folder_attributes = instance.data["folderEntity"]["attrib"] - frame_start = folder_attributes["frameStart"] - frame_end = folder_attributes["frameEnd"] - handle_start = folder_attributes["handleStart"] - handle_end = folder_attributes["handleEnd"] - duration = (frame_end - frame_start + 1) + handle_start + handle_end - - repres = instance.data.get("representations") - if not repres: - self.log.info("No representations, skipping.") - return - - first_repre = repres[0] - ext = first_repre['ext'].replace(".", '') - - if not ext or ext.lower() not in self.check_extensions: - self.log.warning("Cannot check for extension {}".format(ext)) - return - - files = first_repre["files"] - if isinstance(files, str): - files = [files] - frames = len(files) - - msg = ( - "Frame duration from DB:'{}' doesn't match number of files:'{}'" - " Please change frame range for Folder or limit no. of files" - ). format(int(duration), frames) - - formatting_data = {"duration": duration, - "found": frames} - if frames != duration: - raise PublishXmlValidationError(self, msg, - formatting_data=formatting_data) - - self.log.debug("Valid ranges expected '{}' - found '{}'". - format(int(duration), frames)) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_online_file.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_online_file.py deleted file mode 100644 index e9add2369b..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_online_file.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -import ayon_api -import pyblish.api - -from ayon_core.pipeline.publish import ( - ValidateContentsOrder, - PublishValidationError, - OptionalPyblishPluginMixin, -) - - -class ValidateOnlineFile(OptionalPyblishPluginMixin, - pyblish.api.InstancePlugin): - """Validate that product doesn't exist yet.""" - label = "Validate Existing Online Files" - hosts = ["traypublisher"] - families = ["online"] - order = ValidateContentsOrder - - optional = True - - def process(self, instance): - if not self.is_active(instance.data): - return - project_name = instance.context.data["projectName"] - folder_id = instance.data["folderEntity"]["id"] - product_entity = ayon_api.get_product_by_name( - project_name, instance.data["productName"], folder_id) - - if product_entity: - raise PublishValidationError( - "Product to be published already exists.", - title=self.label - ) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/ui/__init__.py b/server_addon/traypublisher/client/ayon_traypublisher/ui/__init__.py deleted file mode 100644 index 188a234a9e..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/ui/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .window import main - - -__all__ = ( - "main", -) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/ui/window.py b/server_addon/traypublisher/client/ayon_traypublisher/ui/window.py deleted file mode 100644 index 288dac8529..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/ui/window.py +++ /dev/null @@ -1,271 +0,0 @@ -"""Tray publisher is extending publisher tool. - -Adds ability to select project using overlay widget with list of projects. - -Tray publisher can be considered as host implementeation with creators and -publishing plugins. -""" - -import platform - -from qtpy import QtWidgets, QtCore -import qtawesome - -from ayon_core.lib import AYONSettingsRegistry, is_running_from_build -from ayon_core.pipeline import install_host -from ayon_core.tools.publisher.control_qt import QtPublisherController -from ayon_core.tools.publisher.window import PublisherWindow -from ayon_core.tools.common_models import ProjectsModel -from ayon_core.tools.utils import ( - PlaceholderLineEdit, - get_ayon_qt_app, - ProjectsQtModel, - ProjectSortFilterProxy, - PROJECT_NAME_ROLE, -) -from ayon_traypublisher.api import TrayPublisherHost - - -class TrayPublisherRegistry(AYONSettingsRegistry): - def __init__(self): - super(TrayPublisherRegistry, self).__init__("traypublisher") - - -class TrayPublisherController(QtPublisherController): - def __init__(self, *args, **kwargs): - super(TrayPublisherController, self).__init__(*args, **kwargs) - self._projects_model = ProjectsModel(self) - - @property - def host(self): - return self._host - - def reset_hierarchy_cache(self): - self._hierarchy_model.reset() - - def get_project_items(self, sender=None): - return self._projects_model.get_project_items(sender) - - -class StandaloneOverlayWidget(QtWidgets.QFrame): - project_selected = QtCore.Signal(str) - - def __init__(self, controller, publisher_window): - super(StandaloneOverlayWidget, self).__init__(publisher_window) - self.setObjectName("OverlayFrame") - - middle_frame = QtWidgets.QFrame(self) - middle_frame.setObjectName("ChooseProjectFrame") - - content_widget = QtWidgets.QWidget(middle_frame) - - header_label = QtWidgets.QLabel("Choose project", content_widget) - header_label.setObjectName("ChooseProjectLabel") - # Create project models and view - projects_model = ProjectsQtModel(controller) - projects_proxy = ProjectSortFilterProxy() - projects_proxy.setSourceModel(projects_model) - projects_proxy.setFilterKeyColumn(0) - - projects_view = QtWidgets.QListView(content_widget) - projects_view.setObjectName("ChooseProjectView") - projects_view.setModel(projects_proxy) - projects_view.setEditTriggers( - QtWidgets.QAbstractItemView.NoEditTriggers - ) - - confirm_btn = QtWidgets.QPushButton("Confirm", content_widget) - cancel_btn = QtWidgets.QPushButton("Cancel", content_widget) - cancel_btn.setVisible(False) - btns_layout = QtWidgets.QHBoxLayout() - btns_layout.addStretch(1) - btns_layout.addWidget(cancel_btn, 0) - btns_layout.addWidget(confirm_btn, 0) - - txt_filter = PlaceholderLineEdit(content_widget) - txt_filter.setPlaceholderText("Quick filter projects..") - txt_filter.setClearButtonEnabled(True) - txt_filter.addAction(qtawesome.icon("fa.filter", color="gray"), - QtWidgets.QLineEdit.LeadingPosition) - - content_layout = QtWidgets.QVBoxLayout(content_widget) - content_layout.setContentsMargins(0, 0, 0, 0) - content_layout.setSpacing(20) - content_layout.addWidget(header_label, 0) - content_layout.addWidget(txt_filter, 0) - content_layout.addWidget(projects_view, 1) - content_layout.addLayout(btns_layout, 0) - - middle_layout = QtWidgets.QHBoxLayout(middle_frame) - middle_layout.setContentsMargins(30, 30, 10, 10) - middle_layout.addWidget(content_widget) - - main_layout = QtWidgets.QHBoxLayout(self) - main_layout.setContentsMargins(10, 10, 10, 10) - main_layout.addStretch(1) - main_layout.addWidget(middle_frame, 2) - main_layout.addStretch(1) - - projects_view.doubleClicked.connect(self._on_double_click) - confirm_btn.clicked.connect(self._on_confirm_click) - cancel_btn.clicked.connect(self._on_cancel_click) - txt_filter.textChanged.connect(self._on_text_changed) - - self._projects_view = projects_view - self._projects_model = projects_model - self._projects_proxy = projects_proxy - self._cancel_btn = cancel_btn - self._confirm_btn = confirm_btn - self._txt_filter = txt_filter - - self._publisher_window = publisher_window - self._project_name = None - - def showEvent(self, event): - self._projects_model.refresh() - # Sort projects after refresh - self._projects_proxy.sort(0) - - setting_registry = TrayPublisherRegistry() - try: - project_name = setting_registry.get_item("project_name") - except ValueError: - project_name = None - - if project_name: - src_index = self._projects_model.get_index_by_project_name( - project_name - ) - index = self._projects_proxy.mapFromSource(src_index) - if index.isValid(): - selection_model = self._projects_view.selectionModel() - selection_model.select( - index, - QtCore.QItemSelectionModel.SelectCurrent - ) - self._projects_view.setCurrentIndex(index) - - self._cancel_btn.setVisible(self._project_name is not None) - super(StandaloneOverlayWidget, self).showEvent(event) - - def _on_double_click(self): - self.set_selected_project() - - def _on_confirm_click(self): - self.set_selected_project() - - def _on_cancel_click(self): - self._set_project(self._project_name) - - def _on_text_changed(self): - self._projects_proxy.setFilterRegularExpression( - self._txt_filter.text()) - - def set_selected_project(self): - index = self._projects_view.currentIndex() - - project_name = index.data(PROJECT_NAME_ROLE) - if project_name: - self._set_project(project_name) - - @property - def host(self): - return self._publisher_window.controller.host - - def _set_project(self, project_name): - self._project_name = project_name - self.host.set_project_name(project_name) - self.setVisible(False) - self.project_selected.emit(project_name) - - setting_registry = TrayPublisherRegistry() - setting_registry.set_item("project_name", project_name) - - -class TrayPublishWindow(PublisherWindow): - def __init__(self, *args, **kwargs): - controller = TrayPublisherController() - super(TrayPublishWindow, self).__init__( - controller=controller, reset_on_show=False - ) - - flags = self.windowFlags() - # Disable always on top hint - if flags & QtCore.Qt.WindowStaysOnTopHint: - flags ^= QtCore.Qt.WindowStaysOnTopHint - - self.setWindowFlags(flags) - - overlay_widget = StandaloneOverlayWidget(controller, self) - - btns_widget = self._header_extra_widget - - back_to_overlay_btn = QtWidgets.QPushButton( - "Change project", btns_widget - ) - save_btn = QtWidgets.QPushButton("Save", btns_widget) - # TODO implement save mechanism of tray publisher - save_btn.setVisible(False) - - btns_layout = QtWidgets.QHBoxLayout(btns_widget) - btns_layout.setContentsMargins(0, 0, 0, 0) - - btns_layout.addWidget(save_btn, 0) - btns_layout.addWidget(back_to_overlay_btn, 0) - - overlay_widget.project_selected.connect(self._on_project_select) - back_to_overlay_btn.clicked.connect(self._on_back_to_overlay) - save_btn.clicked.connect(self._on_tray_publish_save) - - self._back_to_overlay_btn = back_to_overlay_btn - self._overlay_widget = overlay_widget - - def _set_publish_frame_visible(self, publish_frame_visible): - super(TrayPublishWindow, self)._set_publish_frame_visible( - publish_frame_visible - ) - self._back_to_overlay_btn.setVisible(not publish_frame_visible) - - def _on_back_to_overlay(self): - self._overlay_widget.setVisible(True) - self._resize_overlay() - - def _resize_overlay(self): - self._overlay_widget.resize( - self.width(), - self.height() - ) - - def resizeEvent(self, event): - super(TrayPublishWindow, self).resizeEvent(event) - self._resize_overlay() - - def _on_project_select(self, project_name): - # TODO register project specific plugin paths - self._controller.save_changes(False) - self._controller.reset_hierarchy_cache() - - self.reset() - if not self._controller.instances: - self._go_to_create_tab() - - def _on_tray_publish_save(self): - self._controller.save_changes() - print("NOT YET IMPLEMENTED") - - -def main(): - host = TrayPublisherHost() - install_host(host) - - app_instance = get_ayon_qt_app() - - if not is_running_from_build() and platform.system().lower() == "windows": - import ctypes - ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( - u"traypublisher" - ) - - window = TrayPublishWindow() - window.show() - app_instance.exec_() diff --git a/server_addon/traypublisher/client/ayon_traypublisher/version.py b/server_addon/traypublisher/client/ayon_traypublisher/version.py deleted file mode 100644 index 16e0ec1829..0000000000 --- a/server_addon/traypublisher/client/ayon_traypublisher/version.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- -"""Package declaring AYON addon 'traypublisher' version.""" -__version__ = "0.2.5" diff --git a/server_addon/traypublisher/package.py b/server_addon/traypublisher/package.py deleted file mode 100644 index 6a9ecdb0be..0000000000 --- a/server_addon/traypublisher/package.py +++ /dev/null @@ -1,10 +0,0 @@ -name = "traypublisher" -title = "TrayPublisher" -version = "0.2.5" - -client_dir = "ayon_traypublisher" - -ayon_required_addons = { - "core": ">0.3.2", -} -ayon_compatible_addons = {} diff --git a/server_addon/traypublisher/server/LICENSE b/server_addon/traypublisher/server/LICENSE deleted file mode 100644 index d645695673..0000000000 --- a/server_addon/traypublisher/server/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/server_addon/traypublisher/server/README.md b/server_addon/traypublisher/server/README.md deleted file mode 100644 index c0029bc782..0000000000 --- a/server_addon/traypublisher/server/README.md +++ /dev/null @@ -1,4 +0,0 @@ -Photoshp Addon -=============== - -Integration with Adobe Traypublisher. diff --git a/server_addon/traypublisher/server/__init__.py b/server_addon/traypublisher/server/__init__.py deleted file mode 100644 index 830f325ac0..0000000000 --- a/server_addon/traypublisher/server/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from ayon_server.addons import BaseServerAddon - -from .settings import TraypublisherSettings, DEFAULT_TRAYPUBLISHER_SETTING - - -class Traypublisher(BaseServerAddon): - settings_model = TraypublisherSettings - - async def get_default_settings(self): - settings_model_cls = self.get_settings_model() - return settings_model_cls(**DEFAULT_TRAYPUBLISHER_SETTING) diff --git a/server_addon/traypublisher/server/settings/__init__.py b/server_addon/traypublisher/server/settings/__init__.py deleted file mode 100644 index bcf8beffa7..0000000000 --- a/server_addon/traypublisher/server/settings/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .main import ( - TraypublisherSettings, - DEFAULT_TRAYPUBLISHER_SETTING, -) - - -__all__ = ( - "TraypublisherSettings", - "DEFAULT_TRAYPUBLISHER_SETTING", -) diff --git a/server_addon/traypublisher/server/settings/creator_plugins.py b/server_addon/traypublisher/server/settings/creator_plugins.py deleted file mode 100644 index 0e7963a33e..0000000000 --- a/server_addon/traypublisher/server/settings/creator_plugins.py +++ /dev/null @@ -1,341 +0,0 @@ -from pydantic import validator -from ayon_server.settings import BaseSettingsModel, SettingsField -from ayon_server.settings.validators import ensure_unique_names -from ayon_server.exceptions import BadRequestException - - -class BatchMovieCreatorPlugin(BaseSettingsModel): - """Allows to publish multiple video files in one go.
Name of matching - asset is parsed from file names ('asset.mov', 'asset_v001.mov', - 'my_asset_to_publish.mov')""" - - default_variants: list[str] = SettingsField( - title="Default variants", - default_factory=list - ) - - default_tasks: list[str] = SettingsField( - title="Default tasks", - default_factory=list - ) - - extensions: list[str] = SettingsField( - title="Extensions", - default_factory=list - ) - - -class ColumnItemModel(BaseSettingsModel): - """Allows to publish multiple video files in one go.
Name of matching - asset is parsed from file names ('asset.mov', 'asset_v001.mov', - 'my_asset_to_publish.mov')""" - - name: str = SettingsField( - title="Name", - default="" - ) - - type: str = SettingsField( - title="Type", - default="" - ) - - default: str = SettingsField( - title="Default", - default="" - ) - - required_column: bool = SettingsField( - title="Required Column", - default=False - ) - - validation_pattern: str = SettingsField( - title="Validation Regex Pattern", - default="^(.*)$" - ) - - -class ColumnConfigModel(BaseSettingsModel): - """Allows to publish multiple video files in one go.
Name of matching - asset is parsed from file names ('asset.mov', 'asset_v001.mov', - 'my_asset_to_publish.mov')""" - - csv_delimiter: str = SettingsField( - title="CSV delimiter", - default="," - ) - - columns: list[ColumnItemModel] = SettingsField( - title="Columns", - default_factory=list - ) - - @validator("columns") - def validate_unique_outputs(cls, value): - ensure_unique_names(value) - return value - - -class RepresentationItemModel(BaseSettingsModel): - """Allows to publish multiple video files in one go. - - Name of matching asset is parsed from file names - ('asset.mov', 'asset_v001.mov', 'my_asset_to_publish.mov') - """ - - name: str = SettingsField( - title="Name", - default="" - ) - - extensions: list[str] = SettingsField( - title="Extensions", - default_factory=list - ) - - @validator("extensions") - def validate_extension(cls, value): - for ext in value: - if not ext.startswith("."): - raise BadRequestException(f"Extension must start with '.': {ext}") - return value - - -class RepresentationConfigModel(BaseSettingsModel): - """Allows to publish multiple video files in one go.
Name of matching - asset is parsed from file names ('asset.mov', 'asset_v001.mov', - 'my_asset_to_publish.mov')""" - - tags_delimiter: str = SettingsField( - title="Tags delimiter", - default=";" - ) - - default_tags: list[str] = SettingsField( - title="Default tags", - default_factory=list - ) - - representations: list[RepresentationItemModel] = SettingsField( - title="Representations", - default_factory=list - ) - - @validator("representations") - def validate_unique_outputs(cls, value): - ensure_unique_names(value) - return value - - -class IngestCSVPluginModel(BaseSettingsModel): - """Allows to publish multiple video files in one go.
Name of matching - asset is parsed from file names ('asset.mov', 'asset_v001.mov', - 'my_asset_to_publish.mov')""" - - enabled: bool = SettingsField( - title="Enabled", - default=False - ) - - columns_config: ColumnConfigModel = SettingsField( - title="Columns config", - default_factory=ColumnConfigModel - ) - - representations_config: RepresentationConfigModel = SettingsField( - title="Representations config", - default_factory=RepresentationConfigModel - ) - - -class TrayPublisherCreatePluginsModel(BaseSettingsModel): - BatchMovieCreator: BatchMovieCreatorPlugin = SettingsField( - title="Batch Movie Creator", - default_factory=BatchMovieCreatorPlugin - ) - IngestCSV: IngestCSVPluginModel = SettingsField( - title="Ingest CSV", - default_factory=IngestCSVPluginModel - ) - - -DEFAULT_CREATORS = { - "BatchMovieCreator": { - "default_variants": [ - "Main" - ], - "default_tasks": [ - "Compositing" - ], - "extensions": [ - ".mov" - ] - }, - "IngestCSV": { - "enabled": True, - "columns_config": { - "csv_delimiter": ",", - "columns": [ - { - "name": "File Path", - "type": "text", - "default": "", - "required_column": True, - "validation_pattern": "^([a-zA-Z\\:\\ 0-9#._\\\\/]*)$" - }, - { - "name": "Folder Path", - "type": "text", - "default": "", - "required_column": True, - "validation_pattern": "^([a-zA-Z0-9_\\/]*)$" - }, - { - "name": "Task Name", - "type": "text", - "default": "", - "required_column": True, - "validation_pattern": "^(.*)$" - }, - { - "name": "Product Type", - "type": "text", - "default": "", - "required_column": False, - "validation_pattern": "^(.*)$" - }, - { - "name": "Variant", - "type": "text", - "default": "", - "required_column": False, - "validation_pattern": "^(.*)$" - }, - { - "name": "Version", - "type": "number", - "default": "1", - "required_column": True, - "validation_pattern": "^(\\d{1,3})$" - }, - { - "name": "Version Comment", - "type": "text", - "default": "", - "required_column": False, - "validation_pattern": "^(.*)$" - }, - { - "name": "Version Thumbnail", - "type": "text", - "default": "", - "required_column": False, - "validation_pattern": "^([a-zA-Z\\:\\ 0-9#._\\\\/]*)$" - }, - { - "name": "Frame Start", - "type": "number", - "default": "0", - "required_column": True, - "validation_pattern": "^(\\d{1,8})$" - }, - { - "name": "Frame End", - "type": "number", - "default": "0", - "required_column": True, - "validation_pattern": "^(\\d{1,8})$" - }, - { - "name": "Handle Start", - "type": "number", - "default": "0", - "required_column": True, - "validation_pattern": "^(\\d)$" - }, - { - "name": "Handle End", - "type": "number", - "default": "0", - "required_column": True, - "validation_pattern": "^(\\d)$" - }, - { - "name": "FPS", - "type": "decimal", - "default": "0.0", - "required_column": True, - "validation_pattern": "^[0-9]*\\.[0-9]+$|^[0-9]+$" - }, - { - "name": "Slate Exists", - "type": "bool", - "default": "True", - "required_column": False, - "validation_pattern": "(True|False)" - }, - { - "name": "Representation", - "type": "text", - "default": "", - "required_column": False, - "validation_pattern": "^(.*)$" - }, - { - "name": "Representation Colorspace", - "type": "text", - "default": "", - "required_column": False, - "validation_pattern": "^(.*)$" - }, - { - "name": "Representation Tags", - "type": "text", - "default": "", - "required_column": False, - "validation_pattern": "^(.*)$" - } - ] - }, - "representations_config": { - "tags_delimiter": ";", - "default_tags": [ - "review" - ], - "representations": [ - { - "name": "preview", - "extensions": [ - ".mp4", - ".mov" - ] - }, - { - "name": "exr", - "extensions": [ - ".exr" - ] - }, - { - "name": "edit", - "extensions": [ - ".mov" - ] - }, - { - "name": "review", - "extensions": [ - ".mov" - ] - }, - { - "name": "nuke", - "extensions": [ - ".nk" - ] - } - ] - } - } -} diff --git a/server_addon/traypublisher/server/settings/editorial_creators.py b/server_addon/traypublisher/server/settings/editorial_creators.py deleted file mode 100644 index d9f5e302a4..0000000000 --- a/server_addon/traypublisher/server/settings/editorial_creators.py +++ /dev/null @@ -1,181 +0,0 @@ -from ayon_server.settings import ( - BaseSettingsModel, - SettingsField, - task_types_enum, -) - - -class ClipNameTokenizerItem(BaseSettingsModel): - _layout = "expanded" - name: str = SettingsField("", title="Tokenizer name") - regex: str = SettingsField("", title="Tokenizer regex") - - -class ShotAddTasksItem(BaseSettingsModel): - _layout = "expanded" - name: str = SettingsField('', title="Key") - task_type: str = SettingsField( - title="Task type", - enum_resolver=task_types_enum - ) - - -class ShotRenameSubmodel(BaseSettingsModel): - enabled: bool = True - shot_rename_template: str = SettingsField( - "", - title="Shot rename template" - ) - - -parent_type_enum = [ - {"value": "Project", "label": "Project"}, - {"value": "Folder", "label": "Folder"}, - {"value": "Episode", "label": "Episode"}, - {"value": "Sequence", "label": "Sequence"}, -] - - -class TokenToParentConvertorItem(BaseSettingsModel): - # TODO - was 'type' must be renamed in code to `parent_type` - parent_type: str = SettingsField( - "Project", - enum_resolver=lambda: parent_type_enum - ) - name: str = SettingsField( - "", - title="Parent token name", - description="Unique name used in `Parent path template`" - ) - value: str = SettingsField( - "", - title="Parent token value", - description="Template where any text, Anatomy keys and Tokens could be used" # noqa - ) - - -class ShotHierarchySubmodel(BaseSettingsModel): - enabled: bool = True - parents_path: str = SettingsField( - "", - title="Parents path template", - description="Using keys from \"Token to parent convertor\" or tokens directly" # noqa - ) - parents: list[TokenToParentConvertorItem] = SettingsField( - default_factory=TokenToParentConvertorItem, - title="Token to parent convertor" - ) - - -output_file_type = [ - {"value": ".mp4", "label": "MP4"}, - {"value": ".mov", "label": "MOV"}, - {"value": ".wav", "label": "WAV"} -] - - -class ProductTypePresetItem(BaseSettingsModel): - product_type: str = SettingsField("", title="Product type") - # TODO add placeholder '< Inherited >' - variant: str = SettingsField("", title="Variant") - review: bool = SettingsField(True, title="Review") - output_file_type: str = SettingsField( - ".mp4", - enum_resolver=lambda: output_file_type - ) - - -class EditorialSimpleCreatorPlugin(BaseSettingsModel): - default_variants: list[str] = SettingsField( - default_factory=list, - title="Default Variants" - ) - clip_name_tokenizer: list[ClipNameTokenizerItem] = SettingsField( - default_factory=ClipNameTokenizerItem, - description=( - "Using Regex expression to create tokens. \nThose can be used" - " later in \"Shot rename\" creator \nor \"Shot hierarchy\"." - "\n\nTokens should be decorated with \"_\" on each side" - ) - ) - shot_rename: ShotRenameSubmodel = SettingsField( - title="Shot Rename", - default_factory=ShotRenameSubmodel - ) - shot_hierarchy: ShotHierarchySubmodel = SettingsField( - title="Shot Hierarchy", - default_factory=ShotHierarchySubmodel - ) - shot_add_tasks: list[ShotAddTasksItem] = SettingsField( - title="Add tasks to shot", - default_factory=ShotAddTasksItem - ) - product_type_presets: list[ProductTypePresetItem] = SettingsField( - default_factory=list - ) - - -class TraypublisherEditorialCreatorPlugins(BaseSettingsModel): - editorial_simple: EditorialSimpleCreatorPlugin = SettingsField( - title="Editorial simple creator", - default_factory=EditorialSimpleCreatorPlugin, - ) - - -DEFAULT_EDITORIAL_CREATORS = { - "editorial_simple": { - "default_variants": [ - "Main" - ], - "clip_name_tokenizer": [ - {"name": "_sequence_", "regex": "(sc\\d{3})"}, - {"name": "_shot_", "regex": "(sh\\d{3})"} - ], - "shot_rename": { - "enabled": True, - "shot_rename_template": "{project[code]}_{_sequence_}_{_shot_}" - }, - "shot_hierarchy": { - "enabled": True, - "parents_path": "{project}/{folder}/{sequence}", - "parents": [ - { - "parent_type": "Project", - "name": "project", - "value": "{project[name]}" - }, - { - "parent_type": "Folder", - "name": "folder", - "value": "shots" - }, - { - "parent_type": "Sequence", - "name": "sequence", - "value": "{_sequence_}" - } - ] - }, - "shot_add_tasks": [], - "product_type_presets": [ - { - "product_type": "review", - "variant": "Reference", - "review": True, - "output_file_type": ".mp4" - }, - { - "product_type": "plate", - "variant": "", - "review": False, - "output_file_type": ".mov" - }, - { - "product_type": "audio", - "variant": "", - "review": False, - "output_file_type": ".wav" - } - ] - } -} diff --git a/server_addon/traypublisher/server/settings/imageio.py b/server_addon/traypublisher/server/settings/imageio.py deleted file mode 100644 index 0f4659527f..0000000000 --- a/server_addon/traypublisher/server/settings/imageio.py +++ /dev/null @@ -1,62 +0,0 @@ -from pydantic import validator -from ayon_server.settings import BaseSettingsModel, SettingsField -from ayon_server.settings.validators import ensure_unique_names - - -class ImageIOConfigModel(BaseSettingsModel): - """[DEPRECATED] Addon OCIO config settings. Please set the OCIO config - path in the Core addon profiles here - (ayon+settings://core/imageio/ocio_config_profiles). - """ - override_global_config: bool = SettingsField( - False, - title="Override global OCIO config", - description=( - "DEPRECATED functionality. Please set the OCIO config path in the " - "Core addon profiles here (ayon+settings://core/imageio/" - "ocio_config_profiles)." - ), - ) - filepath: list[str] = SettingsField( - default_factory=list, - title="Config path", - description=( - "DEPRECATED functionality. Please set the OCIO config path in the " - "Core addon profiles here (ayon+settings://core/imageio/" - "ocio_config_profiles)." - ), - ) - - -class ImageIOFileRuleModel(BaseSettingsModel): - name: str = SettingsField("", title="Rule name") - pattern: str = SettingsField("", title="Regex pattern") - colorspace: str = SettingsField("", title="Colorspace name") - ext: str = SettingsField("", title="File extension") - - -class ImageIOFileRulesModel(BaseSettingsModel): - activate_host_rules: bool = SettingsField(False) - rules: list[ImageIOFileRuleModel] = SettingsField( - default_factory=list, - title="Rules" - ) - - @validator("rules") - def validate_unique_outputs(cls, value): - ensure_unique_names(value) - return value - - -class TrayPublisherImageIOModel(BaseSettingsModel): - activate_host_color_management: bool = SettingsField( - True, title="Enable Color Management" - ) - ocio_config: ImageIOConfigModel = SettingsField( - default_factory=ImageIOConfigModel, - title="OCIO config" - ) - file_rules: ImageIOFileRulesModel = SettingsField( - default_factory=ImageIOFileRulesModel, - title="File Rules" - ) diff --git a/server_addon/traypublisher/server/settings/main.py b/server_addon/traypublisher/server/settings/main.py deleted file mode 100644 index 760c529f49..0000000000 --- a/server_addon/traypublisher/server/settings/main.py +++ /dev/null @@ -1,51 +0,0 @@ -from ayon_server.settings import BaseSettingsModel, SettingsField - -from .imageio import TrayPublisherImageIOModel -from .simple_creators import ( - SimpleCreatorPlugin, - DEFAULT_SIMPLE_CREATORS, -) -from .editorial_creators import ( - TraypublisherEditorialCreatorPlugins, - DEFAULT_EDITORIAL_CREATORS, -) -from .creator_plugins import ( - TrayPublisherCreatePluginsModel, - DEFAULT_CREATORS, -) -from .publish_plugins import ( - TrayPublisherPublishPlugins, - DEFAULT_PUBLISH_PLUGINS, -) - - -class TraypublisherSettings(BaseSettingsModel): - """Traypublisher Project Settings.""" - imageio: TrayPublisherImageIOModel = SettingsField( - default_factory=TrayPublisherImageIOModel, - title="Color Management (ImageIO)" - ) - simple_creators: list[SimpleCreatorPlugin] = SettingsField( - title="Simple Create Plugins", - default_factory=SimpleCreatorPlugin, - ) - editorial_creators: TraypublisherEditorialCreatorPlugins = SettingsField( - title="Editorial Creators", - default_factory=TraypublisherEditorialCreatorPlugins, - ) - create: TrayPublisherCreatePluginsModel = SettingsField( - title="Create", - default_factory=TrayPublisherCreatePluginsModel - ) - publish: TrayPublisherPublishPlugins = SettingsField( - title="Publish Plugins", - default_factory=TrayPublisherPublishPlugins - ) - - -DEFAULT_TRAYPUBLISHER_SETTING = { - "simple_creators": DEFAULT_SIMPLE_CREATORS, - "editorial_creators": DEFAULT_EDITORIAL_CREATORS, - "create": DEFAULT_CREATORS, - "publish": DEFAULT_PUBLISH_PLUGINS, -} diff --git a/server_addon/traypublisher/server/settings/publish_plugins.py b/server_addon/traypublisher/server/settings/publish_plugins.py deleted file mode 100644 index 99a0bbf107..0000000000 --- a/server_addon/traypublisher/server/settings/publish_plugins.py +++ /dev/null @@ -1,116 +0,0 @@ -from ayon_server.settings import ( - BaseSettingsModel, - SettingsField, -) - - -class ValidatePluginModel(BaseSettingsModel): - _isGroup = True - enabled: bool = True - optional: bool = SettingsField(True, title="Optional") - active: bool = SettingsField(True, title="Active") - - -class ValidateFrameRangeModel(ValidatePluginModel): - """Allows to publish multiple video files in one go.
Name of matching - asset is parsed from file names ('asset.mov', 'asset_v001.mov', - 'my_asset_to_publish.mov')""" - - -class ExtractEditorialPckgFFmpegModel(BaseSettingsModel): - video_filters: list[str] = SettingsField( - default_factory=list, - title="Video filters" - ) - audio_filters: list[str] = SettingsField( - default_factory=list, - title="Audio filters" - ) - input: list[str] = SettingsField( - default_factory=list, - title="Input arguments" - ) - output: list[str] = SettingsField( - default_factory=list, - title="Output arguments" - ) - - -class ExtractEditorialPckgOutputDefModel(BaseSettingsModel): - _layout = "expanded" - ext: str = SettingsField("", title="Output extension") - - ffmpeg_args: ExtractEditorialPckgFFmpegModel = SettingsField( - default_factory=ExtractEditorialPckgFFmpegModel, - title="FFmpeg arguments" - ) - - -class ExtractEditorialPckgConversionModel(BaseSettingsModel): - """Set output definition if resource files should be converted.""" - conversion_enabled: bool = SettingsField(True, - title="Conversion enabled") - output: ExtractEditorialPckgOutputDefModel = SettingsField( - default_factory=ExtractEditorialPckgOutputDefModel, - title="Output Definitions", - ) - - -class TrayPublisherPublishPlugins(BaseSettingsModel): - CollectFrameDataFromAssetEntity: ValidatePluginModel = SettingsField( - default_factory=ValidatePluginModel, - title="Collect Frame Data From Folder Entity", - ) - ValidateFrameRange: ValidateFrameRangeModel = SettingsField( - title="Validate Frame Range", - default_factory=ValidateFrameRangeModel, - ) - ValidateExistingVersion: ValidatePluginModel = SettingsField( - title="Validate Existing Version", - default_factory=ValidatePluginModel, - ) - - ExtractEditorialPckgConversion: ExtractEditorialPckgConversionModel = ( - SettingsField( - default_factory=ExtractEditorialPckgConversionModel, - title="Extract Editorial Package Conversion" - ) - ) - - -DEFAULT_PUBLISH_PLUGINS = { - "CollectFrameDataFromAssetEntity": { - "enabled": True, - "optional": True, - "active": True - }, - "ValidateFrameRange": { - "enabled": True, - "optional": True, - "active": True - }, - "ValidateExistingVersion": { - "enabled": True, - "optional": True, - "active": True - }, - "ExtractEditorialPckgConversion": { - "optional": False, - "conversion_enabled": True, - "output": { - "ext": "", - "ffmpeg_args": { - "video_filters": [], - "audio_filters": [], - "input": [ - "-apply_trc gamma22" - ], - "output": [ - "-pix_fmt yuv420p", - "-crf 18", - "-intra" - ] - } - } - } -} diff --git a/server_addon/traypublisher/server/settings/simple_creators.py b/server_addon/traypublisher/server/settings/simple_creators.py deleted file mode 100644 index 6b979bbe52..0000000000 --- a/server_addon/traypublisher/server/settings/simple_creators.py +++ /dev/null @@ -1,310 +0,0 @@ -from ayon_server.settings import BaseSettingsModel, SettingsField - - -class SimpleCreatorPlugin(BaseSettingsModel): - _layout = "expanded" - product_type: str = SettingsField("", title="Product type") - # TODO add placeholder - identifier: str = SettingsField("", title="Identifier") - label: str = SettingsField("", title="Label") - icon: str = SettingsField("", title="Icon") - default_variants: list[str] = SettingsField( - default_factory=list, - title="Default Variants" - ) - description: str = SettingsField( - "", - title="Description", - widget="textarea" - ) - detailed_description: str = SettingsField( - "", - title="Detailed Description", - widget="textarea" - ) - allow_sequences: bool = SettingsField( - False, - title="Allow sequences" - ) - allow_multiple_items: bool = SettingsField( - False, - title="Allow multiple items" - ) - allow_version_control: bool = SettingsField( - False, - title="Allow version control" - ) - extensions: list[str] = SettingsField( - default_factory=list, - title="Extensions" - ) - - -DEFAULT_SIMPLE_CREATORS = [ - { - "product_type": "workfile", - "identifier": "", - "label": "Workfile", - "icon": "fa.file", - "default_variants": [ - "Main" - ], - "description": "Backup of a working scene", - "detailed_description": "Workfiles are full scenes from any application that are directly edited by artists. They represent a state of work on a task at a given point and are usually not directly referenced into other scenes.", - "allow_sequences": False, - "allow_multiple_items": False, - "allow_version_control": False, - "extensions": [ - ".ma", - ".mb", - ".nk", - ".hrox", - ".hip", - ".hiplc", - ".hipnc", - ".blend", - ".scn", - ".tvpp", - ".comp", - ".zip", - ".prproj", - ".drp", - ".psd", - ".psb", - ".aep" - ] - }, - { - "product_type": "model", - "identifier": "", - "label": "Model", - "icon": "fa.cubes", - "default_variants": [ - "Main", - "Proxy", - "Sculpt" - ], - "description": "Clean models", - "detailed_description": "Models should only contain geometry data, without any extras like cameras, locators or bones.\n\nKeep in mind that models published from tray publisher are not validated for correctness. ", - "allow_sequences": False, - "allow_multiple_items": True, - "allow_version_control": False, - "extensions": [ - ".ma", - ".mb", - ".obj", - ".abc", - ".fbx", - ".bgeo", - ".bgeogz", - ".bgeosc", - ".usd", - ".blend" - ] - }, - { - "product_type": "pointcache", - "identifier": "", - "label": "Pointcache", - "icon": "fa.gears", - "default_variants": [ - "Main" - ], - "description": "Geometry Caches", - "detailed_description": "Alembic or bgeo cache of animated data", - "allow_sequences": True, - "allow_multiple_items": True, - "allow_version_control": False, - "extensions": [ - ".abc", - ".bgeo", - ".bgeogz", - ".bgeosc" - ] - }, - { - "product_type": "plate", - "identifier": "", - "label": "Plate", - "icon": "mdi.camera-image", - "default_variants": [ - "Main", - "BG", - "Animatic", - "Reference", - "Offline" - ], - "description": "Footage Plates", - "detailed_description": "Any type of image seqeuence coming from outside of the studio. Usually camera footage, but could also be animatics used for reference.", - "allow_sequences": True, - "allow_multiple_items": True, - "allow_version_control": False, - "extensions": [ - ".exr", - ".png", - ".dng", - ".dpx", - ".jpg", - ".tiff", - ".tif", - ".mov", - ".mp4", - ".avi" - ] - }, - { - "product_type": "render", - "identifier": "", - "label": "Render", - "icon": "mdi.folder-multiple-image", - "default_variants": [], - "description": "Rendered images or video", - "detailed_description": "Sequence or single file renders", - "allow_sequences": True, - "allow_multiple_items": True, - "allow_version_control": False, - "extensions": [ - ".exr", - ".png", - ".dng", - ".dpx", - ".jpg", - ".jpeg", - ".tiff", - ".tif", - ".mov", - ".mp4", - ".avi" - ] - }, - { - "product_type": "camera", - "identifier": "", - "label": "Camera", - "icon": "fa.video-camera", - "default_variants": [], - "description": "3d Camera", - "detailed_description": "Ideally this should be only camera itself with baked animation, however, it can technically also include helper geometry.", - "allow_sequences": False, - "allow_multiple_items": True, - "allow_version_control": False, - "extensions": [ - ".abc", - ".ma", - ".hip", - ".blend", - ".fbx", - ".usd" - ] - }, - { - "product_type": "image", - "identifier": "", - "label": "Image", - "icon": "fa.image", - "default_variants": [ - "Reference", - "Texture", - "Concept", - "Background" - ], - "description": "Single image", - "detailed_description": "Any image data can be published as image product type. References, textures, concept art, matte paints. This is a fallback 2d product type for everything that doesn't fit more specific product type.", - "allow_sequences": False, - "allow_multiple_items": True, - "allow_version_control": False, - "extensions": [ - ".exr", - ".jpg", - ".jpeg", - ".dng", - ".dpx", - ".bmp", - ".tif", - ".tiff", - ".png", - ".psb", - ".psd" - ] - }, - { - "product_type": "vdb", - "identifier": "", - "label": "VDB Volumes", - "icon": "fa.cloud", - "default_variants": [], - "description": "Sparse volumetric data", - "detailed_description": "Hierarchical data structure for the efficient storage and manipulation of sparse volumetric data discretized on three-dimensional grids", - "allow_sequences": True, - "allow_multiple_items": True, - "allow_version_control": False, - "extensions": [ - ".vdb" - ] - }, - { - "product_type": "matchmove", - "identifier": "", - "label": "Matchmove", - "icon": "fa.empire", - "default_variants": [ - "Camera", - "Object", - "Mocap" - ], - "description": "Matchmoving script", - "detailed_description": "Script exported from matchmoving application to be later processed into a tracked camera with additional data", - "allow_sequences": False, - "allow_multiple_items": True, - "allow_version_control": False, - "extensions": [] - }, - { - "product_type": "rig", - "identifier": "", - "label": "Rig", - "icon": "fa.wheelchair", - "default_variants": [], - "description": "CG rig file", - "detailed_description": "CG rigged character or prop. Rig should be clean of any extra data and directly loadable into it's respective application\t", - "allow_sequences": False, - "allow_multiple_items": False, - "allow_version_control": False, - "extensions": [ - ".ma", - ".blend", - ".hip", - ".hda" - ] - }, - { - "product_type": "simpleUnrealTexture", - "identifier": "", - "label": "Simple UE texture", - "icon": "fa.image", - "default_variants": [], - "description": "Simple Unreal Engine texture", - "detailed_description": "Texture files with Unreal Engine naming conventions", - "allow_sequences": False, - "allow_multiple_items": True, - "allow_version_control": False, - "extensions": [] - }, - { - "product_type": "audio", - "identifier": "", - "label": "Audio ", - "icon": "fa5s.file-audio", - "default_variants": [ - "Main" - ], - "description": "Audio product", - "detailed_description": "Audio files for review or final delivery", - "allow_sequences": False, - "allow_multiple_items": False, - "allow_version_control": False, - "extensions": [ - ".wav" - ] - } -]