2016-03-11 17:52:12 +01:00
|
|
|
import asyncio
|
2016-03-08 16:22:20 +01:00
|
|
|
import sys
|
|
|
|
import os
|
2016-03-09 17:34:37 +01:00
|
|
|
import functools
|
2016-03-15 13:47:21 +01:00
|
|
|
import logging
|
2016-03-08 16:22:20 +01:00
|
|
|
|
|
|
|
# Add our shipped Pillar SDK wheel to the Python path
|
|
|
|
if not any('pillar_sdk' in path for path in sys.path):
|
|
|
|
import glob
|
2016-03-08 17:56:32 +01:00
|
|
|
|
2016-03-08 16:22:20 +01:00
|
|
|
# TODO: gracefully handle errors when the wheel cannot be found.
|
|
|
|
my_dir = os.path.dirname(__file__)
|
|
|
|
pillar_wheel = glob.glob(os.path.join(my_dir, 'pillar_sdk*.whl'))[0]
|
|
|
|
sys.path.append(pillar_wheel)
|
|
|
|
|
|
|
|
import pillarsdk
|
2016-03-08 17:56:32 +01:00
|
|
|
import pillarsdk.exceptions
|
2016-03-11 17:52:12 +01:00
|
|
|
import pillarsdk.utils
|
2016-03-08 17:56:32 +01:00
|
|
|
|
|
|
|
_pillar_api = None # will become a pillarsdk.Api object.
|
2016-03-15 13:47:21 +01:00
|
|
|
log = logging.getLogger(__name__)
|
2016-03-08 17:56:32 +01:00
|
|
|
|
|
|
|
|
|
|
|
class UserNotLoggedInError(RuntimeError):
|
|
|
|
"""Raised when the user should be logged in on Blender ID, but isn't.
|
|
|
|
|
|
|
|
This is basically for every interaction with Pillar.
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def blender_id_profile() -> dict:
|
|
|
|
"""Returns the Blender ID profile of the currently logged in user."""
|
|
|
|
|
2016-03-09 14:09:06 +01:00
|
|
|
import bpy
|
|
|
|
|
2016-03-08 17:56:32 +01:00
|
|
|
active_user_id = getattr(bpy.context.window_manager, 'blender_id_active_profile', None)
|
|
|
|
if not active_user_id:
|
|
|
|
return None
|
|
|
|
|
|
|
|
import blender_id.profiles
|
|
|
|
return blender_id.profiles.get_active_profile()
|
|
|
|
|
|
|
|
|
|
|
|
def pillar_api() -> pillarsdk.Api:
|
|
|
|
"""Returns the Pillar SDK API object for the current user.
|
|
|
|
|
|
|
|
The user must be logged in.
|
|
|
|
"""
|
|
|
|
|
|
|
|
global _pillar_api
|
2016-03-09 14:09:06 +01:00
|
|
|
import bpy
|
2016-03-08 17:56:32 +01:00
|
|
|
|
|
|
|
# Only return the Pillar API object if the user is still logged in.
|
|
|
|
profile = blender_id_profile()
|
|
|
|
if not profile:
|
|
|
|
raise UserNotLoggedInError()
|
|
|
|
|
|
|
|
if _pillar_api is None:
|
|
|
|
endpoint = bpy.context.user_preferences.addons['blender_cloud'].preferences.pillar_server
|
|
|
|
_pillar_api = pillarsdk.Api(endpoint=endpoint,
|
|
|
|
username=profile['username'],
|
|
|
|
password=None,
|
|
|
|
token=profile['token'])
|
|
|
|
|
|
|
|
return _pillar_api
|
|
|
|
|
|
|
|
|
2016-03-11 17:52:12 +01:00
|
|
|
async def get_project_uuid(project_url: str) -> str:
|
2016-03-08 17:56:32 +01:00
|
|
|
"""Returns the UUID for the project, given its '/p/<project_url>' string."""
|
|
|
|
|
2016-03-11 17:52:12 +01:00
|
|
|
find_one = functools.partial(pillarsdk.Project.find_one, {
|
|
|
|
'where': {'url': project_url},
|
|
|
|
'projection': {'permissions': 1},
|
|
|
|
}, api=pillar_api())
|
|
|
|
|
|
|
|
loop = asyncio.get_event_loop()
|
2016-03-08 17:56:32 +01:00
|
|
|
try:
|
2016-03-11 17:52:12 +01:00
|
|
|
project = await loop.run_in_executor(None, find_one)
|
2016-03-08 17:56:32 +01:00
|
|
|
except pillarsdk.exceptions.ResourceNotFound:
|
2016-03-15 13:47:21 +01:00
|
|
|
log.error('Project with URL %r does not exist', project_url)
|
2016-03-08 17:56:32 +01:00
|
|
|
return None
|
|
|
|
|
2016-03-15 13:47:21 +01:00
|
|
|
log.info('Found project %r', project)
|
2016-03-08 17:56:32 +01:00
|
|
|
return project['_id']
|
|
|
|
|
|
|
|
|
2016-03-11 17:52:12 +01:00
|
|
|
async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None) -> list:
|
2016-03-09 14:09:06 +01:00
|
|
|
"""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
|
2016-03-08 17:56:32 +01:00
|
|
|
|
2016-03-11 17:52:12 +01:00
|
|
|
node_all = functools.partial(pillarsdk.Node.all, {
|
2016-03-08 17:56:32 +01:00
|
|
|
'projection': {'name': 1, 'parent': 1, 'node_type': 1,
|
|
|
|
'properties.order': 1, 'properties.status': 1,
|
2016-03-11 17:52:12 +01:00
|
|
|
'properties.content_type': 1, 'picture': 1},
|
2016-03-09 14:09:06 +01:00
|
|
|
'where': where,
|
2016-03-11 17:52:12 +01:00
|
|
|
'sort': 'properties.order'}, api=pillar_api())
|
|
|
|
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
children = await loop.run_in_executor(None, node_all)
|
2016-03-08 16:22:20 +01:00
|
|
|
|
2016-03-08 17:56:32 +01:00
|
|
|
return children['_items']
|
2016-03-09 14:09:06 +01:00
|
|
|
|
|
|
|
|
2016-03-11 17:52:12 +01:00
|
|
|
async def download_to_file(url, filename, chunk_size=10 * 1024):
|
|
|
|
"""Downloads a file via HTTP(S) directly to the filesystem."""
|
|
|
|
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
await loop.run_in_executor(None, pillarsdk.utils.download_to_file, url, filename, chunk_size)
|
|
|
|
|
|
|
|
|
|
|
|
async def stream_thumb_to_file(file: pillarsdk.File, directory: str, desired_size: str):
|
|
|
|
"""Streams a thumbnail to a file.
|
|
|
|
|
|
|
|
@param file: the pillar File object that represents the image whose thumbnail to download.
|
|
|
|
@param directory: the directory to save the file to.
|
|
|
|
@param desired_size: thumbnail size
|
|
|
|
@return: the absolute path of the downloaded file.
|
|
|
|
"""
|
|
|
|
|
|
|
|
api = pillar_api()
|
|
|
|
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
thumb_link = await loop.run_in_executor(None, functools.partial(
|
|
|
|
file.thumbnail_file, desired_size, api=api))
|
|
|
|
|
|
|
|
if thumb_link is None:
|
|
|
|
raise ValueError("File {} has no thumbnail of size {}"
|
|
|
|
.format(file['_id'], desired_size))
|
|
|
|
|
|
|
|
root, ext = os.path.splitext(file['file_path'])
|
|
|
|
thumb_fname = "{0}-{1}.jpg".format(root, desired_size)
|
|
|
|
thumb_path = os.path.abspath(os.path.join(directory, thumb_fname))
|
|
|
|
|
|
|
|
await download_to_file(thumb_link, thumb_path)
|
|
|
|
|
|
|
|
return thumb_path
|
|
|
|
|
|
|
|
|
|
|
|
async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str,
|
|
|
|
thumbnail_directory: str,
|
|
|
|
*,
|
|
|
|
thumbnail_loading: callable,
|
|
|
|
thumbnail_loaded: callable):
|
2016-03-09 17:34:37 +01:00
|
|
|
"""Generator, fetches all texture thumbnails in a certain parent node.
|
2016-03-09 14:09:06 +01:00
|
|
|
|
|
|
|
@param parent_node_uuid: the UUID of the parent node. All sub-nodes will be downloaded.
|
|
|
|
@param desired_size: size indicator, from 'sbtmlh'.
|
2016-03-09 17:34:37 +01:00
|
|
|
@param thumbnail_directory: directory in which to store the downloaded thumbnails.
|
2016-03-14 17:23:56 +01:00
|
|
|
@param thumbnail_loading: callback function that takes (node_id, pillarsdk.File object)
|
|
|
|
parameters, which is called before a thumbnail will be downloaded. This allows you to
|
2016-03-11 17:52:12 +01:00
|
|
|
show a "downloading" indicator.
|
2016-03-14 17:23:56 +01:00
|
|
|
@param thumbnail_loaded: callback function that takes (node_id, pillarsdk.File object,
|
|
|
|
thumbnail path) parameters, which is called for every thumbnail after it's been downloaded.
|
2016-03-09 14:09:06 +01:00
|
|
|
"""
|
|
|
|
|
2016-03-09 17:34:37 +01:00
|
|
|
api = pillar_api()
|
2016-03-11 17:52:12 +01:00
|
|
|
loop = asyncio.get_event_loop()
|
2016-03-09 17:34:37 +01:00
|
|
|
|
2016-03-11 17:52:12 +01:00
|
|
|
file_find = functools.partial(pillarsdk.File.find, params={
|
|
|
|
'projection': {'filename': 1, 'variations': 1, 'width': 1, 'height': 1},
|
|
|
|
}, api=api)
|
2016-03-09 14:09:06 +01:00
|
|
|
|
2016-03-14 09:33:43 +01:00
|
|
|
async def handle_texture_node(texture_node):
|
2016-03-11 17:52:12 +01:00
|
|
|
# Skip non-texture nodes, as we can't thumbnail them anyway.
|
|
|
|
if texture_node['node_type'] != 'texture':
|
2016-03-14 09:33:43 +01:00
|
|
|
return
|
2016-03-09 17:34:37 +01:00
|
|
|
|
2016-03-11 17:52:12 +01:00
|
|
|
# Find the File that belongs to this texture node
|
|
|
|
pic_uuid = texture_node['picture']
|
2016-03-14 17:23:56 +01:00
|
|
|
loop.call_soon_threadsafe(functools.partial(thumbnail_loading,
|
|
|
|
texture_node['_id'],
|
2016-03-14 18:12:29 +01:00
|
|
|
texture_node))
|
|
|
|
file_desc = await loop.run_in_executor(None, file_find, pic_uuid)
|
2016-03-09 14:09:06 +01:00
|
|
|
|
2016-03-11 17:52:12 +01:00
|
|
|
if file_desc is None:
|
|
|
|
print('Unable to find file for texture node {}'.format(pic_uuid))
|
|
|
|
thumb_path = None
|
|
|
|
else:
|
|
|
|
# Save the thumbnail
|
|
|
|
thumb_path = await stream_thumb_to_file(file_desc, thumbnail_directory, desired_size)
|
|
|
|
# print('Texture node {} has file {}'.format(texture_node['_id'], thumb_path))
|
2016-03-09 14:09:06 +01:00
|
|
|
|
2016-03-14 17:23:56 +01:00
|
|
|
loop.call_soon_threadsafe(functools.partial(thumbnail_loaded,
|
|
|
|
texture_node['_id'],
|
|
|
|
file_desc, thumb_path))
|
2016-03-09 14:09:06 +01:00
|
|
|
|
2016-03-14 09:33:43 +01:00
|
|
|
# Download all texture nodes in parallel.
|
|
|
|
texture_nodes = await get_nodes(parent_node_uuid=parent_node_uuid)
|
|
|
|
|
|
|
|
# raises any exception from failed handle_texture_node() calls.
|
|
|
|
await asyncio.gather(*(handle_texture_node(texture_node)
|
|
|
|
for texture_node in texture_nodes))
|
|
|
|
|
2016-03-09 14:09:06 +01:00
|
|
|
print('Done downloading texture thumbnails')
|
2016-03-09 17:34:37 +01:00
|
|
|
|
|
|
|
|
2016-03-11 17:52:12 +01:00
|
|
|
async def parent_node_uuid(node_uuid: str) -> str:
|
2016-03-09 17:34:37 +01:00
|
|
|
"""Returns the UUID of the node's parent node, or an empty string if this is the top level."""
|
|
|
|
|
|
|
|
api = pillar_api()
|
2016-03-11 17:52:12 +01:00
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
|
2016-03-15 13:47:21 +01:00
|
|
|
log.debug('Finding parent node for node %r', node_uuid)
|
2016-03-11 17:52:12 +01:00
|
|
|
find_node = functools.partial(pillarsdk.Node.find, node_uuid,
|
|
|
|
{'projection': {'parent': 1}}, api=api)
|
|
|
|
node = await loop.run_in_executor(None, find_node)
|
2016-03-09 17:34:37 +01:00
|
|
|
if node is None:
|
2016-03-15 13:47:21 +01:00
|
|
|
log.debug('Unable to find node %r, returning empty parent', node_uuid)
|
2016-03-09 17:34:37 +01:00
|
|
|
return ''
|
|
|
|
|
|
|
|
print('Found node {}'.format(node))
|
|
|
|
try:
|
|
|
|
return node['parent']
|
|
|
|
except KeyError:
|
|
|
|
return ''
|