SOLVED Get asset from asset browser python

Hi,

I can't seem to figure out how to get a reference to selected items in the asset browser.
Ideally I want to select a few textures in the assetbrowser, press the script button, and then use the filepaths to plug into texture channels.
How do I access the currently selected asset, and if that's not possible using python, how do i go about dragging them into something to acces them?

Thank you!

@kilian

Do you mean this ?

def GetTextureInBrowser(assetId): 

    # assetId: maxon.Id = maxon.Id("file_68991a1ba2bbef15")
    
    repository: maxon.AssetRepositoryRef = maxon.AssetInterface.GetUserPrefsRepository()
    asset: maxon.AssetDescription = repository.FindLatestAsset(
        maxon.AssetTypes.File(), assetId, maxon.Id(), maxon.ASSET_FIND_MODE.LATEST)
    name: str = asset.GetMetaString(maxon.OBJECT.BASE.NAME, maxon.Resource.GetCurrentLanguage(), "")

    url: maxon.Url = asset.GetUrl()

    print(url)

    return url

@dunhou
Thank you so much for your answer!
I might be mistaken, but I don't think this is usable because i don't know the "assetId". The script has to find out which asset is selected, not dragged, just selected.
Or am i missing something?

Hello @kilian,

Thank you for reaching out to us. And thanks again for the community answers. This is a tricky one to answer, at least in the current state of Python Asset API. In general, the answer is easy:

  1. You cannot retrieve the selection state of the Asset Browser as there can be multiple Asset Browser instances open at the same time.
  2. While 1. might not be the strongest reasoning, the way how such things are intended to be done, is with maxon.AssetManagerInterface.OpenPopup. So, you open a little Asset Browser popup, as for example how it is done by the Node Editor or the Load Preset button, let the user select something and then retrieve the selection state in that Asset Browser instance.

But in Python there unfold a few problems:

  1. Accessing the asset descriptions passed to AssetManagerInterface.OpenPopup delegate outSelection in form of an maxon.DragAndDropDataAssetArray instance will crash Cinema 4D. The culprit is DragAndDropDataAssetArray.GetAssetDescriptions(), not the delegate itself. So, while you can open a popup and let the user select things, you cannot get hold of the selection state at the moment.
  2. The same thing applies to asset drag and drop events which are managed by the same data type.

But what you could do, is use a custom GUI which has implicit asset drag and drop support, as for example CUSTOMGUI_FILENAME, to drag and drop assets into them to get the URL of the asset. But there are also quite a few hoops to jump through there. I have provided an example below.

Please note that when evaluated conservatively, what you want to be done is not possible in Python at the moment and you should switch to C++. What I am providing here is very much a workaround.

Cheers,
Ferdinand

edit: @m_adam suggested kindly that one could monkey patch maxon.AssetInterface.GetAssetUrl oneself inside the script, which makes this a lot less dicey. I have adopted the code below which will now operate on the proper asset scheme Urls.

The result:
asset_stuff.gif

The code:

"""Provides a temporary work around for letting a user select a set of texture assets to create a
material.

This can be run as a Script Manager script and will let the user drag two (texture) assets into
CUSTOMGUI_FILENAME gadgets in order to create a material.

The Asset API does not provide access to the Asset Browser in an extended fashion, which is why 
there maxon.AssetManagerInterface.GetSelectedAssets(). The intended way is to use maxon.
AssetManagerInterface.OpenPopup(), which will open a new popup Asset Browser instance. maxon.
DragAndDropDataAssetArray.GetAssetDescriptions(), a method required by .OpenPopup() will however
currently crash Cinema 4D.

This file provides a work around. It is not pretty but it gets the job done. As soon as we fix
the relevant methods, you should adopt them.

References:
    [1] https://developers.maxon.net/docs/Cinema4DCPPSDK/html/page_handbook_assetapi_metadata.html#assetapi_snippet_find_categoryasset_by_path
""" 

import c4d
import maxon
import typing

doc: c4d.documents.BaseDocument  # The active document

