From 6a5c8c7e9a50fa95732e9b2fc889904da432a246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Wed, 9 Mar 2016 14:09:06 +0100 Subject: [PATCH] Added downloading of texture thumbnails The downloading is multi-threaded, and streams directly to disk in 10kB chunks. --- blender_cloud/__init__.py | 19 +++++++--- blender_cloud/pillar.py | 73 +++++++++++++++++++++++++++++++++------ 2 files changed, 78 insertions(+), 14 deletions(-) diff --git a/blender_cloud/__init__.py b/blender_cloud/__init__.py index 5298316..8cdbc4a 100644 --- a/blender_cloud/__init__.py +++ b/blender_cloud/__init__.py @@ -71,7 +71,8 @@ class BlenderCloudPreferences(AddonPreferences): else: blender_id_icon = 'WORLD_DATA' blender_id_text = "You are logged in as %s." % blender_id_profile['username'] - blender_id_help = "To logout or change profile, go to the Blender ID add-on preferences." + blender_id_help = "To logout or change profile, " \ + "go to the Blender ID add-on preferences." sub = layout.column() sub.label(text=blender_id_text, icon=blender_id_icon) @@ -89,10 +90,20 @@ class PillarCredentialsUpdate(Operator): bl_idname = "pillar.credentials_update" bl_label = "Update credentials" + @classmethod + def poll(cls, context): + # Only allow activation when the user is actually logged in. + return cls.is_logged_in(context) + + @classmethod + def is_logged_in(cls, context): + active_user_id = getattr(context.window_manager, 'blender_id_active_profile', None) + return bool(active_user_id) + def execute(self, context): - active_user_id = getattr(bpy.context.window_manager, 'blender_id_active_profile', None) - if not active_user_id: - self.report({'ERROR'}, "No profile active found") + # Only allow activation when the user is actually logged in. + if not self.is_logged_in(context): + self.report({'ERROR'}, "No active profile found") return {'CANCELLED'} # Test the new URL diff --git a/blender_cloud/pillar.py b/blender_cloud/pillar.py index 2644384..2926d84 100644 --- a/blender_cloud/pillar.py +++ b/blender_cloud/pillar.py @@ -1,5 +1,6 @@ import sys import os +import concurrent.futures # Add our shipped Pillar SDK wheel to the Python path if not any('pillar_sdk' in path for path in sys.path): @@ -12,7 +13,6 @@ if not any('pillar_sdk' in path for path in sys.path): import pillarsdk import pillarsdk.exceptions -import bpy _pillar_api = None # will become a pillarsdk.Api object. @@ -27,6 +27,8 @@ class UserNotLoggedInError(RuntimeError): def blender_id_profile() -> dict: """Returns the Blender ID profile of the currently logged in user.""" + import bpy + active_user_id = getattr(bpy.context.window_manager, 'blender_id_active_profile', None) if not active_user_id: return None @@ -42,6 +44,7 @@ def pillar_api() -> pillarsdk.Api: """ global _pillar_api + import bpy # Only return the Pillar API object if the user is still logged in. profile = blender_id_profile() @@ -74,22 +77,72 @@ def get_project_uuid(project_url: str) -> str: return project['_id'] -def get_nodes(project_uuid: str, parent_node_uuid: str = '') -> list: - if not parent_node_uuid: - parent_spec = {'$exists': False} - else: - parent_spec = parent_node_uuid +def get_nodes(project_uuid: str = None, parent_node_uuid: str = None) -> list: + """Gets nodes for either a project or given a parent node. + + @param project_uuid: the UUID of the project, or None if only querying by parent_node_uuid. + @param parent_node_uuid: the UUID of the parent node. Can be the empty string if the + node should be a top-level node in the project. Can also be None to query all nodes in a + project. In both these cases the project UUID should be given. + """ + + if not project_uuid and not parent_node_uuid: + raise ValueError('get_nodes(): either project_uuid or parent_node_uuid must be given.') + + where = {'properties.status': 'published'} + + # Build the parent node where-clause + if parent_node_uuid == '': + where['parent'] = {'$exists': False} + elif parent_node_uuid is not None: + where['parent'] = parent_node_uuid + + # Build the project where-clause + if project_uuid: + where['project'] = project_uuid children = pillarsdk.Node.all({ 'projection': {'name': 1, 'parent': 1, 'node_type': 1, 'properties.order': 1, 'properties.status': 1, - 'properties.content_type': 1, + 'properties.content_type': 1, 'picture': 1, 'permissions': 1, 'project': 1, # for permission checking }, - 'where': {'project': project_uuid, - 'parent': parent_spec, - 'properties.status': 'published'}, + 'where': where, 'sort': 'properties.order'}, api=pillar_api()) return children['_items'] + + +def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str): + """Fetches all texture thumbnails in a certain parent node. + + @param parent_node_uuid: the UUID of the parent node. All sub-nodes will be downloaded. + @param desired_size: size indicator, from 'sbtmlh'. + """ + + def fetch_thumbnail_from_node(texture_node: pillarsdk.Node, api: pillarsdk.Api): + # Fetch the File description JSON + pic_uuid = texture_node['picture'] + file_desc = pillarsdk.File.find(pic_uuid, { + 'projection': {'filename': 1, 'variations': 1, 'width': 1, 'height': 1}, + }, api=api) + + # Save the thumbnail + thumb_path = file_desc.stream_thumb_to_file('/tmp', desired_size, api=api) + + return texture_node['name'], thumb_path + + api = pillar_api() + + with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: + # Queue up fetching of thumbnails + futures = [executor.submit(fetch_thumbnail_from_node, node, api) + for node in get_nodes(parent_node_uuid=parent_node_uuid) + if node['node_type'] == 'texture'] + + for future in futures: + node, file = future.result() + print('Node {} has picture {}'.format(node, file)) + + print('Done downloading texture thumbnails')