Skip to content

Commit

Permalink
Merge pull request #11 from ynput/enhancement/AY-5780_Resolve-enhanci…
Browse files Browse the repository at this point in the history
…ng-editorial-pkg-loader

Resolve: enhancing editorial pkg loader
  • Loading branch information
jakubjezek001 authored Sep 18, 2024
2 parents b3dcd46 + c860167 commit 147b3bb
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 9 deletions.
44 changes: 40 additions & 4 deletions client/ayon_resolve/api/lib.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import sys
import json
import re
import os
import json
import uuid
import contextlib
from typing import List, Dict, Any
from opentimelineio import opentime
Expand Down Expand Up @@ -40,6 +41,36 @@
self.pype_timeline_name = "OpenPypeTimeline"


def get_timeline_media_pool_item(timeline, root=None) -> object:
"""Return MediaPoolItem from Timeline
Args:
timeline (resolve.Timeline): timeline object
root (resolve.Folder): root folder / bin object
Returns:
resolve.MediaPoolItem: media pool item from timeline
"""

# Due to limitations in the Resolve API we can't get
# the media pool item directly from the timeline.
# We can find it by name, however names are not
# enforced to be unique across bins. So, we give it
# unique name.
original_name = timeline.GetName()
identifier = str(uuid.uuid4().hex)
try:
timeline.SetName(identifier)
for item in iter_all_media_pool_clips(root=root):
if item.GetName() != identifier:
continue
return item
finally:
# Revert to original name
timeline.SetName(original_name)


@contextlib.contextmanager
def maintain_current_timeline(to_timeline: object,
from_timeline: object = None):
Expand Down Expand Up @@ -961,9 +992,14 @@ def get_reformated_path(path, padded=False, first=False):
return path


def iter_all_media_pool_clips():
"""Recursively iterate all media pool clips in current project"""
root = get_current_project().GetMediaPool().GetRootFolder()
def iter_all_media_pool_clips(root=None):
"""Recursively iterate all media pool clips in current project
Args:
root (Optional[resolve.Folder]): root folder / bin object.
When None, defaults to media pool root folder.
"""
root = root or get_current_project().GetMediaPool().GetRootFolder()
queue = [root]
for folder in queue:
for clip in folder.GetClipList():
Expand Down
7 changes: 7 additions & 0 deletions client/ayon_resolve/api/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ def ls():
continue
data = json.loads(data)

# treat data as container
# There might be cases where clip's metadata are having additional
# because it needs to store 'load' and 'publish' data. In that case
# we need to get only 'load' data
if data.get("load"):
data = data["load"]

# If not all required data, skip it
required = ['schema', 'id', 'loader', 'representation']
if not all(key in data for key in required):
Expand Down
30 changes: 30 additions & 0 deletions client/ayon_resolve/api/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import qargparse
from qtpy import QtWidgets, QtCore