# --- AssetInterface monkey patching fix for AssetInterface.GetAssetUrl() --------------------------
@maxon.interface.MAXON_INTERFACE(maxon.consts.MAXON_REFERENCE_COPY_ON_WRITE, "net.maxon.interface.asset")
class AssetInterfaceFix(maxon.AssetBaseWithUpdateInterface):
    @staticmethod
    @maxon.interface.MAXON_STATICMETHOD("net.maxon.interface.asset.GetAssetUrl")
    def GetAssetUrl(self, asset, isLatest):
        pass

maxon.AssetInterface.GetAssetUrl = AssetInterfaceFix.GetAssetUrl
# --- End of fix -----------------------------------------------------------------------------------

class MaterialCreationDialog (c4d.gui.GeDialog):
    """Implements a dialog into which one can drag media assets to create a material.
    """
    # The gadget IDs.
    ID_GROUP: int = 1000
    ID_LBL_COLOR: int = 1001
    ID_FLE_COLOR: int = 1002
    ID_LBL_BUMP: int = 1003
    ID_FLE_BUMP: int = 1004
    ID_BTN_CREATE: int = 1005

    def CreateLayout(self) -> bool:
        """Called to populate the dialog.
        """
        self.SetTitle("Material Creation Dialog")
        self.GroupBegin(self.ID_GROUP, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, cols=2)
        self.GroupBorderSpace(5, 5, 5, 5)

        # The static text and CUSTOMGUI_FILENAME for the color channel asset. CUSTOMGUI_FILENAME is 
        # a custom GUI which can implicitly deal with asset drag and drop events. I.e., you can drag
        # an asset onto it, and it will create a link.
        self.AddStaticText(self.ID_LBL_COLOR, c4d.BFH_FIT, name="Color")
        self.AddCustomGui(self.ID_FLE_COLOR, c4d.CUSTOMGUI_FILENAME, "", c4d.BFH_SCALEFIT, 0, 0, 
                          c4d.BaseContainer())

        # The static text and CUSTOMGUI_FILENAME for the bump channel asset.
        self.AddStaticText(self.ID_LBL_BUMP, c4d.BFH_FIT, name="Bump")
        self.AddCustomGui(self.ID_FLE_BUMP, c4d.CUSTOMGUI_FILENAME, "", c4d.BFH_SCALEFIT, 0, 0, 
                          c4d.BaseContainer())

        self.AddButton(self.ID_BTN_CREATE, c4d.BFH_FIT, name="Create Material")

        self.GroupEnd()
        return super().CreateLayout()

    def Command(self, cid: int, msg: c4d.BaseContainer) -> bool:
        """Called on gadget interaction events.
        """
        # The create button has been clicked, CreateMaterial() is being called with the strings
        # shown in the filename GUIs.
        if cid == self.ID_BTN_CREATE:
            MaterialCreationDialog.CreateMaterial(
                urlColor=self.GetString(self.ID_FLE_COLOR),
                urlBump=self.GetString(self.ID_FLE_BUMP))

        return super().Command(cid, msg)

    @staticmethod
    def CreateMaterial(urlColor: str, urlBump: str) -> bool:
        """Creates a material for the assets referenced in the two filename custom GUI gadgets.
        """
        # Bail when any of the asset URL strings is empty.
        if not isinstance(urlColor, str) or urlColor == "":
            return False
        if not isinstance(urlBump, str) or urlBump == "":
            return False

        # Get the true asset Url, or at least one which is working. What does this mean? There are 
        # in principal three Urls associated with an asset:
        #
        #  1. The human readable asset Url in the assetdb scheme, e.g.:
        #       assetdb:///tex/Surfaces/Dirt Scratches & Smudges/RustPaint0291_M.jpg
        #     The catch here is that this is not a Url in the more strict sense, it is just smoke 
        #     and mirrors and the AssetApi has no clue what to do with it. In C++ there are
        #     methods attached to UrlInterface with which one can convert such faux Url to the true 
        #     Url, but they are missing in Python. When one would write such Url to a bitmap shader,
        #     Cinema 4D would not recognize it (without some major message trickery). In the user 
        #     interface one can use assetdb Urls to reference assets, in the API not.
        #  2. The true asset Url in the asset scheme, e.g.,
        #       asset:///file_edb3eb584c0d905c~.jpg
        #     This is the "true" asset Url one should use for linking a texture asset in a material.
        #     In the Ui, Cinema 4D will show it as (1.). It can be retrieved with 
        #     maxon.AssetInterface.GetAssetUrl(asset, True), but the
        #     method is broken.
        #  3. The raw asset url, usually in the ramdisk scheme, e.g.,:
        #       ramdisk://A9C0F37BAE5146F2/file_3b194acc5a745a2c/1/asset.jpg
        #     This is the true physical file of the asset. Usually one should not touch it. Write 
        #     operations to it are off limits.
        urlColor = MaterialCreationDialog.GetTrueAssetUrl(urlColor)
        urlBump = MaterialCreationDialog.GetTrueAssetUrl(urlBump)
        if None in (urlColor, urlBump):
            return False

        # At this point we have replaced the assetdb scheme asset Urls with asset scheme Urls.
        # This is now standard material creation.

        # Get the active document, create a material, and two bitmap shaders, one for the color, and
        # one for the bump channel.
        doc: c4d.documents.BaseDocument = c4d.documents.GetActiveDocument()     

        material: c4d.BaseMaterial = c4d.BaseMaterial(c4d.Mmaterial)
        colorShader: c4d.BaseShader = c4d.BaseShader(c4d.Xbitmap)
        bumpShader: c4d.BaseShader = c4d.BaseShader(c4d.Xbitmap)
        if None in (material, colorShader, bumpShader):
            raise MemoryError("Could not allocate material or shader.")

        # Link the two assets in the filename properties of the bitmap shaders.
        colorShader[c4d.BITMAPSHADER_FILENAME] = urlColor
        bumpShader[c4d.BITMAPSHADER_FILENAME] = urlBump

        # Insert the shaders into the material and link them in the color and bump channel, and 
        # enable the bump channel.
        material.InsertShader(colorShader)
        material.InsertShader(bumpShader)
        material[c4d.MATERIAL_COLOR_SHADER] = colorShader
        material[c4d.MATERIAL_BUMP_SHADER] = bumpShader
        material[c4d.MATERIAL_USE_BUMP] = True

        # Insert the material into the document, set it as active and push an update event.
        doc.InsertMaterial(material)
        doc.SetActiveMaterial(material)
        c4d.EventAdd()

        return True

    @staticmethod
    def GetTrueAssetUrl(assetDbUrl: str) -> typing.Optional[str]:
        """Returns the true asset Url in the asset scheme for an asset Url in the assetdb scheme.

        This is very much a hack as lined out above.
        """
        # Bail on illegal inputs.
        if not isinstance(assetDbUrl, str) or not assetDbUrl.startswith("assetdb:///"):
            return None

        # Get the user preferences repository as a lookup repo.
        repo: maxon.AssetRepositoryRef  = maxon.AssetInterface.GetUserPrefsRepository()
        if repo is None:
            return

        # Massage #assetDbUrl into form: Cut of the scheme, split by / and pop off the last element 
        # as the file name. For assetdb:///tex/Surfaces/Dirt Scratches & Smudges/RustPaint0291_M.jpg,
        # the values would be:
        #
        #   categories = ["tex", "Surfaces", "Dirt Scratches & Smudges"]
        #   filename = "RustPaint0291_M.jpg"
        #
        categories: list[str] = assetDbUrl[11:].split("/")
        filename: str = categories.pop()
        if filename is None or filename == "":
            return

        # Find the Id of all asset categories which match the name of the last category in the 
        # assetdb Url. E.g. for assetdb:///tex/Surfaces/Dirt Scratches & Smudges/RustPaint0291_M.jpg
        # it would search for all categories named "Dirt Scratches & Smudges".
        categoryIds: list[maxon.Id] = []
        if len(categories) > 0:
            lastCategory: str = categories.pop()
            categoryIds = MaterialCreationDialog.FindCategoryByName(repo, lastCategory)

        # Get all asset descriptions for assets of type file, which includes texture assets.
        fileAssets: list[maxon.AssetDescription] = []
        repo.FindAssets(maxon.AssetTypes.File(), maxon.Id(), maxon.Id(),
            maxon.ASSET_FIND_MODE.LATEST, fileAssets)

        # Iterate over them and get the metadata of each of them while doing so.
        for asset in fileAssets:
            metadata: maxon.AssetMetaData = asset.GetMetaData()
            if metadata is None:
                continue
            
            # Retrieve the subtype of the asset, when it is not SubType_ENUM_MediaImage, i.e., a 
            # texture file asset, we skip the asset.
            subType: maxon.Id = metadata.Get(maxon.ASSETMETADATA.SubType, None)
            if (subType is None or 
                maxon.InternedId(subType) != maxon.ASSETMETADATA.SubType_ENUM_MediaImage):
                continue
            
            # Get the name of the asset, e.g., "RustPaint0291_M.jpg". When it does not align with 
            # #filename, skip the asset.
            name: str = asset.GetMetaString(
                maxon.OBJECT.BASE.NAME, maxon.Resource.GetCurrentLanguage(), "")
            if name != filename:
                continue
            
            # This asset aligns in file name with the file name we extracted form the assetdb Url. 
            # Now we test if also the asset Id of the category asset of this asset aligns with what 
            # could be found in the assetdb Url. I am testing here only the first category, which can
            # lead to false positives. I the C++ docs [1] I have shown how to recursively find asset 
            # categories by a path, but I did not want to make this example even longer.
            categoryId: maxon.Id = maxon.CategoryAssetInterface.GetParentCategory(asset)
            if len(categoryIds) == 0 or categoryId in categoryIds:
                # Get the true asset Url, this only works with the monkey patch provided at the
                # top of the file.
                trueAssetUrl: maxon.Url = maxon.AssetInterface.GetAssetUrl(asset, True)
                # Return the string wrapped by the maxon.Url, e.g. 
                #   "asset:///file_edb3eb584c0d905c~.jpg"
                return trueAssetUrl.GetUrl()

    @staticmethod
    def FindCategoryByName(repo: maxon.AssetRepositoryRef, name: str) -> list[maxon.Id]:
        """Finds all asset categories with the given #name in the given repository #repo and returns
        their Ids.
        """
        # Get all asset category assets from the repository.
        categoryAssets: list[maxon.AssetDescription] = []
        repo.FindAssets(maxon.AssetTypes.Category(), maxon.Id(), maxon.Id(),
            maxon.ASSET_FIND_MODE.LATEST, categoryAssets)

        # And find all categories whose name matches #name, and return them.
        result: list[maxon.Id]= []
        for asset in categoryAssets:
            thisName: str = asset.GetMetaString(
                maxon.OBJECT.BASE.NAME, maxon.Resource.GetCurrentLanguage(), "")
            if thisName == name:
                result.append(asset.GetId())

        return result

# Hack to keep an async dialog alive in a Script Manger environment, please do not do this in a 
# production environment.
dlg: MaterialCreationDialog = MaterialCreationDialog()

def main() -> None:
    """Runs the example.
    """
    dlg.Open(c4d.DLG_TYPE_ASYNC, defaultw=500, defaulth=200)

if __name__ == '__main__':
    main()

@ferdinand
Thanks a lot Ferdinand for this detailed and complete answer! This is exactly what I hoped to do! I tested the code and it works great!

Since a few of the fixes you use are not recommended, i will solve it in another way for now, until it gets fixed for python, or i switch to C++. I got my plugin doing what i want, for the moment.

Pretty sure this will also help someone else in the future.

Have a nice day!