From 8ad356e3324cddef42d41f9b9b588ef1ebd2f8bf Mon Sep 17 00:00:00 2001
From: Campbell Barton <ideasman42@gmail.com>
Date: Wed, 5 Mar 2014 19:14:49 +1100
Subject: [PATCH] Sketchfab integration, D321

Lets you use your sketchfab account from within Blender to upload models online.
---
 io_online_sketchfab/__init__.py        | 466 +++++++++++++++++++++++++
 io_online_sketchfab/pack_for_export.py | 124 +++++++
 2 files changed, 590 insertions(+)
 create mode 100644 io_online_sketchfab/__init__.py
 create mode 100644 io_online_sketchfab/pack_for_export.py

diff --git a/io_online_sketchfab/__init__.py b/io_online_sketchfab/__init__.py
new file mode 100644
index 000000000..3e5d74729
--- /dev/null
+++ b/io_online_sketchfab/__init__.py
@@ -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()
diff --git a/io_online_sketchfab/pack_for_export.py b/io_online_sketchfab/pack_for_export.py
new file mode 100644
index 000000000..a8e7fc9b4
--- /dev/null
+++ b/io_online_sketchfab/pack_for_export.py
@@ -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)
+
-- 
GitLab