from ayon_core.pipeline.constants import AVALON_INSTANCE_ID
from ayon_core.settings import get_current_project_settings
from ayon_core.pipeline import (
LegacyCreator,
Expand Down Expand Up @@ -903,6 +904,35 @@ def _create_parents(self):
self.parents.append(parent)


def get_editorial_publish_data(
folder_path,
product_name,
version=None
) -> dict:
"""Get editorial publish data from context.
Args:
folder_path (str): Folder path where editorial package is located.
product_name (str): Editorial product name.
version (Optional[str]): Editorial product version. Defaults to None.
Returns:
dict: Editorial publish data.
"""
data = {
"id": AVALON_INSTANCE_ID,
"productType": "editorial_pkg",
"productName": product_name,
"folderPath": folder_path,
"active": True,
}

if version:
data["version"] = version

return data


def get_representation_files(representation):
anatomy = Anatomy()
files = []
Expand Down
31 changes: 31 additions & 0 deletions client/ayon_resolve/plugins/create/create_editorial_package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import json
from ayon_core.pipeline.create.legacy_create import LegacyCreator

from ayon_resolve.api import lib


class CreateEditorialPackage(LegacyCreator):
"""Create Editorial Package."""

name = "editorial_pkg"
label = "Editorial Package"
product_type = "editorial_pkg"
icon = "camera"
defaults = ["Main"]

def process(self):
"""Process the creation of the editorial package."""
current_timeline = lib.get_current_timeline()

if not current_timeline:
raise RuntimeError("Make sure to have an active current timeline.")

timeline_media_pool_item = lib.get_timeline_media_pool_item(
current_timeline
)

publish_data = {"publish": self.data}

timeline_media_pool_item.SetMetadata(
lib.pype_tag_name, json.dumps(publish_data)
)
117 changes: 112 additions & 5 deletions client/ayon_resolve/plugins/load/load_editorial_package.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import json
from pathlib import Path
import random

from ayon_core.pipeline import (
AVALON_CONTAINER_ID,
load,
get_representation_path,
)

from ayon_resolve.api import lib
from ayon_resolve.api.plugin import get_editorial_publish_data


class LoadEditorialPackage(load.LoaderPlugin):
Expand All @@ -32,21 +36,124 @@ def load(self, context, name, namespace, data):

project = lib.get_current_project()
media_pool = project.GetMediaPool()
folder_path = context["folder"]["path"]

# create versioned bin for editorial package
version_name = context["version"]["name"]
bin_name = f"{name}_{version_name}"
lib.create_bin(bin_name)
loaded_bin = lib.create_bin(f"{folder_path}/{name}/{version_name}")

# make timeline unique name based on folder path
folder_path_name = folder_path.replace("/", "_").lstrip("_")
loaded_timeline_name = (
f"{folder_path_name}_{name}_{version_name}_timeline")
import_options = {
"timelineName": "Editorial Package Timeline",
"timelineName": loaded_timeline_name,
"importSourceClips": True,
"sourceClipsPath": search_folder_path.as_posix(),
}

# import timeline from otio file
timeline = media_pool.ImportTimelineFromFile(files, import_options)

# get timeline media pool item for metadata update
timeline_media_pool_item = lib.get_timeline_media_pool_item(
timeline, loaded_bin
)

# Update the metadata
clip_data = self._get_container_data(
context, data)

timeline_media_pool_item.SetMetadata(
lib.pype_tag_name, json.dumps(clip_data)
)

# set clip color based on random choice
clip_color = self.get_random_clip_color()
timeline_media_pool_item.SetClipColor(clip_color)

# TODO: there are two ways to import timeline resources (representation
# and resources folder) but Resolve seems to ignore any of this
# since it is importing sources automatically. But we might need
# to at least set some metadata to those loaded media pool items
print("Timeline imported: ", timeline)

def update(self, container, context):
# TODO: implement update method in future
pass
timeline_mp_clip = container["_item"]
timeline_mp_clip.SetMetadata(lib.pype_tag_name, "")

self.load(
context,
context["product"]["name"],
container["namespace"],
container
)

def _get_container_data(
self,
context: dict,
data: dict
) -> dict:
"""Return metadata related to the representation and version."""

# add additional metadata from the version to imprint AYON knob
version_entity = context["version"]

for key in ("_item", "name"):
data.pop(key, None) # remove unnecessary key from the data if it exists

data = {
"load": data,
}

# add version attributes to the load data
data["load"].update(
version_entity["attrib"]
)

# add variables related to version context
data["load"].update(
{
"schema": "ayon:container-3.0",
"id": AVALON_CONTAINER_ID,
"loader": str(self.__class__.__name__),
"author": version_entity["data"]["author"],
"representation": context["representation"]["id"],
"version": version_entity["version"],
}
)

# add publish data for streamline publishing
data["publish"] = get_editorial_publish_data(
folder_path=context["folder"]["path"],
product_name=context["product"]["name"],
version=version_entity["version"],
)

return data

def get_random_clip_color(self):
"""Return clip color."""

# list of all available davinci resolve clip colors
colors = [
"Orange",
"Apricot"
"Yellow",
"Lime",
"Olive",
"Green",
"Teal",
"Navy",
"Blue",
"Purple",
"Violet",
"Pink",
"Tan",
"Beige",
"Brown",
"Chocolate",
]

# return one of the colors based on random position
return random.choice(colors)

0 comments on commit 147b3bb

Please sign in to comment.