diff --git a/pype/plugins/global/publish/collect_anatomy.py b/pype/plugins/global/publish/collect_anatomy.py new file mode 100644 index 0000000000..9412209850 --- /dev/null +++ b/pype/plugins/global/publish/collect_anatomy.py @@ -0,0 +1,20 @@ +""" +Requires: + None +Provides: + context -> anatomy (pypeapp.Anatomy) +""" + +from pypeapp import Anatomy +import pyblish.api + + +class CollectAnatomy(pyblish.api.ContextPlugin): + """Collect Anatomy into Context""" + + order = pyblish.api.CollectorOrder + label = "Collect Anatomy" + + def process(self, context): + context.data['anatomy'] = Anatomy() + self.log.info("Anatomy templates collected...") diff --git a/pype/plugins/global/publish/collect_comment.py b/pype/plugins/global/publish/collect_comment.py index 5bbd1da2a1..22970665a1 100644 --- a/pype/plugins/global/publish/collect_comment.py +++ b/pype/plugins/global/publish/collect_comment.py @@ -1,3 +1,10 @@ +""" +Requires: + None +Provides: + context -> comment (str) +""" + import pyblish.api diff --git a/pype/plugins/global/publish/collect_context.py b/pype/plugins/global/publish/collect_context.py index 31ab95259c..5f443ac5fb 100644 --- a/pype/plugins/global/publish/collect_context.py +++ b/pype/plugins/global/publish/collect_context.py @@ -1,3 +1,18 @@ +""" +Requires: + environment -> SAPUBLISH_INPATH + environment -> SAPUBLISH_OUTPATH + +Provides: + context -> returnJsonPath (str) + context -> project + context -> asset + instance -> destination_list (list) + instance -> representations (list) + instance -> source (list) + instance -> representations +""" + import os import pyblish.api from avalon import io @@ -31,9 +46,25 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): in_data = json.load(f) asset_name = in_data['asset'] + family_preset_key = in_data.get('family_preset_key', '') family = in_data['family'] subset = in_data['subset'] + # Load presets + presets = context.data.get("presets") + if not presets: + from pypeapp import config + presets = config.get_presets() + + # Get from presets anatomy key that will be used for getting template + # - default integrate new is used if not set + anatomy_key = presets.get( + "standalone_publish", {}).get( + "families", {}).get( + family_preset_key, {}).get( + "anatomy_template" + ) + project = io.find_one({'type': 'project'}) asset = io.find_one({ 'type': 'asset', @@ -50,6 +81,8 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): "label": subset, "name": subset, "family": family, + "frameStart": in_data.get("representations", [None])[0].get("frameStart", None), + "frameEnd": in_data.get("representations", [None])[0].get("frameEnd", None), "families": [family, 'ftrack'], }) self.log.info("collected instance: {}".format(instance.data)) @@ -63,7 +96,9 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): component['destination'] = component['files'] component['stagingDir'] = component['stagingDir'] - component['anatomy_template'] = 'render' + # Do not set anatomy_template if not specified + if anatomy_key: + component['anatomy_template'] = anatomy_key if isinstance(component['files'], list): collections, remainder = clique.assemble(component['files']) self.log.debug("collecting sequence: {}".format(collections)) diff --git a/pype/plugins/global/publish/collect_context_label.py b/pype/plugins/global/publish/collect_context_label.py index ec8e0f7cdc..9c07d7de5b 100644 --- a/pype/plugins/global/publish/collect_context_label.py +++ b/pype/plugins/global/publish/collect_context_label.py @@ -1,3 +1,10 @@ +""" +Requires: + context -> currentFile (str) +Provides: + context -> label (str) +""" + import os import pyblish.api diff --git a/pype/plugins/global/publish/collect_current_shell_file.py b/pype/plugins/global/publish/collect_current_shell_file.py index a467459bc8..961cad86a1 100644 --- a/pype/plugins/global/publish/collect_current_shell_file.py +++ b/pype/plugins/global/publish/collect_current_shell_file.py @@ -1,3 +1,11 @@ +""" +Requires: + None + +Provides: + context -> currentFile (str) +""" + import os import pyblish.api diff --git a/pype/plugins/global/publish/collect_deadline_user.py b/pype/plugins/global/publish/collect_deadline_user.py index 624e455251..125f9d0d26 100644 --- a/pype/plugins/global/publish/collect_deadline_user.py +++ b/pype/plugins/global/publish/collect_deadline_user.py @@ -1,3 +1,11 @@ +""" +Requires: + environment -> DEADLINE_PATH + +Provides: + context -> deadlineUser (str) +""" + import os import subprocess @@ -54,4 +62,3 @@ class CollectDeadlineUser(pyblish.api.ContextPlugin): self.log.info("Found Deadline user: {}".format(user)) context.data['deadlineUser'] = user - diff --git a/pype/plugins/global/publish/collect_filesequences.py b/pype/plugins/global/publish/collect_filesequences.py index 33531549cb..73f3a459c8 100644 --- a/pype/plugins/global/publish/collect_filesequences.py +++ b/pype/plugins/global/publish/collect_filesequences.py @@ -1,3 +1,13 @@ +""" +Requires: + environment -> PYPE_PUBLISH_PATHS + context -> workspaceDir + +Provides: + context -> user (str) + instance -> new instance +""" + import os import re import copy diff --git a/pype/plugins/global/publish/collect_machine_name.py b/pype/plugins/global/publish/collect_machine_name.py index 02360cff04..72ef68f8ed 100644 --- a/pype/plugins/global/publish/collect_machine_name.py +++ b/pype/plugins/global/publish/collect_machine_name.py @@ -1,3 +1,11 @@ +""" +Requires: + none + +Provides: + context -> machine (str) +""" + import pyblish.api diff --git a/pype/plugins/global/publish/collect_output_repre_config.py b/pype/plugins/global/publish/collect_output_repre_config.py index 5595e29cab..73ab050bcf 100644 --- a/pype/plugins/global/publish/collect_output_repre_config.py +++ b/pype/plugins/global/publish/collect_output_repre_config.py @@ -1,5 +1,11 @@ -import os -import json +""" +Requires: + config_data -> ftrack.output_representation + +Provides: + context -> output_repre_config (str) +""" + import pyblish.api from pypeapp import config @@ -9,7 +15,7 @@ class CollectOutputRepreConfig(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder label = "Collect Config for representation" - hosts = ["shell"] + hosts = ["shell", "standalonepublisher"] def process(self, context): config_data = config.get_presets()["ftrack"]["output_representation"] diff --git a/pype/plugins/global/publish/collect_presets.py b/pype/plugins/global/publish/collect_presets.py index 7e0d3e2f4b..5e79c555e2 100644 --- a/pype/plugins/global/publish/collect_presets.py +++ b/pype/plugins/global/publish/collect_presets.py @@ -1,3 +1,12 @@ +""" +Requires: + config_data -> colorspace.default + config_data -> dataflow.default + +Provides: + context -> presets +""" + from pyblish import api from pypeapp import config @@ -5,7 +14,7 @@ from pypeapp import config class CollectPresets(api.ContextPlugin): """Collect Presets.""" - order = api.CollectorOrder + order = api.CollectorOrder - 0.491 label = "Collect Presets" def process(self, context): diff --git a/pype/plugins/global/publish/collect_project_data.py b/pype/plugins/global/publish/collect_project_data.py index de51ad880c..acdbc2c41f 100644 --- a/pype/plugins/global/publish/collect_project_data.py +++ b/pype/plugins/global/publish/collect_project_data.py @@ -1,8 +1,15 @@ +""" +Requires: + None + +Provides: + context -> projectData +""" + import pyblish.api import pype.api as pype - class CollectProjectData(pyblish.api.ContextPlugin): """Collecting project data from avalon db""" diff --git a/pype/plugins/global/publish/collect_scene_version.py b/pype/plugins/global/publish/collect_scene_version.py index 12075e2417..3fac823b5c 100644 --- a/pype/plugins/global/publish/collect_scene_version.py +++ b/pype/plugins/global/publish/collect_scene_version.py @@ -13,6 +13,8 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): label = 'Collect Version' def process(self, context): + if "standalonepublisher" in context.data.get("host"): + return filename = os.path.basename(context.data.get('currentFile')) diff --git a/pype/plugins/global/publish/collect_templates.py b/pype/plugins/global/publish/collect_templates.py index fe48e97c03..b3aecca21a 100644 --- a/pype/plugins/global/publish/collect_templates.py +++ b/pype/plugins/global/publish/collect_templates.py @@ -1,16 +1,86 @@ +""" +Requires: + session -> AVALON_PROJECT + context -> anatomy (pypeapp.Anatomy) + instance -> subset + instance -> asset + instance -> family -import pype.api as pype -from pypeapp import Anatomy +Provides: + instance -> template + instance -> assumedTemplateData + instance -> assumedDestination +""" +import os + +from avalon import io, api import pyblish.api -class CollectTemplates(pyblish.api.ContextPlugin): - """Inject the current working file into context""" +class CollectTemplates(pyblish.api.InstancePlugin): + """Fill templates with data needed for publish""" - order = pyblish.api.CollectorOrder - label = "Collect Templates" + order = pyblish.api.CollectorOrder + 0.1 + label = "Collect and fill Templates" - def process(self, context): - context.data['anatomy'] = Anatomy() - self.log.info("Anatomy templates collected...") + def process(self, instance): + # get all the stuff from the database + subset_name = instance.data["subset"] + asset_name = instance.data["asset"] + project_name = api.Session["AVALON_PROJECT"] + + project = io.find_one({"type": "project", + "name": project_name}, + projection={"config": True, "data": True}) + + template = project["config"]["template"]["publish"] + anatomy = instance.context.data['anatomy'] + + asset = io.find_one({"type": "asset", + "name": asset_name, + "parent": project["_id"]}) + + assert asset, ("No asset found by the name '{}' " + "in project '{}'".format(asset_name, project_name)) + silo = asset['silo'] + + subset = io.find_one({"type": "subset", + "name": subset_name, + "parent": asset["_id"]}) + + # assume there is no version yet, we start at `1` + version = None + version_number = 1 + if subset is not None: + version = io.find_one({"type": "version", + "parent": subset["_id"]}, + sort=[("name", -1)]) + + # if there is a subset there ought to be version + if version is not None: + version_number += int(version["name"]) + + hierarchy = asset['data']['parents'] + if hierarchy: + # hierarchy = os.path.sep.join(hierarchy) + hierarchy = os.path.join(*hierarchy) + + template_data = {"root": api.Session["AVALON_PROJECTS"], + "project": {"name": project_name, + "code": project['data']['code']}, + "silo": silo, + "family": instance.data['family'], + "asset": asset_name, + "subset": subset_name, + "version": version_number, + "hierarchy": hierarchy, + "representation": "TEMP"} + + instance.data["template"] = template + instance.data["assumedTemplateData"] = template_data + + # We take the parent folder of representation 'filepath' + instance.data["assumedDestination"] = os.path.dirname( + (anatomy.format(template_data))["publish"]["path"] + ) diff --git a/pype/plugins/global/publish/extract_thumbnail_sa.py b/pype/plugins/global/publish/extract_thumbnail_sa.py new file mode 100644 index 0000000000..f42985b560 --- /dev/null +++ b/pype/plugins/global/publish/extract_thumbnail_sa.py @@ -0,0 +1,126 @@ +import os +import tempfile +import subprocess +import pyblish.api +import pype.api + + +class ExtractThumbnail(pyblish.api.InstancePlugin): + """Extract jpeg thumbnail from component input from standalone publisher + + Uses jpeg file from component if possible (when single or multiple jpegs + are loaded to component selected as thumbnail) otherwise extracts from + input file/s single jpeg to temp. + """ + + label = "Extract Thumbnail" + hosts = ["standalonepublisher"] + order = pyblish.api.ExtractorOrder + + def process(self, instance): + repres = instance.data.get('representations') + if not repres: + return + + thumbnail_repre = None + for repre in repres: + if repre.get("thumbnail"): + thumbnail_repre = repre + break + + if not thumbnail_repre: + return + + files = thumbnail_repre.get("files") + if not files: + return + + if isinstance(files, list): + files_len = len(files) + file = str(files[0]) + else: + files_len = 1 + file = files + + is_jpeg = False + if file.endswith(".jpeg") or file.endswith(".jpg"): + is_jpeg = True + + if is_jpeg and files_len == 1: + # skip if already is single jpeg file + return + + elif is_jpeg: + # use first frame as thumbnail if is sequence of jpegs + full_thumbnail_path = file + self.log.info( + "For thumbnail is used file: {}".format(full_thumbnail_path) + ) + + else: + # Convert to jpeg if not yet + full_input_path = os.path.join(thumbnail_repre["stagingDir"], file) + self.log.info("input {}".format(full_input_path)) + + full_thumbnail_path = tempfile.mkstemp(suffix=".jpg")[1] + self.log.info("output {}".format(full_thumbnail_path)) + + config_data = instance.context.data.get("output_repre_config", {}) + + proj_name = os.environ.get("AVALON_PROJECT", "__default__") + profile = config_data.get( + proj_name, + config_data.get("__default__", {}) + ) + + ffmpeg_path = os.getenv("FFMPEG_PATH", "") + if ffmpeg_path: + ffmpeg_path += "/ffmpeg" + else: + ffmpeg_path = "ffmpeg" + + jpeg_items = [] + jpeg_items.append(ffmpeg_path) + # override file if already exists + jpeg_items.append("-y") + # add input filters from peresets + if profile: + jpeg_items.extend(profile.get('input', [])) + # input file + jpeg_items.append("-i {}".format(full_input_path)) + # extract only single file + jpeg_items.append("-vframes 1") + # output file + jpeg_items.append(full_thumbnail_path) + + subprocess_jpeg = " ".join(jpeg_items) + + # run subprocess + self.log.debug("Executing: {}".format(subprocess_jpeg)) + subprocess.Popen( + subprocess_jpeg, + stdout=subprocess.PIPE, + shell=True + ) + + # remove thumbnail key from origin repre + thumbnail_repre.pop("thumbnail") + + filename = os.path.basename(full_thumbnail_path) + staging_dir = os.path.dirname(full_thumbnail_path) + + # create new thumbnail representation + representation = { + 'name': 'jpg', + 'ext': 'jpg', + 'files': filename, + "stagingDir": staging_dir, + "thumbnail": True, + "tags": [] + } + + # # add Delete tag when temp file was rendered + # if not is_jpeg: + # representation["tags"].append("delete") + + instance.data["representations"].append(representation) diff --git a/pype/plugins/global/publish/integrate_assumed_destination.py b/pype/plugins/global/publish/integrate_assumed_destination.py index 6999ce6ab8..3bbd4cf33b 100644 --- a/pype/plugins/global/publish/integrate_assumed_destination.py +++ b/pype/plugins/global/publish/integrate_assumed_destination.py @@ -30,7 +30,8 @@ class IntegrateAssumedDestination(pyblish.api.InstancePlugin): "resources") # Clean the path - mock_destination = os.path.abspath(os.path.normpath(mock_destination)).replace("\\", "/") + mock_destination = os.path.abspath( + os.path.normpath(mock_destination)).replace("\\", "/") # Define resource destination and transfers resources = instance.data.get("resources", list()) @@ -38,7 +39,8 @@ class IntegrateAssumedDestination(pyblish.api.InstancePlugin): for resource in resources: # Add destination to the resource - source_filename = os.path.basename(resource["source"]).replace("\\", "/") + source_filename = os.path.basename( + resource["source"]).replace("\\", "/") destination = os.path.join(mock_destination, source_filename) # Force forward slashes to fix issue with software unable @@ -53,7 +55,8 @@ class IntegrateAssumedDestination(pyblish.api.InstancePlugin): files = resource['files'] for fsrc in files: fname = os.path.basename(fsrc) - fdest = os.path.join(mock_destination, fname).replace("\\", "/") + fdest = os.path.join( + mock_destination, fname).replace("\\", "/") transfers.append([fsrc, fdest]) instance.data["resources"] = resources diff --git a/pype/standalonepublish/publish.py b/pype/standalonepublish/publish.py index 13b505666c..f199aaf84e 100644 --- a/pype/standalonepublish/publish.py +++ b/pype/standalonepublish/publish.py @@ -103,7 +103,7 @@ def avalon_api_publish(data, gui=True): "-pp", os.pathsep.join(pyblish.api.registered_paths()) ] - os.environ["PYBLISH_HOSTS"] = "shell" + os.environ["PYBLISH_HOSTS"] = "standalonepublisher" os.environ["SAPUBLISH_INPATH"] = json_data_path if gui: @@ -139,7 +139,7 @@ def cli_publish(data, gui=True): if gui: args += ["gui"] - os.environ["PYBLISH_HOSTS"] = "shell" + os.environ["PYBLISH_HOSTS"] = "standalonepublisher" os.environ["SAPUBLISH_INPATH"] = json_data_path os.environ["SAPUBLISH_OUTPATH"] = return_data_path diff --git a/pype/standalonepublish/widgets/__init__.py b/pype/standalonepublish/widgets/__init__.py index 4c6a0e85a5..c6e0dd9a47 100644 --- a/pype/standalonepublish/widgets/__init__.py +++ b/pype/standalonepublish/widgets/__init__.py @@ -6,6 +6,7 @@ HelpRole = QtCore.Qt.UserRole + 2 FamilyRole = QtCore.Qt.UserRole + 3 ExistsRole = QtCore.Qt.UserRole + 4 PluginRole = QtCore.Qt.UserRole + 5 +PluginKeyRole = QtCore.Qt.UserRole + 6 from ..resources import get_resource from .button_from_svgs import SvgResizable, SvgButton diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index e60db892db..a5a686bae1 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -220,15 +220,21 @@ class DropDataFrame(QtWidgets.QFrame): self._process_data(data) def load_data_with_probe(self, filepath): + ffprobe_path = os.getenv("FFMPEG_PATH", "") + if ffprobe_path: + ffprobe_path += '/ffprobe' + else: + ffprobe_path = 'ffprobe' + args = [ - 'ffprobe', + ffprobe_path, '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', filepath ] ffprobe_p = subprocess.Popen( - args, + ' '.join(args), stdout=subprocess.PIPE, shell=True ) diff --git a/pype/standalonepublish/widgets/widget_family.py b/pype/standalonepublish/widgets/widget_family.py index 63776b1df3..26eb8077d9 100644 --- a/pype/standalonepublish/widgets/widget_family.py +++ b/pype/standalonepublish/widgets/widget_family.py @@ -5,7 +5,7 @@ import json from collections import namedtuple from . import QtWidgets, QtCore -from . import HelpRole, FamilyRole, ExistsRole, PluginRole +from . import HelpRole, FamilyRole, ExistsRole, PluginRole, PluginKeyRole from . import FamilyDescriptionWidget from pypeapp import config @@ -116,8 +116,10 @@ class FamilyWidget(QtWidgets.QWidget): def collect_data(self): plugin = self.list_families.currentItem().data(PluginRole) + key = self.list_families.currentItem().data(PluginKeyRole) family = plugin.family.rsplit(".", 1)[-1] data = { + 'family_preset_key': key, 'family': family, 'subset': self.input_result.text(), 'version': self.version_spinbox.value() @@ -318,7 +320,7 @@ class FamilyWidget(QtWidgets.QWidget): has_families = False presets = config.get_presets().get('standalone_publish', {}) - for creator in presets.get('families', {}).values(): + for key, creator in presets.get('families', {}).items(): creator = namedtuple("Creator", creator.keys())(*creator.values()) label = creator.label or creator.family @@ -327,6 +329,7 @@ class FamilyWidget(QtWidgets.QWidget): item.setData(HelpRole, creator.help or "") item.setData(FamilyRole, creator.family) item.setData(PluginRole, creator) + item.setData(PluginKeyRole, key) item.setData(ExistsRole, False) self.list_families.addItem(item)