Sketchfab integration, D321

Lets you use your sketchfab account from within Blender to upload models online.
This commit is contained in:
Campbell Barton 2014-03-05 19:14:49 +11:00
parent 20a4a567f9
commit 8ad356e332
2 changed files with 590 additions and 0 deletions

View File

@ -0,0 +1,466 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
bl_info = {
"name": "Sketchfab Exporter",
"author": "Bart Crouch",
"version": (1, 2, 2),
"blender": (2, 7, 0),
"location": "Tools > Upload tab",
"description": "Upload your model to Sketchfab",
"warning": "",
"wiki_url": "",
"tracker_url": "",
"category": "Import-Export"
}
if "bpy" in locals():
pass
else:
# uuid module causes an error messagebox on windows
# - https://developer.blender.org/T38364
# - https://developer.blender.org/T27666
# using a dirty workaround to preload uuid without ctypes, until blender gets compiled with vs2012
import platform
if platform.system() == "Windows":
import ctypes
CDLL = ctypes.CDLL
ctypes.CDLL = None
import uuid
ctypes.CDLL = CDLL
del ctypes, CDLL
import bpy
import os
import threading
import subprocess
from bpy.app.handlers import persistent
from bpy.props import (StringProperty,
EnumProperty,
BoolProperty,
PointerProperty,
)
SKETCHFAB_API_URL = "https://api.sketchfab.com"
SKETCHFAB_API_MODELS_URL = SKETCHFAB_API_URL + "/v1/models"
SKETCHFAB_API_TOKEN_URL = SKETCHFAB_API_URL + "/v1/users/claim-token"
SKETCHFAB_MODEL_URL = "https://sketchfab.com/show/"
SKETCHFAB_EXPORT_FILENAME = "sketchfab-export.blend"
_presets = os.path.join(bpy.utils.user_resource('SCRIPTS'), "presets")
SKETCHFAB_PRESET_FILENAME = os.path.join(_presets, "sketchfab.txt")
SKETCHFAB_EXPORT_DATA_FILE = os.path.join(_presets, "sketchfab-export-data.json")
del _presets
# Singleton for storing global state
class _SketchfabState:
__slots__ = (
"uploading",
"token_reload",
"size_label",
"model_url",
# store report args
"report_message",
"report_type",
)
def __init__(self):
self.uploading = False
self.token_reload = True
self.size_label = ""
self.model_url = ""
self.report_message = ""
self.report_type = ''
sf_state = _SketchfabState()
del _SketchfabState
# if True, no contact is made with the webserver
DEBUG_MODE = False
# change a bytes int into a properly formatted string
def format_size(size):
size /= 1024
size_suffix = "kB"
if size > 1024:
size /= 1024
size_suffix = "mB"
if size >= 100:
size = "%d" % int(size)
else:
size = "%.1f" % size
size += " " + size_suffix
return size
# attempt to load token from presets
@persistent
def load_token(dummy=False):
filepath = SKETCHFAB_PRESET_FILENAME
if not os.path.exists(filepath):
return
token = ""
try:
with open(filepath, 'r', encoding='utf-8') as f:
token = f.readline()
except:
import traceback
traceback.print_exc()
wm = bpy.context.window_manager
wm.sketchfab.token = token
# save token to file
def update_token(self, context):
token = context.window_manager.sketchfab.token
filepath = SKETCHFAB_PRESET_FILENAME
path = os.path.dirname(filepath)
if not os.path.exists(path):
os.makedirs(path)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(token)
def upload_report(report_message, report_type):
sf_state.report_message = report_message
sf_state.report_type = report_type
# upload the blend-file to sketchfab
def upload(filepath, filename):
import requests
wm = bpy.context.window_manager
props = wm.sketchfab
title = props.title
if not title:
title = os.path.splitext(os.path.basename(bpy.data.filepath))[0]
data = {
"title": title,
"description": props.description,
"filename": filename,
"tags": props.tags,
"private": props.private,
"token": props.token,
"source": "blender-exporter",
}
if props.private and props.password != "":
data["password"] = props.password
files = {
"fileModel": open(filepath, 'rb'),
}
try:
r = requests.post(SKETCHFAB_API_MODELS_URL, data=data, files=files, verify=False)
except requests.exceptions.RequestException as e:
return upload_report("Upload failed. Error: %s" % str(e), 'WARNING')
result = r.json()
if r.status_code != requests.codes.ok:
return upload_report("Upload failed. Error: %s" % result["error"], 'WARNING')
sf_state.model_url = SKETCHFAB_MODEL_URL + result["result"]["id"]
return upload_report("Upload complete. Available on your sketchfab.com dashboard.", 'INFO')
# operator to export model to sketchfab
class ExportSketchfab(bpy.types.Operator):
"""Upload your model to Sketchfab"""
bl_idname = "export.sketchfab"
bl_label = "Upload"
_timer = None
_thread = None
def modal(self, context, event):
if event.type == 'TIMER':
if not self._thread.is_alive():
wm = context.window_manager
props = wm.sketchfab
terminate(props.filepath)
if context.area:
context.area.tag_redraw()
# forward message from upload thread
if not sf_state.report_type:
sf_state.report_type = 'ERROR'
self.report({sf_state.report_type}, sf_state.report_message)
wm.event_timer_remove(self._timer)
self._thread.join()
sf_state.uploading = False
return {'FINISHED'}
return {'PASS_THROUGH'}
def execute(self, context):
import json
if sf_state.uploading:
self.report({'WARNING'}, "Please wait till current upload is finished")
return {'CANCELLED'}
wm = context.window_manager
sf_state.model_url = ""
props = wm.sketchfab
if not props.token:
self.report({'ERROR'}, "Token is missing")
return {'CANCELLED'}
# Prepare to save the file
binary_path = bpy.app.binary_path
script_path = os.path.dirname(os.path.realpath(__file__))
basename, ext = os.path.splitext(bpy.data.filepath)
if not basename:
basename = os.path.join(basename, "temp")
if not ext:
ext = ".blend"
filepath = basename + "-export-sketchfab" + ext
try:
# save a copy of actual scene but don't interfere with the users models
bpy.ops.wm.save_as_mainfile(filepath=filepath, compress=True, copy=True)
with open(SKETCHFAB_EXPORT_DATA_FILE, 'w') as s:
json.dump({
"models": props.models,
"lamps": props.lamps,
}, s)
subprocess.check_call([
binary_path,
"--background",
"-noaudio",
filepath,
"--python", os.path.join(script_path, "pack_for_export.py"),
])
os.remove(filepath)
# read subprocess call results
with open(SKETCHFAB_EXPORT_DATA_FILE, 'r') as s:
r = json.load(s)
size = r["size"]
props.filepath = r["filepath"]
filename = r["filename"]
except Exception as e:
self.report({'WARNING'}, "Error occured while preparing your file: %s" % str(e))
return {'FINISHED'}
sf_state.uploading = True
sf_state.size_label = format_size(size)
self._thread = threading.Thread(
target=upload,
args=(props.filepath, filename),
)
self._thread.start()
wm.modal_handler_add(self)
self._timer = wm.event_timer_add(1.0, context.window)
return {'RUNNING_MODAL'}
def cancel(self, context):
wm = context.window_manager
wm.event_timer_remove(self._timer)
self._thread.join()
# user interface
class VIEW3D_PT_sketchfab(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
bl_category = "Upload"
bl_context = "objectmode"
bl_label = "Sketchfab"
def draw(self, context):
wm = context.window_manager
props = wm.sketchfab
if sf_state.token_reload:
sf_state.token_reload = False
if not props.token:
load_token()
layout = self.layout
layout.label("Export:")
col = layout.box().column(align=True)
col.prop(props, "models")
col.prop(props, "lamps")
layout.label("Model info:")
col = layout.box().column(align=True)
col.prop(props, "title")
col.prop(props, "description")
col.prop(props, "tags")
col.prop(props, "private")
if props.private:
col.prop(props, "password")
layout.label("Sketchfab account:")
col = layout.box().column(align=True)
col.prop(props, "token")
row = col.row()
row.operator("wm.sketchfab_email_token", text="Claim Your Token")
row.alignment = 'RIGHT'
if sf_state.uploading:
layout.operator("export.sketchfab", text="Uploading %s" % sf_state.size_label)
else:
layout.operator("export.sketchfab")
model_url = sf_state.model_url
if model_url:
layout.operator("wm.url_open", text="View Online Model", icon='URL').url = model_url
# property group containing all properties for the user interface
class SketchfabProps(bpy.types.PropertyGroup):
description = StringProperty(
name="Description",
description="Description of the model (optional)",
default="")
filepath = StringProperty(
name="Filepath",
description="internal use",
default="",
)
lamps = EnumProperty(
name="Lamps",
items=(('ALL', "All", "Export all lamps in the file"),
('NONE', "None", "Don't export any lamps"),
('SELECTION', "Selection", "Only export selected lamps")),
description="Determines which lamps are exported",
default='ALL',
)
models = EnumProperty(
name="Models",
items=(('ALL', "All", "Export all meshes in the file"),
('SELECTION', "Selection", "Only export selected meshes")),
description="Determines which meshes are exported",
default='SELECTION',
)
private = BoolProperty(
name="Private",
description="Upload as private (requires a pro account)",
default=False,
)
password = StringProperty(
name="Password",
description="Password-protect your model (requires a pro account)",
default="",
)
tags = StringProperty(
name="Tags",
description="List of tags, separated by spaces (optional)",
default="",
)
title = StringProperty(
name="Title",
description="Title of the model (determined automatically if left empty)",
default="",
)
token = StringProperty(
name="Api Key",
description="You can find this on your dashboard at the Sketchfab website",
default="",
update=update_token,
)
class SketchfabEmailToken(bpy.types.Operator):
bl_idname = "wm.sketchfab_email_token"
bl_label = "Enter your email to get a sketchfab token"
email = StringProperty(
name="Email",
default="you@example.com",
)
def execute(self, context):
import re
import requests
EMAIL_RE = re.compile(r'[^@]+@[^@]+\.[^@]+')
if not EMAIL_RE.match(self.email):
self.report({'ERROR'}, "Wrong email format")
try:
r = requests.get(SKETCHFAB_API_TOKEN_URL + "?source=blender-exporter&email=" + self.email, verify=False)
except requests.exceptions.RequestException as e:
self.report({'ERROR'}, str(e))
return {'FINISHED'}
if r.status_code != requests.codes.ok:
self.report({'ERROR'}, "An error occured. Check the format of your email")
else:
self.report({'INFO'}, "Your email was sent at your email address")
return {'FINISHED'}
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self, width=550)
# remove file copy
def terminate(filepath):
os.remove(filepath)
# registration
classes = (
ExportSketchfab,
SketchfabProps,
SketchfabEmailToken,
VIEW3D_PT_sketchfab,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.WindowManager.sketchfab = PointerProperty(
type=SketchfabProps)
load_token()
bpy.app.handlers.load_post.append(load_token)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
del bpy.types.WindowManager.sketchfab
if __name__ == "__main__":
register()

View File

@ -0,0 +1,124 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# This script is called from the sketchfab addon directly
# to pack and save the file from a blender instance
# so that the users file is left untouched.
import os
import bpy
import json
SKETCHFAB_EXPORT_DATA_FILENAME = 'sketchfab-export-data.json'
SKETCHFAB_EXPORT_DATA_FILE = os.path.join(
bpy.utils.user_resource('SCRIPTS'),
"presets",
SKETCHFAB_EXPORT_DATA_FILENAME,
)
# save a copy of the current blendfile
def save_blend_copy():
import time
filepath = os.path.dirname(bpy.data.filepath)
filename = time.strftime("Sketchfab_%Y_%m_%d_%H_%M_%S.blend",
time.localtime(time.time()))
filepath = os.path.join(filepath, filename)
bpy.ops.wm.save_as_mainfile(filepath=filepath,
compress=True,
copy=True)
size = os.path.getsize(filepath)
return (filepath, filename, size)
# change visibility statuses and pack images
def prepare_assets(export_settings):
hidden = set()
images = set()
if (export_settings['models'] == 'SELECTION' or
export_settings['lamps'] != 'ALL'):
for ob in bpy.data.objects:
if ob.type == 'MESH':
for mat_slot in ob.material_slots:
if not mat_slot.material:
continue
for tex_slot in mat_slot.material.texture_slots:
if not tex_slot:
continue
tex = tex_slot.texture
if tex.type == 'IMAGE':
image = tex.image
if image is not None:
images.add(image)
if ((export_settings['models'] == 'SELECTION' and ob.type == 'MESH') or
(export_settings['lamps'] == 'SELECTION' and ob.type == 'LAMP')):
if not ob.select and not ob.hide:
ob.hide = True
hidden.add(ob)
elif export_settings['lamps'] == 'NONE' and ob.type == 'LAMP':
if not ob.hide:
ob.hide = True
hidden.add(ob)
for img in images:
if not img.packed_file:
try:
img.pack()
except:
# can fail in rare cases
import traceback
traceback.print_exc()
def prepare_file(export_settings):
prepare_assets(export_settings)
return save_blend_copy()
def read_settings():
with open(SKETCHFAB_EXPORT_DATA_FILE, 'r') as s:
return json.load(s)
def write_result(filepath, filename, size):
with open(SKETCHFAB_EXPORT_DATA_FILE, 'w') as s:
json.dump({
'filepath': filepath,
'filename': filename,
'size': size,
}, s)
if __name__ == "__main__":
try:
export_settings = read_settings()
filepath, filename, size = prepare_file(export_settings)
write_result(filepath, filename, size)
except:
import traceback
traceback.print_exc()
import sys
sys.exit(1)