Compare commits
24 Commits
version-1.
...
version-1.
Author | SHA1 | Date | |
---|---|---|---|
5f5f0d8db9 | |||
30f71ac9fc | |||
bdef942b0b | |||
2a0ef39b12 | |||
c57a3bc902 | |||
b94998d12e | |||
1cd42e246e | |||
079689a532 | |||
597ba6de1c | |||
7b59391872 | |||
8201ba7691 | |||
8f2b0f8faa | |||
33b52cc8a9 | |||
be46b9cf81 | |||
ba4c951d32 | |||
5c7343f8c9 | |||
64d36818fe | |||
07f28d3072 | |||
48ca91a364 | |||
7ee052f71b | |||
2bb859efd9 | |||
ac3943fe6c | |||
5eaee872bf | |||
6ce4399407 |
@@ -21,7 +21,7 @@
|
||||
bl_info = {
|
||||
'name': 'Blender Cloud Texture Browser',
|
||||
'author': 'Sybren A. Stüvel and Francesco Siddi',
|
||||
'version': (0, 2, 0),
|
||||
'version': (1, 1, 0),
|
||||
'blender': (2, 77, 0),
|
||||
'location': 'Ctrl+Shift+Alt+A anywhere',
|
||||
'description': 'Allows downloading of textures from the Blender Cloud. Requires '
|
||||
|
@@ -30,14 +30,6 @@ class BlenderCloudPreferences(AddonPreferences):
|
||||
get=lambda self: PILLAR_SERVER_URL
|
||||
)
|
||||
|
||||
# TODO: Move to the Scene properties?
|
||||
project_uuid = bpy.props.StringProperty(
|
||||
name='Project UUID',
|
||||
description='UUID of the current Blender Cloud project',
|
||||
default='5672beecc0261b2005ed1a33',
|
||||
get=lambda self: '5672beecc0261b2005ed1a33'
|
||||
)
|
||||
|
||||
local_texture_dir = StringProperty(
|
||||
name='Default Blender Cloud texture storage directory',
|
||||
subtype='DIR_PATH',
|
||||
@@ -152,16 +144,12 @@ def register():
|
||||
bpy.utils.register_class(BlenderCloudPreferences)
|
||||
bpy.utils.register_class(PillarCredentialsUpdate)
|
||||
|
||||
WindowManager.blender_cloud_project = StringProperty(
|
||||
name="Blender Cloud project UUID",
|
||||
default='5672beecc0261b2005ed1a33') # TODO: don't hard-code this
|
||||
|
||||
WindowManager.blender_cloud_node = StringProperty(
|
||||
name="Blender Cloud node UUID",
|
||||
default='') # empty == top-level of project
|
||||
|
||||
addon_prefs = preferences()
|
||||
|
||||
WindowManager.last_blender_cloud_location = StringProperty(
|
||||
name="Last Blender Cloud browser location",
|
||||
default="/")
|
||||
|
||||
def default_if_empty(scene, context):
|
||||
"""The scene's local_texture_dir, if empty, reverts to the addon prefs."""
|
||||
|
||||
|
@@ -44,13 +44,27 @@ library_path = '/tmp'
|
||||
library_icons_path = os.path.join(os.path.dirname(__file__), "icons")
|
||||
|
||||
|
||||
class UpNode(pillarsdk.Node):
|
||||
class SpecialFolderNode(pillarsdk.Node):
|
||||
pass
|
||||
|
||||
|
||||
class UpNode(SpecialFolderNode):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self['_id'] = 'UP'
|
||||
self['node_type'] = 'UP'
|
||||
|
||||
|
||||
class ProjectNode(SpecialFolderNode):
|
||||
def __init__(self, project):
|
||||
super().__init__()
|
||||
|
||||
assert isinstance(project, pillarsdk.Project), 'wrong type for project: %r' % type(project)
|
||||
|
||||
self.merge(project.to_dict())
|
||||
self['node_type'] = 'PROJECT'
|
||||
|
||||
|
||||
class MenuItem:
|
||||
"""GUI menu item for the 3D View GUI."""
|
||||
|
||||
@@ -66,19 +80,30 @@ class MenuItem:
|
||||
'SPINNER': os.path.join(library_icons_path, 'spinner.png'),
|
||||
}
|
||||
|
||||
SUPPORTED_NODE_TYPES = {'UP', 'group_texture', 'texture'}
|
||||
SUPPORTED_NODE_TYPES = {'UP', 'PROJECT', 'group_texture', 'texture'}
|
||||
|
||||
def __init__(self, node, file_desc, thumb_path: str, label_text):
|
||||
self.log = logging.getLogger('%s.MenuItem' % __name__)
|
||||
if node['node_type'] not in self.SUPPORTED_NODE_TYPES:
|
||||
self.log.info('Invalid node type in node: %s', node)
|
||||
raise TypeError('Node of type %r not supported; supported are %r.' % (
|
||||
node.group_texture, self.SUPPORTED_NODE_TYPES))
|
||||
node['node_type'], self.SUPPORTED_NODE_TYPES))
|
||||
|
||||
assert isinstance(node, pillarsdk.Node), 'wrong type for node: %r' % type(node)
|
||||
assert isinstance(node['_id'], str), 'wrong type for node["_id"]: %r' % type(node['_id'])
|
||||
self.node = node # pillarsdk.Node, contains 'node_type' key to indicate type
|
||||
self.file_desc = file_desc # pillarsdk.File object, or None if a 'folder' node.
|
||||
self.label_text = label_text
|
||||
self._thumb_path = ''
|
||||
self.icon = None
|
||||
self._is_folder = node['node_type'] == 'group_texture' or isinstance(node, UpNode)
|
||||
self._is_folder = (node['node_type'] == 'group_texture' or
|
||||
isinstance(node, SpecialFolderNode))
|
||||
|
||||
# Determine sorting order.
|
||||
# by default, sort all the way at the end and folders first.
|
||||
self._order = 0 if self._is_folder else 10000
|
||||
if node and node.properties and node.properties.order is not None:
|
||||
self._order = node.properties.order
|
||||
|
||||
self.thumb_path = thumb_path
|
||||
|
||||
@@ -88,6 +113,10 @@ class MenuItem:
|
||||
self.width = 0
|
||||
self.height = 0
|
||||
|
||||
def sort_key(self):
|
||||
"""Key for sorting lists of MenuItems."""
|
||||
return self._order, self.label_text
|
||||
|
||||
@property
|
||||
def thumb_path(self) -> str:
|
||||
return self._thumb_path
|
||||
@@ -184,12 +213,10 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
|
||||
_state = 'INITIALIZING'
|
||||
|
||||
project_uuid = '5672beecc0261b2005ed1a33' # Blender Cloud project UUID
|
||||
node = None # The Node object we're currently showing, or None if we're at the project top.
|
||||
node_uuid = '' # Blender Cloud node UUID we're currently showing, i.e. None-safe self.node['_id']
|
||||
current_path = pillar.CloudPath('/')
|
||||
project_name = ''
|
||||
|
||||
# This contains a stack of Node objects that lead up to the currently browsed node.
|
||||
# This allows us to display the "up" item.
|
||||
path_stack = []
|
||||
|
||||
async_task = None # asyncio task for fetching thumbnails
|
||||
@@ -198,7 +225,6 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
log = logging.getLogger('%s.BlenderCloudBrowser' % __name__)
|
||||
|
||||
_menu_item_lock = threading.Lock()
|
||||
current_path = ''
|
||||
current_display_content = []
|
||||
loaded_images = set()
|
||||
thumbnails_cache = ''
|
||||
@@ -215,9 +241,9 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
return {'CANCELLED'}
|
||||
|
||||
wm = context.window_manager
|
||||
self.project_uuid = wm.blender_cloud_project
|
||||
self.node_uuid = wm.blender_cloud_node
|
||||
self.path_stack = []
|
||||
|
||||
self.current_path = pillar.CloudPath(wm.last_blender_cloud_location)
|
||||
self.path_stack = [] # list of nodes that make up the current path.
|
||||
|
||||
self.thumbnails_cache = cache.cache_directory('thumbnails')
|
||||
self.mouse_x = event.mouse_x
|
||||
@@ -237,8 +263,9 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
self.loaded_images = set()
|
||||
self.check_credentials()
|
||||
|
||||
context.window.cursor_modal_set('DEFAULT')
|
||||
context.window_manager.modal_handler_add(self)
|
||||
self.timer = context.window_manager.event_timer_add(1 / 30, context.window)
|
||||
self.timer = context.window_manager.event_timer_add(1 / 15, context.window)
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
@@ -268,24 +295,37 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
self.mouse_x = event.mouse_x
|
||||
self.mouse_y = event.mouse_y
|
||||
|
||||
if self._state == 'BROWSING' and event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
|
||||
left_mouse_release = event.type == 'LEFTMOUSE' and event.value == 'RELEASE'
|
||||
if self._state == 'PLEASE_SUBSCRIBE' and left_mouse_release:
|
||||
self.open_browser_subscribe()
|
||||
self._finish(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
if self._state == 'BROWSING':
|
||||
selected = self.get_clicked()
|
||||
|
||||
if selected is None:
|
||||
# No item clicked, ignore it.
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
if selected.is_folder:
|
||||
self.descend_node(selected.node)
|
||||
if selected:
|
||||
context.window.cursor_set('HAND')
|
||||
else:
|
||||
if selected.file_desc is None:
|
||||
# This can happen when the thumbnail information isn't loaded yet.
|
||||
# Just ignore the click for now.
|
||||
# TODO: think of a way to handle this properly.
|
||||
return {'RUNNING_MODAL'}
|
||||
self.handle_item_selection(context, selected)
|
||||
context.window.cursor_set('DEFAULT')
|
||||
|
||||
elif event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||
if left_mouse_release:
|
||||
if selected is None:
|
||||
# No item clicked, ignore it.
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
if selected.is_folder:
|
||||
self.descend_node(selected.node)
|
||||
else:
|
||||
if selected.file_desc is None:
|
||||
# This can happen when the thumbnail information isn't loaded yet.
|
||||
# Just ignore the click for now.
|
||||
# TODO: think of a way to handle this properly.
|
||||
self.log.debug('Selected item %r has no file_desc', selected)
|
||||
return {'RUNNING_MODAL'}
|
||||
self.handle_item_selection(context, selected)
|
||||
|
||||
if event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||
self._finish(context)
|
||||
return {'CANCELLED'}
|
||||
|
||||
@@ -301,6 +341,10 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
|
||||
try:
|
||||
await pillar.check_pillar_credentials()
|
||||
except pillar.NotSubscribedToCloudError:
|
||||
self.log.info('User not subscribed to Blender Cloud.')
|
||||
self._show_subscribe_screen()
|
||||
return
|
||||
except pillar.CredentialsNotSyncedError:
|
||||
self.log.info('Credentials not synced, re-syncing automatically.')
|
||||
else:
|
||||
@@ -310,8 +354,12 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
|
||||
try:
|
||||
await pillar.refresh_pillar_credentials()
|
||||
except pillar.NotSubscribedToCloudError:
|
||||
self.log.info('User is not a Blender Cloud subscriber.')
|
||||
self._show_subscribe_screen()
|
||||
return
|
||||
except pillar.UserNotLoggedInError:
|
||||
self.error('User not logged in on Blender ID.')
|
||||
self.log.error('User not logged in on Blender ID.')
|
||||
else:
|
||||
self.log.info('Credentials refreshed and ok, browsing assets.')
|
||||
await self.async_download_previews()
|
||||
@@ -320,27 +368,45 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
raise pillar.UserNotLoggedInError()
|
||||
# self._new_async_task(self._check_credentials())
|
||||
|
||||
def _show_subscribe_screen(self):
|
||||
"""Shows the "You need to subscribe" screen."""
|
||||
|
||||
self._state = 'PLEASE_SUBSCRIBE'
|
||||
bpy.context.window.cursor_set('HAND')
|
||||
|
||||
def descend_node(self, node):
|
||||
"""Descends the node hierarchy by visiting this node.
|
||||
|
||||
Also keeps track of the current node, so that we know where the "up" button should go.
|
||||
"""
|
||||
|
||||
# Going up or down?
|
||||
if self.path_stack and isinstance(node, UpNode):
|
||||
self.log.debug('Going up, pop the stack; pre-pop stack is %r', self.path_stack)
|
||||
node = self.path_stack.pop()
|
||||
assert isinstance(node, pillarsdk.Node), 'Wrong type %s' % node
|
||||
|
||||
if isinstance(node, UpNode):
|
||||
# Going up.
|
||||
self.log.debug('Going up to %r', self.current_path)
|
||||
self.current_path = self.current_path.parent
|
||||
if self.path_stack:
|
||||
self.path_stack.pop()
|
||||
if not self.path_stack:
|
||||
self.project_name = ''
|
||||
else:
|
||||
# Going down, keep track of where we were (project top-level is None)
|
||||
self.path_stack.append(self.node)
|
||||
self.log.debug('Going up, push the stack; post-push stack is %r', self.path_stack)
|
||||
# Going down, keep track of where we were
|
||||
if isinstance(node, ProjectNode):
|
||||
self.project_name = node['name']
|
||||
|
||||
self.current_path /= node['_id']
|
||||
self.log.debug('Going down to %r', self.current_path)
|
||||
self.path_stack.append(node)
|
||||
|
||||
# Set 'current' to the given node
|
||||
self.node_uuid = node['_id'] if node else None
|
||||
self.node = node
|
||||
self.browse_assets()
|
||||
|
||||
@property
|
||||
def node(self):
|
||||
if not self.path_stack:
|
||||
return None
|
||||
return self.path_stack[-1]
|
||||
|
||||
def _stop_async_task(self):
|
||||
self.log.debug('Stopping async task')
|
||||
if self.async_task is None:
|
||||
@@ -378,6 +444,7 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
|
||||
context.space_data.draw_handler_remove(self._draw_handle, 'WINDOW')
|
||||
context.window_manager.event_timer_remove(self.timer)
|
||||
context.window.cursor_modal_restore()
|
||||
|
||||
if self.maximized_area:
|
||||
bpy.ops.screen.screen_full_area(use_hide_panels=True)
|
||||
@@ -406,6 +473,8 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
self.current_display_content.append(menu_item)
|
||||
self.loaded_images.add(menu_item.icon.filepath_raw)
|
||||
|
||||
self.sort_menu()
|
||||
|
||||
return menu_item
|
||||
|
||||
def update_menu_item(self, node, *args) -> MenuItem:
|
||||
@@ -421,11 +490,23 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
else:
|
||||
raise ValueError('Unable to find MenuItem(node_uuid=%r)' % node_uuid)
|
||||
|
||||
self.sort_menu()
|
||||
|
||||
def sort_menu(self):
|
||||
"""Sorts the self.current_display_content list."""
|
||||
|
||||
if not self.current_display_content:
|
||||
return
|
||||
|
||||
with self._menu_item_lock:
|
||||
self.current_display_content.sort(key=MenuItem.sort_key)
|
||||
|
||||
async def async_download_previews(self):
|
||||
self._state = 'BROWSING'
|
||||
|
||||
thumbnails_directory = self.thumbnails_cache
|
||||
self.log.info('Asynchronously downloading previews to %r', thumbnails_directory)
|
||||
self.log.info('Current BCloud path is %r', self.current_path)
|
||||
self.clear_images()
|
||||
|
||||
def thumbnail_loading(node, texture_node):
|
||||
@@ -434,50 +515,59 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
def thumbnail_loaded(node, file_desc, thumb_path):
|
||||
self.update_menu_item(node, file_desc, thumb_path, file_desc['filename'])
|
||||
|
||||
# Download either by group_texture node UUID or by project UUID (which
|
||||
# shows all top-level nodes)
|
||||
if self.node_uuid:
|
||||
self.log.debug('Getting subnodes for parent node %r', self.node_uuid)
|
||||
children = await pillar.get_nodes(parent_node_uuid=self.node_uuid,
|
||||
node_type='group_textures')
|
||||
|
||||
# Make sure we can go up again.
|
||||
if self.path_stack:
|
||||
self.add_menu_item(UpNode(), None, 'FOLDER', '.. up ..')
|
||||
elif self.project_uuid:
|
||||
self.log.debug('Getting subnodes for project node %r', self.project_uuid)
|
||||
children = await pillar.get_nodes(self.project_uuid, '')
|
||||
project_uuid = self.current_path.project_uuid
|
||||
node_uuid = self.current_path.node_uuid
|
||||
|
||||
if node_uuid:
|
||||
# Query for sub-nodes of this node.
|
||||
self.log.debug('Getting subnodes for parent node %r', node_uuid)
|
||||
children = await pillar.get_nodes(parent_node_uuid=node_uuid,
|
||||
node_type='group_texture')
|
||||
elif project_uuid:
|
||||
# Query for top-level nodes.
|
||||
self.log.debug('Getting subnodes for project node %r', project_uuid)
|
||||
children = await pillar.get_nodes(project_uuid=project_uuid,
|
||||
parent_node_uuid='',
|
||||
node_type='group_texture')
|
||||
else:
|
||||
# TODO: add "nothing here" icon and trigger re-draw
|
||||
self.log.warning("Not node UUID and no project UUID, I can't do anything!")
|
||||
# Query for projects
|
||||
self.log.debug('No node UUID and no project UUID, listing all projects')
|
||||
children = await pillar.get_texture_projects()
|
||||
for proj_dict in children:
|
||||
self.add_menu_item(ProjectNode(proj_dict), None, 'FOLDER', proj_dict['name'])
|
||||
return
|
||||
|
||||
# Make sure we can go up again.
|
||||
self.add_menu_item(UpNode(), None, 'FOLDER', '.. up ..')
|
||||
|
||||
# Download all child nodes
|
||||
self.log.debug('Iterating over child nodes of %r', self.node_uuid)
|
||||
self.log.debug('Iterating over child nodes of %r', self.current_path)
|
||||
for child in children:
|
||||
# print(' - %(_id)s = %(name)s' % child)
|
||||
if child['node_type'] not in MenuItem.SUPPORTED_NODE_TYPES:
|
||||
self.log.debug('Skipping node of type %r', child['node_type'])
|
||||
continue
|
||||
self.add_menu_item(child, None, 'FOLDER', child['name'])
|
||||
|
||||
# There are only sub-nodes at the project level, no texture nodes,
|
||||
# so we won't have to bother looking for textures.
|
||||
if not self.node_uuid:
|
||||
if not node_uuid:
|
||||
return
|
||||
|
||||
directory = os.path.join(thumbnails_directory, self.project_uuid, self.node_uuid)
|
||||
directory = os.path.join(thumbnails_directory, project_uuid, node_uuid)
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
self.log.debug('Fetching texture thumbnails for node %r', self.node_uuid)
|
||||
await pillar.fetch_texture_thumbs(self.node_uuid, 's', directory,
|
||||
self.log.debug('Fetching texture thumbnails for node %r', node_uuid)
|
||||
await pillar.fetch_texture_thumbs(node_uuid, 's', directory,
|
||||
thumbnail_loading=thumbnail_loading,
|
||||
thumbnail_loaded=thumbnail_loaded,
|
||||
future=self.signalling_future)
|
||||
|
||||
def browse_assets(self):
|
||||
self.log.debug('Browsing assets at project %r node %r', self.project_uuid, self.node_uuid)
|
||||
self.log.debug('Browsing assets at %r', self.current_path)
|
||||
self._new_async_task(self.async_download_previews())
|
||||
|
||||
def _new_async_task(self, async_task: asyncio.coroutine, future: asyncio.Future=None):
|
||||
def _new_async_task(self, async_task: asyncio.coroutine, future: asyncio.Future = None):
|
||||
"""Stops the currently running async task, and starts another one."""
|
||||
|
||||
self.log.debug('Setting up a new task %r, so any existing task must be stopped', async_task)
|
||||
@@ -499,6 +589,7 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
'BROWSING': self._draw_browser,
|
||||
'DOWNLOADING_TEXTURE': self._draw_downloading,
|
||||
'EXCEPTION': self._draw_exception,
|
||||
'PLEASE_SUBSCRIBE': self._draw_subscribe,
|
||||
}
|
||||
|
||||
if self._state in drawers:
|
||||
@@ -510,7 +601,7 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
|
||||
blf.size(font_id, 20, 72)
|
||||
blf.position(font_id, 5, 5, 0)
|
||||
blf.draw(font_id, self._state)
|
||||
blf.draw(font_id, '%s %s' % (self._state, self.project_name))
|
||||
bgl.glDisable(bgl.GL_BLEND)
|
||||
|
||||
@staticmethod
|
||||
@@ -641,6 +732,11 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
blf.draw(font_id, line)
|
||||
bgl.glDisable(bgl.GL_BLEND)
|
||||
|
||||
def _draw_subscribe(self, context):
|
||||
self._draw_text_on_colour(context,
|
||||
'Click to subscribe to the Blender Cloud',
|
||||
(0.0, 0.0, 0.2, 0.6))
|
||||
|
||||
def get_clicked(self) -> MenuItem:
|
||||
|
||||
for item in self.current_display_content:
|
||||
@@ -652,11 +748,13 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
def handle_item_selection(self, context, item: MenuItem):
|
||||
"""Called when the user clicks on a menu item that doesn't represent a folder."""
|
||||
|
||||
from pillarsdk.utils import sanitize_filename
|
||||
|
||||
self.clear_images()
|
||||
self._state = 'DOWNLOADING_TEXTURE'
|
||||
|
||||
node_path_components = [node['name'] for node in self.path_stack if node is not None]
|
||||
local_path_components = [self.project_uuid] + node_path_components + [self.node['name']]
|
||||
node_path_components = (node['name'] for node in self.path_stack if node is not None)
|
||||
local_path_components = [sanitize_filename(comp) for comp in node_path_components]
|
||||
|
||||
top_texture_directory = bpy.path.abspath(context.scene.local_texture_dir)
|
||||
local_path = os.path.join(top_texture_directory, *local_path_components)
|
||||
@@ -689,6 +787,13 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
||||
future=signalling_future))
|
||||
self.async_task.add_done_callback(texture_download_completed)
|
||||
|
||||
def open_browser_subscribe(self):
|
||||
import webbrowser
|
||||
|
||||
webbrowser.open_new_tab('https://cloud.blender.org/join')
|
||||
|
||||
self.report({'INFO'}, 'We just started a browser for you.')
|
||||
|
||||
|
||||
# store keymaps here to access after registration
|
||||
addon_keymaps = []
|
||||
|
@@ -31,14 +31,15 @@ class UserNotLoggedInError(RuntimeError):
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return 'UserNotLoggedInError'
|
||||
return self.__class__.__name__
|
||||
|
||||
|
||||
class CredentialsNotSyncedError(UserNotLoggedInError):
|
||||
"""Raised when the user may be logged in on Blender ID, but has no Blender Cloud token."""
|
||||
|
||||
def __str__(self):
|
||||
return 'CredentialsNotSyncedError'
|
||||
|
||||
class NotSubscribedToCloudError(UserNotLoggedInError):
|
||||
"""Raised when the user may be logged in on Blender ID, but has no Blender Cloud token."""
|
||||
|
||||
|
||||
class PillarError(RuntimeError):
|
||||
@@ -62,6 +63,8 @@ class CloudPath(pathlib.PurePosixPath):
|
||||
@property
|
||||
def project_uuid(self) -> str:
|
||||
assert self.parts[0] == '/'
|
||||
if len(self.parts) <= 1:
|
||||
return None
|
||||
return self.parts[1]
|
||||
|
||||
@property
|
||||
@@ -71,11 +74,10 @@ class CloudPath(pathlib.PurePosixPath):
|
||||
|
||||
@property
|
||||
def node_uuid(self) -> str:
|
||||
node_uuids = self.node_uuids
|
||||
|
||||
if not node_uuids:
|
||||
if len(self.parts) <= 2:
|
||||
return None
|
||||
return node_uuids[-1]
|
||||
|
||||
return self.parts[-1]
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -171,11 +173,24 @@ async def check_pillar_credentials():
|
||||
if not subclient:
|
||||
raise CredentialsNotSyncedError()
|
||||
|
||||
try:
|
||||
await get_project_uuid('textures') # Any query will do.
|
||||
except pillarsdk.UnauthorizedAccess:
|
||||
pillar_user_id = subclient['subclient_user_id']
|
||||
if not pillar_user_id:
|
||||
raise CredentialsNotSyncedError()
|
||||
|
||||
try:
|
||||
db_user = await pillar_call(pillarsdk.User.find, pillar_user_id)
|
||||
except (pillarsdk.UnauthorizedAccess, pillarsdk.ResourceNotFound):
|
||||
raise CredentialsNotSyncedError()
|
||||
|
||||
roles = db_user.roles
|
||||
log.debug('User has roles %r', roles)
|
||||
if not roles or not {'subscriber', 'demo'}.intersection(set(roles)):
|
||||
# Delete the subclient info. This forces a re-check later, which can
|
||||
# then pick up on the user's new status.
|
||||
del profile.subclients[SUBCLIENT_ID]
|
||||
profile.save_json()
|
||||
raise NotSubscribedToCloudError()
|
||||
|
||||
|
||||
async def refresh_pillar_credentials():
|
||||
"""Refreshes the authentication token on Pillar.
|
||||
@@ -193,11 +208,15 @@ async def refresh_pillar_credentials():
|
||||
|
||||
# Create a subclient token and send it to Pillar.
|
||||
# May raise a blender_id.BlenderIdCommError
|
||||
blender_id.create_subclient_token(SUBCLIENT_ID, pillar_endpoint)
|
||||
try:
|
||||
blender_id.create_subclient_token(SUBCLIENT_ID, pillar_endpoint)
|
||||
except blender_id.communication.BlenderIdCommError as ex:
|
||||
log.warning("Unable to create authentication token: %s", ex)
|
||||
raise CredentialsNotSyncedError()
|
||||
|
||||
# Test the new URL
|
||||
_pillar_api = None
|
||||
await get_project_uuid('textures') # Any query will do.
|
||||
await check_pillar_credentials()
|
||||
|
||||
|
||||
async def get_project_uuid(project_url: str) -> str:
|
||||
@@ -217,7 +236,7 @@ async def get_project_uuid(project_url: str) -> str:
|
||||
|
||||
|
||||
async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None,
|
||||
node_type: str = None) -> list:
|
||||
node_type = 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.
|
||||
@@ -242,7 +261,10 @@ async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None,
|
||||
where['project'] = project_uuid
|
||||
|
||||
if node_type:
|
||||
where['node_type'] = node_type
|
||||
if isinstance(node_type, str):
|
||||
where['node_type'] = node_type
|
||||
else:
|
||||
where['node_type'] = {'$in': node_type}
|
||||
|
||||
children = await pillar_call(pillarsdk.Node.all, {
|
||||
'projection': {'name': 1, 'parent': 1, 'node_type': 1,
|
||||
@@ -250,12 +272,24 @@ async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None,
|
||||
'properties.files': 1,
|
||||
'properties.content_type': 1, 'picture': 1},
|
||||
'where': where,
|
||||
'sort': 'properties.order',
|
||||
'embed': ['parent']})
|
||||
|
||||
return children['_items']
|
||||
|
||||
|
||||
async def get_texture_projects() -> list:
|
||||
"""Returns project dicts that contain textures."""
|
||||
|
||||
try:
|
||||
children = await pillar_call(pillarsdk.Project.all_from_endpoint,
|
||||
'/bcloud/texture-libraries')
|
||||
except pillarsdk.ResourceNotFound as ex:
|
||||
log.warning('Unable to find texture projects: %s', ex)
|
||||
raise PillarError('Unable to find texture projects: %s' % ex)
|
||||
|
||||
return children['_items']
|
||||
|
||||
|
||||
async def download_to_file(url, filename, *,
|
||||
header_store: str,
|
||||
chunk_size=100 * 1024,
|
||||
@@ -367,7 +401,7 @@ async def fetch_thumbnail_info(file: pillarsdk.File, directory: str, desired_siz
|
||||
finished.
|
||||
"""
|
||||
|
||||
thumb_link = await pillar_call(file.thumbnail_file, desired_size)
|
||||
thumb_link = await pillar_call(file.thumbnail, desired_size)
|
||||
|
||||
if thumb_link is None:
|
||||
raise ValueError("File {} has no thumbnail of size {}"
|
||||
@@ -439,8 +473,22 @@ async def download_texture_thumbnail(texture_node, desired_size: str,
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# Find the File that belongs to this texture node
|
||||
pic_uuid = texture_node['picture']
|
||||
# Find out which file to use for the thumbnail picture.
|
||||
pic_uuid = texture_node.picture
|
||||
if not pic_uuid:
|
||||
# Fall back to the first texture file, if it exists.
|
||||
log.debug('Node %r does not have a picture, falling back to first file.',
|
||||
texture_node['_id'])
|
||||
files = texture_node.properties and texture_node.properties.files
|
||||
if not files:
|
||||
log.info('Node %r does not have a picture nor files, skipping.', texture_node['_id'])
|
||||
return
|
||||
pic_uuid = files[0].file
|
||||
if not pic_uuid:
|
||||
log.info('Node %r does not have a picture nor files, skipping.', texture_node['_id'])
|
||||
return
|
||||
|
||||
# Load the File that belongs to this texture node's picture.
|
||||
loop.call_soon_threadsafe(thumbnail_loading, texture_node, texture_node)
|
||||
file_desc = await pillar_call(pillarsdk.File.find, pic_uuid, params={
|
||||
'projection': {'filename': 1, 'variations': 1, 'width': 1, 'height': 1},
|
||||
@@ -495,8 +543,13 @@ async def download_file_by_uuid(file_uuid,
|
||||
metadata_file = os.path.join(metadata_directory, 'files', '%s.json' % file_uuid)
|
||||
save_as_json(file_desc, metadata_file)
|
||||
|
||||
file_path = os.path.join(target_directory,
|
||||
sanitize_filename('%s-%s' % (map_type, file_desc['filename'])))
|
||||
root, ext = os.path.splitext(file_desc['filename'])
|
||||
if root.endswith(map_type):
|
||||
target_filename = '%s%s' % (root, ext)
|
||||
else:
|
||||
target_filename = '%s-%s%s' % (root, map_type, ext)
|
||||
|
||||
file_path = os.path.join(target_directory, sanitize_filename(target_filename))
|
||||
file_url = file_desc['link']
|
||||
# log.debug('Texture %r:\n%s', file_uuid, pprint.pformat(file_desc.to_dict()))
|
||||
loop.call_soon_threadsafe(file_loading, file_path, file_desc)
|
||||
|
@@ -18,8 +18,9 @@ def load_wheel(module_name, fname_prefix):
|
||||
|
||||
try:
|
||||
module = __import__(module_name)
|
||||
except ImportError:
|
||||
pass
|
||||
except ImportError as ex:
|
||||
log.debug('Unable to import %s directly, will try wheel: %s',
|
||||
module_name, ex)
|
||||
else:
|
||||
log.debug('Was able to load %s from %s, no need to load wheel %s',
|
||||
module_name, module.__file__, fname_prefix)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# Primary requirements:
|
||||
CacheControl==0.11.6
|
||||
lockfile==0.12.2
|
||||
pillarsdk==1.0.0
|
||||
pillarsdk==1.2.0
|
||||
wheel==0.29.0
|
||||
|
||||
# Secondary requirements:
|
||||
|
2
setup.py
2
setup.py
@@ -173,7 +173,7 @@ setup(
|
||||
'wheels': BuildWheels},
|
||||
name='blender_cloud',
|
||||
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
|
||||
version='1.0.0',
|
||||
version='1.1.0',
|
||||
author='Sybren A. Stüvel',
|
||||
author_email='sybren@stuvel.eu',
|
||||
packages=find_packages('.'),
|
||||
|
Reference in New Issue
Block a user