Skip to content
Snippets Groups Projects
Commit 8ad356e3 authored by Campbell Barton's avatar Campbell Barton
Browse files

Sketchfab integration, D321

Lets you use your sketchfab account from within Blender to upload models online.
parent 20a4a567
No related branches found
No related tags found
No related merge requests found
# ##### 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()
# ##### 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)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment