diff --git a/pype/api.py b/pype/api.py index fcdcbce82b..0acb80e383 100644 --- a/pype/api.py +++ b/pype/api.py @@ -46,8 +46,6 @@ from .lib import ( get_data_hierarchical_attr ) -from .widgets.message_window import message - __all__ = [ # plugin classes "Extractor", @@ -89,7 +87,4 @@ __all__ = [ "Colorspace", "Dataflow", - # QtWidgets - "message" - ] diff --git a/pype/ftrack/actions/action_delete_asset.py b/pype/ftrack/actions/action_delete_asset.py index eabadecee6..96087f4c8e 100644 --- a/pype/ftrack/actions/action_delete_asset.py +++ b/pype/ftrack/actions/action_delete_asset.py @@ -85,6 +85,12 @@ class DeleteAsset(BaseAction): 'type': 'asset', 'name': entity['name'] }) + + if av_entity is None: + return { + 'success': False, + 'message': 'Didn\'t found assets in avalon' + } asset_label = { 'type': 'label', diff --git a/pype/nuke/__init__.py b/pype/nuke/__init__.py index 376e8f95b8..a2b1aeda6e 100644 --- a/pype/nuke/__init__.py +++ b/pype/nuke/__init__.py @@ -25,8 +25,6 @@ from pypeapp import Logger log = Logger().get_logger(__name__, "nuke") -# log = api.Logger.getLogger(__name__, "nuke") - AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") PARENT_DIR = os.path.dirname(__file__) @@ -38,9 +36,8 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "nuke", "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "nuke", "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "nuke", "inventory") -self = sys.modules[__name__] -self.nLogger = None +# registering pyblish gui regarding settings in presets if os.getenv("PYBLISH_GUI", None): pyblish.register_gui(os.getenv("PYBLISH_GUI", None)) @@ -66,6 +63,7 @@ class NukeHandler(logging.Handler): "fatal", "error" ]: + msg = self.format(record) nuke.message(msg) @@ -77,9 +75,6 @@ if nuke_handler.get_name() \ logging.getLogger().addHandler(nuke_handler) logging.getLogger().setLevel(logging.INFO) -if not self.nLogger: - self.nLogger = Logger - def reload_config(): """Attempt to reload pipeline at run-time. @@ -157,7 +152,7 @@ def uninstall(): def on_pyblish_instance_toggled(instance, old_value, new_value): """Toggle node passthrough states on instance toggles.""" - self.log.info("instance toggle: {}, old_value: {}, new_value:{} ".format( + log.info("instance toggle: {}, old_value: {}, new_value:{} ".format( instance, old_value, new_value)) from avalon.nuke import ( diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index 46b1d6e4c8..20e7dfb210 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -29,6 +29,53 @@ def onScriptLoad(): nuke.tcl('load movWriter') +def checkInventoryVersions(): + """ + Actiual version idetifier of Loaded containers + + Any time this function is run it will check all nodes and filter only Loader nodes for its version. It will get all versions from database + and check if the node is having actual version. If not then it will color it to red. + + """ + + + # get all Loader nodes by avalon attribute metadata + for each in nuke.allNodes(): + if each.Class() == 'Read': + container = avalon.nuke.parse_container(each) + + if container: + node = container["_tool"] + avalon_knob_data = get_avalon_knob_data(node) + + # get representation from io + representation = io.find_one({ + "type": "representation", + "_id": io.ObjectId(avalon_knob_data["representation"]) + }) + + # Get start frame from version data + version = io.find_one({ + "type": "version", + "_id": representation["parent"] + }) + + # get all versions in list + versions = io.find({ + "type": "version", + "parent": version["parent"] + }).distinct('name') + + max_version = max(versions) + + # check the available version and do match + # change color of node if not max verion + if version.get("name") not in [max_version]: + node["tile_color"].setValue(int("0xd84f20ff", 16)) + else: + node["tile_color"].setValue(int("0x4ecd25ff", 16)) + + def writes_version_sync(): try: rootVersion = pype.get_version_from_path(nuke.root().name()) diff --git a/pype/plugins/nuke/load/load_script_precomp.py b/pype/plugins/nuke/load/load_script_precomp.py new file mode 100644 index 0000000000..6fd76edd03 --- /dev/null +++ b/pype/plugins/nuke/load/load_script_precomp.py @@ -0,0 +1,170 @@ +from avalon import api, style, io +from pype.nuke.lib import get_avalon_knob_data +import nuke +import os +from pype.api import Logger +log = Logger().get_logger(__name__, "nuke") + + + +class LinkAsGroup(api.Loader): + """Copy the published file to be pasted at the desired location""" + + representations = ["nk"] + families = ["*"] + + label = "Load Precomp" + order = 10 + icon = "file" + color = style.colors.dark + + def load(self, context, name, namespace, data): + + from avalon.nuke import containerise + # for k, v in context.items(): + # log.info("key: `{}`, value: {}\n".format(k, v)) + version = context['version'] + version_data = version.get("data", {}) + + vname = version.get("name", None) + first = version_data.get("startFrame", None) + last = version_data.get("endFrame", None) + + # Fallback to asset name when namespace is None + if namespace is None: + namespace = context['asset']['name'] + + file = self.fname.replace("\\", "/") + self.log.info("file: {}\n".format(self.fname)) + + precomp_name = context["representation"]["context"]["subset"] + + # Set global in point to start frame (if in version.data) + start = context["version"]["data"].get("startFrame", None) + + # add additional metadata from the version to imprint to Avalon knob + add_keys = ["startFrame", "endFrame", "handles", + "source", "author", "fps"] + + data_imprint = { + "start_frame": start, + "fstart": first, + "fend": last, + "version": vname + } + for k in add_keys: + data_imprint.update({k: context["version"]['data'][k]}) + data_imprint.update({"objectName": precomp_name}) + + # group context is set to precomp, so back up one level. + nuke.endGroup() + + # P = nuke.nodes.LiveGroup("file {}".format(file)) + P = nuke.createNode( + "Precomp", + "file {}".format(file)) + + # Set colorspace defined in version data + colorspace = context["version"]["data"].get("colorspace", None) + self.log.info("colorspace: {}\n".format(colorspace)) + + + # ['version', 'file', 'reading', 'output', 'useOutput'] + + P["name"].setValue("{}_{}".format(name, namespace)) + P["useOutput"].setValue(True) + + with P: + # iterate trough all nodes in group node and find pype writes + writes = [n.name() for n in nuke.allNodes() + if n.Class() == "Write" + if get_avalon_knob_data(n)] + + # create panel for selecting output + panel_choices = " ".join(writes) + panel_label = "Select write node for output" + p = nuke.Panel("Select Write Node") + p.addEnumerationPulldown( + panel_label, panel_choices) + p.show() + P["output"].setValue(p.value(panel_label)) + + P["tile_color"].setValue(0xff0ff0ff) + + return containerise( + node=P, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + data=data_imprint) + + def switch(self, container, representation): + self.update(container, representation) + + def update(self, container, representation): + """Update the Loader's path + + Nuke automatically tries to reset some variables when changing + the loader's path to a new file. These automatic changes are to its + inputs: + + """ + + from avalon.nuke import ( + update_container + ) + + node = nuke.toNode(container['objectName']) + + root = api.get_representation_path(representation).replace("\\","/") + + # Get start frame from version data + version = io.find_one({ + "type": "version", + "_id": representation["parent"] + }) + + # get all versions in list + versions = io.find({ + "type": "version", + "parent": version["parent"] + }).distinct('name') + + max_version = max(versions) + + updated_dict = {} + updated_dict.update({ + "representation": str(representation["_id"]), + "endFrame": version["data"].get("endFrame"), + "version": version.get("name"), + "colorspace": version["data"].get("colorspace"), + "source": version["data"].get("source"), + "handles": version["data"].get("handles"), + "fps": version["data"].get("fps"), + "author": version["data"].get("author"), + "outputDir": version["data"].get("outputDir"), + }) + + # Update the imprinted representation + update_container( + node, + updated_dict + ) + + node["file"].setValue(root) + + # change color of node + if version.get("name") not in [max_version]: + node["tile_color"].setValue(int("0xd84f20ff", 16)) + else: + node["tile_color"].setValue(int("0xff0ff0ff", 16)) + + log.info("udated to version: {}".format(version.get("name"))) + + + def remove(self, container): + from avalon.nuke import viewer_update_and_undo_stop + node = nuke.toNode(container['objectName']) + with viewer_update_and_undo_stop(): + nuke.delete(node) diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py index b4e3cfb8b5..f03e0fc97e 100644 --- a/pype/plugins/nuke/load/load_sequence.py +++ b/pype/plugins/nuke/load/load_sequence.py @@ -128,11 +128,15 @@ class LoadSequence(api.Loader): # add additional metadata from the version to imprint to Avalon knob add_keys = ["startFrame", "endFrame", "handles", - "source", "colorspace", "author", "fps"] + "source", "colorspace", "author", "fps", "version"] data_imprint = {} for k in add_keys: - data_imprint.update({k: context["version"]['data'][k]}) + if k is 'version': + data_imprint.update({k: context["version"]['name']}) + else: + data_imprint.update({k: context["version"]['data'][k]}) + data_imprint.update({"objectName": read_name}) r["tile_color"].setValue(int("0x4ecd25ff", 16)) diff --git a/pype/plugins/standalonepublish/publish/collect_context.py b/pype/plugins/standalonepublish/publish/collect_context.py index cbe9df1ef6..6ac2dca936 100644 --- a/pype/plugins/standalonepublish/publish/collect_context.py +++ b/pype/plugins/standalonepublish/publish/collect_context.py @@ -55,10 +55,10 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): instance = context.create_instance(subset) instance.data.update({ - "subset": family + subset, + "subset": subset, "asset": asset_name, - "label": family + subset, - "name": family + subset, + "label": subset, + "name": subset, "family": family, "families": [family, 'ftrack'], }) @@ -74,10 +74,9 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): collections, remainder = clique.assemble(component['files']) if collections: self.log.debug(collections) - range = collections[0].format('{range}') - instance.data['startFrame'] = range.split('-')[0] - instance.data['endFrame'] = range.split('-')[1] - + instance.data['startFrame'] = component['startFrame'] + instance.data['endFrame'] = component['endFrame'] + instance.data['frameRate'] = component['frameRate'] instance.data["files"].append(component) instance.data["representations"].append(component) diff --git a/pype/plugins/standalonepublish/publish/integrate_ftrack_instances.py b/pype/plugins/standalonepublish/publish/integrate_ftrack_instances.py index 8d938bceb0..0dc9bb137c 100644 --- a/pype/plugins/standalonepublish/publish/integrate_ftrack_instances.py +++ b/pype/plugins/standalonepublish/publish/integrate_ftrack_instances.py @@ -57,19 +57,19 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "name": "thumbnail" # Default component name is "main". } elif comp['preview']: - if not instance.data.get('startFrameReview'): - instance.data['startFrameReview'] = instance.data['startFrame'] - if not instance.data.get('endFrameReview'): - instance.data['endFrameReview'] = instance.data['endFrame'] + if not comp.get('startFrameReview'): + comp['startFrameReview'] = comp['startFrame'] + if not comp.get('endFrameReview'): + comp['endFrameReview'] = instance.data['endFrame'] location = ft_session.query( 'Location where name is "ftrack.server"').one() component_data = { # Default component name is "main". "name": "ftrackreview-mp4", "metadata": {'ftr_meta': json.dumps({ - 'frameIn': int(instance.data['startFrameReview']), - 'frameOut': int(instance.data['endFrameReview']), - 'frameRate': 25.0})} + 'frameIn': int(comp['startFrameReview']), + 'frameOut': int(comp['endFrameReview']), + 'frameRate': float(comp['frameRate')]})} } else: component_data = { diff --git a/pype/premiere/__init__.py b/pype/premiere/__init__.py index 74ce106de2..cc5abe115e 100644 --- a/pype/premiere/__init__.py +++ b/pype/premiere/__init__.py @@ -7,6 +7,8 @@ from pyblish import api as pyblish from pypeapp import Logger from .. import api +from ..widgets.message_window import message + import requests log = Logger().get_logger(__name__, "premiere") @@ -42,7 +44,7 @@ def request_aport(url_path, data={}): return req except Exception as e: - api.message(title="Premiere Aport Server", + message(title="Premiere Aport Server", message="Before you can run Premiere, start Aport Server. \n Error: {}".format( e), level="critical") @@ -99,7 +101,7 @@ def install(): # synchronize extensions extensions_sync() - api.message(title="pyblish_paths", message=str(reg_paths), level="info") + message(title="pyblish_paths", message=str(reg_paths), level="info") def uninstall(): diff --git a/pype/standalonepublish/app.py b/pype/standalonepublish/app.py index 956cdb6300..da5fbbba10 100644 --- a/pype/standalonepublish/app.py +++ b/pype/standalonepublish/app.py @@ -26,7 +26,6 @@ class Window(QtWidgets.QDialog): initialized = False WIDTH = 1100 HEIGHT = 500 - NOT_SELECTED = '< Nothing is selected >' def __init__(self, parent=None): super(Window, self).__init__(parent=parent) @@ -40,19 +39,9 @@ class Window(QtWidgets.QDialog): # Validators self.valid_parent = False - # statusbar - added under asset_widget - label_message = QtWidgets.QLabel() - label_message.setFixedHeight(20) - # assets widget - widget_assets_wrap = QtWidgets.QWidget() - widget_assets_wrap.setContentsMargins(0, 0, 0, 0) widget_assets = AssetWidget(self) - layout_assets = QtWidgets.QVBoxLayout(widget_assets_wrap) - layout_assets.addWidget(widget_assets) - layout_assets.addWidget(label_message) - # family widget widget_family = FamilyWidget(self) @@ -67,10 +56,10 @@ class Window(QtWidgets.QDialog): QtWidgets.QSizePolicy.Expanding ) body.setOrientation(QtCore.Qt.Horizontal) - body.addWidget(widget_assets_wrap) + body.addWidget(widget_assets) body.addWidget(widget_family) body.addWidget(widget_components) - body.setStretchFactor(body.indexOf(widget_assets_wrap), 2) + body.setStretchFactor(body.indexOf(widget_assets), 2) body.setStretchFactor(body.indexOf(widget_family), 3) body.setStretchFactor(body.indexOf(widget_components), 5) @@ -82,13 +71,10 @@ class Window(QtWidgets.QDialog): # signals widget_assets.selection_changed.connect(self.on_asset_changed) - self.label_message = label_message self.widget_assets = widget_assets self.widget_family = widget_family self.widget_components = widget_components - self.echo("Connected to Database") - # on start self.on_start() @@ -131,22 +117,6 @@ class Window(QtWidgets.QDialog): parents.append(parent['name']) return parents - def echo(self, message): - ''' Shows message in label that disappear in 5s - :param message: Message that will be displayed - :type message: str - ''' - self.label_message.setText(str(message)) - def clear_text(): - ''' Helps prevent crash if this Window object - is deleted before 5s passed - ''' - try: - self.label_message.set_text("") - except: - pass - QtCore.QTimer.singleShot(5000, lambda: clear_text()) - def on_asset_changed(self): '''Callback on asset selection changed @@ -160,7 +130,7 @@ class Window(QtWidgets.QDialog): self.widget_family.change_asset(asset['name']) else: self.valid_parent = False - self.widget_family.change_asset(self.NOT_SELECTED) + self.widget_family.change_asset(None) self.widget_family.on_data_changed() def keyPressEvent(self, event): diff --git a/pype/standalonepublish/widgets/__init__.py b/pype/standalonepublish/widgets/__init__.py index cd99e15bed..4c6a0e85a5 100644 --- a/pype/standalonepublish/widgets/__init__.py +++ b/pype/standalonepublish/widgets/__init__.py @@ -20,15 +20,14 @@ from .model_tree_view_deselectable import DeselectableTreeView from .widget_asset_view import AssetView from .widget_asset import AssetWidget + from .widget_family_desc import FamilyDescriptionWidget from .widget_family import FamilyWidget from .widget_drop_empty import DropEmpty from .widget_component_item import ComponentItem from .widget_components_list import ComponentsList - from .widget_drop_frame import DropDataFrame - from .widget_components import ComponentsWidget -from.widget_shadow import ShadowWidget +from .widget_shadow import ShadowWidget diff --git a/pype/standalonepublish/widgets/model_tasks_template.py b/pype/standalonepublish/widgets/model_tasks_template.py index 4af3b9eea7..bd1984029c 100644 --- a/pype/standalonepublish/widgets/model_tasks_template.py +++ b/pype/standalonepublish/widgets/model_tasks_template.py @@ -8,13 +8,13 @@ class TasksTemplateModel(TreeModel): COLUMNS = ["Tasks"] - def __init__(self): + def __init__(self, selectable=True): super(TasksTemplateModel, self).__init__() - self.selectable = False - self._icons = { - "__default__": awesome.icon("fa.folder-o", - color=style.colors.default) - } + self.selectable = selectable + self.icon = awesome.icon( + 'fa.calendar-check-o', + color=style.colors.default + ) def set_tasks(self, tasks): """Set assets to track by their database id @@ -32,13 +32,11 @@ class TasksTemplateModel(TreeModel): self.beginResetModel() - icon = self._icons["__default__"] for task in tasks: node = Node({ "Tasks": task, - "icon": icon + "icon": self.icon }) - self.add_child(node) self.endResetModel() diff --git a/pype/standalonepublish/widgets/widget_asset.py b/pype/standalonepublish/widgets/widget_asset.py index 45e9757d71..54b7f7db44 100644 --- a/pype/standalonepublish/widgets/widget_asset.py +++ b/pype/standalonepublish/widgets/widget_asset.py @@ -2,6 +2,8 @@ import contextlib from . import QtWidgets, QtCore from . import RecursiveSortFilterProxyModel, AssetModel, AssetView from . import awesome, style +from . import TasksTemplateModel, DeselectableTreeView + @contextlib.contextmanager def preserve_expanded_rows(tree_view, @@ -128,7 +130,7 @@ class AssetWidget(QtWidgets.QWidget): self.parent_widget = parent - layout = QtWidgets.QVBoxLayout(self) + layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(4) @@ -163,12 +165,31 @@ class AssetWidget(QtWidgets.QWidget): layout.addLayout(header) layout.addWidget(view) + # tasks + task_view = DeselectableTreeView() + task_view.setIndentation(0) + task_view.setHeaderHidden(True) + task_view.setVisible(False) + + task_model = TasksTemplateModel() + task_view.setModel(task_model) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(4) + main_layout.addLayout(layout, 80) + main_layout.addWidget(task_view, 20) + # Signals/Slots selection = view.selectionModel() selection.selectionChanged.connect(self.selection_changed) selection.currentChanged.connect(self.current_changed) refresh.clicked.connect(self.refresh) + self.selection_changed.connect(self._refresh_tasks) + + self.task_view = task_view + self.task_model = task_model self.refreshButton = refresh self.model = model self.proxy = proxy @@ -181,10 +202,17 @@ class AssetWidget(QtWidgets.QWidget): def collect_data(self): project = self.db.find_one({'type': 'project'}) asset = self.db.find_one({'_id': self.get_active_asset()}) + + try: + index = self.task_view.selectedIndexes()[0] + task = self.task_model.itemData(index)[0] + except Exception: + task = None data = { 'project': project['name'], 'asset': asset['name'], - 'parents': self.get_parents(asset) + 'parents': self.get_parents(asset), + 'task': task } return data @@ -223,6 +251,18 @@ class AssetWidget(QtWidgets.QWidget): def refresh(self): self._refresh_model() + def _refresh_tasks(self): + tasks = [] + selected = self.get_selected_assets() + if len(selected) == 1: + asset = self.db.find_one({ + "_id": selected[0], "type": "asset" + }) + if asset: + tasks = asset.get('data', {}).get('tasks', []) + self.task_model.set_tasks(tasks) + self.task_view.setVisible(len(tasks)>0) + def get_active_asset(self): """Return the asset id the current asset.""" current = self.view.currentIndex() diff --git a/pype/standalonepublish/widgets/widget_component_item.py b/pype/standalonepublish/widgets/widget_component_item.py index 2e0df9a00c..a58a292ec5 100644 --- a/pype/standalonepublish/widgets/widget_component_item.py +++ b/pype/standalonepublish/widgets/widget_component_item.py @@ -10,6 +10,7 @@ class ComponentItem(QtWidgets.QFrame): C_HOVER = '#ffffff' C_ACTIVE = '#4BB543' C_ACTIVE_HOVER = '#4BF543' + signal_remove = QtCore.Signal(object) signal_thumbnail = QtCore.Signal(object) signal_preview = QtCore.Signal(object) @@ -283,12 +284,28 @@ class ComponentItem(QtWidgets.QFrame): self.preview.change_checked(hover) def collect_data(self): + in_files = self.in_data['files'] + staging_dir = os.path.dirname(in_files[0]) + + files = [os.path.basename(file) for file in in_files] + if len(files) == 1: + files = files[0] + data = { 'ext': self.in_data['ext'], 'label': self.name.text(), - 'representation': self.input_repre.text(), - 'files': self.in_data['files'], + 'name': self.input_repre.text(), + 'stagingDir': staging_dir, + 'files': files, 'thumbnail': self.is_thumbnail(), 'preview': self.is_preview() } + + if ('startFrame' in self.in_data and 'endFrame' in self.in_data): + data['startFrame'] = self.in_data['startFrame'] + data['endFrame'] = self.in_data['endFrame'] + + if 'frameRate' in self.in_data: + data['frameRate'] = self.in_data['frameRate'] + return data diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index cffe673152..4e99f697cb 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -1,5 +1,6 @@ import os import re +import json import clique import subprocess from pypeapp import config @@ -49,7 +50,7 @@ class DropDataFrame(QtWidgets.QFrame): else: # If path is in clipboard as string try: - path = ent.text() + path = os.path.normpath(ent.text()) if os.path.exists(path): paths.append(path) else: @@ -170,6 +171,13 @@ class DropDataFrame(QtWidgets.QFrame): repr_name = file_ext.replace('.', '') range = collection.format('{ranges}') + # TODO: ranges must not be with missing frames!!! + # - this is goal implementation: + # startFrame, endFrame = range.split('-') + rngs = range.split(',') + startFrame = rngs[0].split('-')[0] + endFrame = rngs[-1].split('-')[-1] + actions = [] data = { @@ -177,39 +185,15 @@ class DropDataFrame(QtWidgets.QFrame): 'name': file_base, 'ext': file_ext, 'file_info': range, + 'startFrame': startFrame, + 'endFrame': endFrame, 'representation': repr_name, 'folder_path': folder_path, 'is_sequence': True, 'actions': actions } - self._process_data(data) - def _get_ranges(self, indexes): - if len(indexes) == 1: - return str(indexes[0]) - ranges = [] - first = None - last = None - for index in indexes: - if first is None: - first = index - last = index - elif (last+1) == index: - last = index - else: - if first == last: - range = str(first) - else: - range = '{}-{}'.format(first, last) - ranges.append(range) - first = index - last = index - if first == last: - range = str(first) - else: - range = '{}-{}'.format(first, last) - ranges.append(range) - return ', '.join(ranges) + self._process_data(data) def _process_remainder(self, remainder): filename = os.path.basename(remainder) @@ -232,39 +216,75 @@ class DropDataFrame(QtWidgets.QFrame): 'is_sequence': False, 'actions': actions } - data['file_info'] = self.get_file_info(data) self._process_data(data) - def get_file_info(self, data): - output = None - if data['ext'] == '.mov': - try: - # ffProbe must be in PATH - filepath = data['files'][0] - args = ['ffprobe', '-show_streams', filepath] - p = subprocess.Popen( - args, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=True - ) - datalines=[] - for line in iter(p.stdout.readline, b''): - line = line.decode("utf-8").replace('\r\n', '') - datalines.append(line) + def load_data_with_probe(self, filepath): + args = [ + 'ffprobe', + '-v', 'quiet', + '-print_format', 'json', + '-show_format', + '-show_streams', filepath + ] + ffprobe_p = subprocess.Popen( + args, + stdout=subprocess.PIPE, + shell=True + ) + ffprobe_output = ffprobe_p.communicate()[0] + if ffprobe_p.returncode != 0: + raise RuntimeError( + 'Failed on ffprobe: check if ffprobe path is set in PATH env' + ) + return json.loads(ffprobe_output)['streams'][0] + + def get_file_data(self, data): + filepath = data['files'][0] + ext = data['ext'] + output = {} + probe_data = self.load_data_with_probe(filepath) + + if ( + ext in self.presets['extensions']['image_file'] or + ext in self.presets['extensions']['video_file'] + ): + if 'frameRate' not in data: + # default value + frameRate = 25 + frameRate_string = probe_data.get('r_frame_rate') + if frameRate_string: + frameRate = int(frameRate_string.split('/')[0]) + + output['frameRate'] = frameRate + + if 'startFrame' not in data or 'endFrame' not in data: + startFrame = endFrame = 1 + endFrame_string = probe_data.get('nb_frames') + + if endFrame_string: + endFrame = int(endFrame_string) + + output['startFrame'] = startFrame + output['endFrame'] = endFrame + + file_info = None + if 'file_info' in data: + file_info = data['file_info'] + elif ext in ['.mov']: + file_info = probe_data.get('codec_name') + + output['file_info'] = file_info - find_value = 'codec_name' - for line in datalines: - if line.startswith(find_value): - output = line.replace(find_value + '=', '') - break - except Exception as e: - pass return output def _process_data(self, data): ext = data['ext'] + # load file data info + file_data = self.get_file_data(data) + for key, value in file_data.items(): + data[key] = value + icon = 'default' for ico, exts in self.presets['extensions'].items(): if ext in exts: diff --git a/pype/standalonepublish/widgets/widget_family.py b/pype/standalonepublish/widgets/widget_family.py index 7259ecdb64..63776b1df3 100644 --- a/pype/standalonepublish/widgets/widget_family.py +++ b/pype/standalonepublish/widgets/widget_family.py @@ -17,12 +17,14 @@ class FamilyWidget(QtWidgets.QWidget): data = dict() _jobs = dict() Separator = "---separator---" + NOT_SELECTED = '< Nothing is selected >' def __init__(self, parent): super().__init__(parent) # Store internal states in here self.state = {"valid": False} self.parent_widget = parent + self.asset_name = self.NOT_SELECTED body = QtWidgets.QWidget() lists = QtWidgets.QWidget() @@ -30,12 +32,10 @@ class FamilyWidget(QtWidgets.QWidget): container = QtWidgets.QWidget() list_families = QtWidgets.QListWidget() - input_asset = QtWidgets.QLineEdit() - input_asset.setEnabled(False) - input_asset.setStyleSheet("color: #BBBBBB;") + input_subset = QtWidgets.QLineEdit() input_result = QtWidgets.QLineEdit() - input_result.setStyleSheet("color: gray;") + input_result.setStyleSheet("color: #BBBBBB;") input_result.setEnabled(False) # region Menu for default subset names @@ -51,6 +51,20 @@ class FamilyWidget(QtWidgets.QWidget): name_layout.addWidget(btn_subset) name_layout.setContentsMargins(0, 0, 0, 0) + # version + version_spinbox = QtWidgets.QSpinBox() + version_spinbox.setMinimum(1) + version_spinbox.setMaximum(9999) + version_spinbox.setEnabled(False) + version_spinbox.setStyleSheet("color: #BBBBBB;") + + version_checkbox = QtWidgets.QCheckBox("Next Available Version") + version_checkbox.setCheckState(QtCore.Qt.CheckState(2)) + + version_layout = QtWidgets.QHBoxLayout() + version_layout.addWidget(version_spinbox) + version_layout.addWidget(version_checkbox) + layout = QtWidgets.QVBoxLayout(container) header = FamilyDescriptionWidget(self) @@ -58,11 +72,11 @@ class FamilyWidget(QtWidgets.QWidget): layout.addWidget(QtWidgets.QLabel("Family")) layout.addWidget(list_families) - layout.addWidget(QtWidgets.QLabel("Asset")) - layout.addWidget(input_asset) layout.addWidget(QtWidgets.QLabel("Subset")) layout.addLayout(name_layout) layout.addWidget(input_result) + layout.addWidget(QtWidgets.QLabel("Version")) + layout.addLayout(version_layout) layout.setContentsMargins(0, 0, 0, 0) options = QtWidgets.QWidget() @@ -75,6 +89,7 @@ class FamilyWidget(QtWidgets.QWidget): layout.setContentsMargins(0, 0, 0, 0) layout = QtWidgets.QVBoxLayout(body) + layout.addWidget(lists) layout.addWidget(options, 0, QtCore.Qt.AlignLeft) layout.setContentsMargins(0, 0, 0, 0) @@ -83,9 +98,9 @@ class FamilyWidget(QtWidgets.QWidget): layout.addWidget(body) input_subset.textChanged.connect(self.on_data_changed) - input_asset.textChanged.connect(self.on_data_changed) list_families.currentItemChanged.connect(self.on_selection_changed) list_families.currentItemChanged.connect(header.set_item) + version_checkbox.stateChanged.connect(self.on_version_refresh) self.stateChanged.connect(self._on_state_changed) @@ -93,8 +108,9 @@ class FamilyWidget(QtWidgets.QWidget): self.menu_subset = menu_subset self.btn_subset = btn_subset self.list_families = list_families - self.input_asset = input_asset self.input_result = input_result + self.version_checkbox = version_checkbox + self.version_spinbox = version_spinbox self.refresh() @@ -103,7 +119,8 @@ class FamilyWidget(QtWidgets.QWidget): family = plugin.family.rsplit(".", 1)[-1] data = { 'family': family, - 'subset': self.input_subset.text() + 'subset': self.input_result.text(), + 'version': self.version_spinbox.value() } return data @@ -112,7 +129,10 @@ class FamilyWidget(QtWidgets.QWidget): return self.parent_widget.db def change_asset(self, name): - self.input_asset.setText(name) + if name is None: + name = self.NOT_SELECTED + self.asset_name = name + self.on_data_changed() def _on_state_changed(self, state): self.state['valid'] = state @@ -153,22 +173,37 @@ class FamilyWidget(QtWidgets.QWidget): self.input_subset.setText(action.text()) def _on_data_changed(self): - item = self.list_families.currentItem() + asset_name = self.asset_name subset_name = self.input_subset.text() - asset_name = self.input_asset.text() + item = self.list_families.currentItem() - # Get the assets from the database which match with the name - assets_db = self.db.find(filter={"type": "asset"}, projection={"name": 1}) - assets = [asset for asset in assets_db if asset_name in asset["name"]] if item is None: return - if assets: - # Get plugin and family - plugin = item.data(PluginRole) - if plugin is None: - return - family = plugin.family.rsplit(".", 1)[-1] + assets = None + if asset_name != self.NOT_SELECTED: + # Get the assets from the database which match with the name + assets_db = self.db.find( + filter={"type": "asset"}, + projection={"name": 1} + ) + assets = [ + asset for asset in assets_db if asset_name in asset["name"] + ] + + # Get plugin and family + plugin = item.data(PluginRole) + if plugin is None: + return + + family = plugin.family.rsplit(".", 1)[-1] + + # Update the result + if subset_name: + subset_name = subset_name[0].upper() + subset_name[1:] + self.input_result.setText("{}{}".format(family, subset_name)) + + if assets: # Get all subsets of the current asset asset_ids = [asset["_id"] for asset in assets] subsets = self.db.find(filter={"type": "subset", @@ -191,28 +226,62 @@ class FamilyWidget(QtWidgets.QWidget): self._build_menu(defaults) - # Update the result - if subset_name: - subset_name = subset_name[0].upper() + subset_name[1:] - self.input_result.setText("{}{}".format(family, subset_name)) - item.setData(ExistsRole, True) - self.echo("Ready ..") else: self._build_menu([]) item.setData(ExistsRole, False) - if asset_name != self.parent_widget.NOT_SELECTED: - self.echo("'%s' not found .." % asset_name) + if asset_name != self.NOT_SELECTED: + # TODO add logging into standalone_publish + print("'%s' not found .." % asset_name) + + self.on_version_refresh() # Update the valid state valid = ( + asset_name != self.NOT_SELECTED and subset_name.strip() != "" and - asset_name.strip() != "" and item.data(QtCore.Qt.ItemIsEnabled) and item.data(ExistsRole) ) self.stateChanged.emit(valid) + def on_version_refresh(self): + auto_version = self.version_checkbox.isChecked() + self.version_spinbox.setEnabled(not auto_version) + if not auto_version: + return + + asset_name = self.asset_name + subset_name = self.input_result.text() + version = 1 + + if ( + asset_name != self.NOT_SELECTED and + subset_name.strip() != '' + ): + asset = self.db.find_one({ + 'type': 'asset', + 'name': asset_name + }) + subset = self.db.find_one({ + 'type': 'subset', + 'parent': asset['_id'], + 'name': subset_name + }) + if subset: + versions = self.db.find({ + 'type': 'version', + 'parent': subset['_id'] + }) + if versions: + versions = sorted( + [v for v in versions], + key=lambda ver: ver['name'] + ) + version = int(versions[-1]['name']) + 1 + + self.version_spinbox.setValue(version) + def on_data_changed(self, *args): # Set invalid state until it's reconfirmed to be valid by the @@ -270,10 +339,6 @@ class FamilyWidget(QtWidgets.QWidget): self.list_families.setCurrentItem(self.list_families.item(0)) - def echo(self, message): - if hasattr(self.parent_widget, 'echo'): - self.parent_widget.echo(message) - def schedule(self, func, time, channel="default"): try: self._jobs[channel].stop() diff --git a/setup/nuke/nuke_path/menu.py b/setup/nuke/nuke_path/menu.py index 9a96a52850..4982513b78 100644 --- a/setup/nuke/nuke_path/menu.py +++ b/setup/nuke/nuke_path/menu.py @@ -1,5 +1,10 @@ -from pype.nuke.lib import writes_version_sync, onScriptLoad +from pype.nuke.lib import ( + writes_version_sync, + onScriptLoad, + checkInventoryVersions +) + import nuke from pypeapp import Logger @@ -8,5 +13,6 @@ log = Logger().get_logger(__name__, "nuke") nuke.addOnScriptSave(writes_version_sync) nuke.addOnScriptSave(onScriptLoad) +nuke.addOnScriptSave(checkInventoryVersions) log.info('Automatic syncing of write file knob to script version')