Changeset View
Standalone View
io_online_sketchfab/__init__.py
- This file was added.
| bl_info = { | |||||
campbellbarton: should have typical shortened GPL header we have at the top of all bundled addons. | |||||
pierreant-pUnsubmitted Not Done Inline ActionsSure. Can you send what you want to be included and I'll add it. pierreant-p: Sure. Can you send what you want to be included and I'll add it. | |||||
campbellbartonAuthorUnsubmitted Not Done Inline Actionsfirst 17 lines from io_export_after_effects.py is fine campbellbarton: first 17 lines from `io_export_after_effects.py` is fine | |||||
| "name": "Sketchfab Exporter", | |||||
| "author": "Bart Crouch", | |||||
| "version": (1, 2, 0), | |||||
| "blender": (2, 6, 3), | |||||
| "location": "View3D > Properties panel", | |||||
| "description": "Upload your model to Sketchfab", | |||||
| "warning": "", | |||||
| "wiki_url": "", | |||||
| "tracker_url": "", | |||||
| "category": "Import-Export" | |||||
| } | |||||
| if "bpy" in locals(): | |||||
| import imp | |||||
| imp.reload(requests) | |||||
| else: | |||||
| from .packages import requests | |||||
campbellbartonAuthorUnsubmitted Not Done Inline ActionsThis is a really large python library (which I didn't include in the patch), we need to consider how to bundle large 3rd party python modules. I can see why its done for the purpose of getting the addon working & reviewed but having each addon include its own large dependancies isn't so good. At the very least this could be moved to: Then at least this is a common location any addon can import from. campbellbarton: This is a really large python library (which I didn't include in the patch), we need to… | |||||
pierreant-pUnsubmitted Not Done Inline ActionsThis library is a slight fork from this: I think having it in a module every add-on can import is a great idea, as it is very generic and many other people could benefit from using it. One thing I would like to point out: I had to fork the main library trunk, because the library has one "import uuid" which cause a c++ runtime warning on c++. Do you have plans for fixing this in 2.7? Would be great/easier to maintain if we don't deviate from the library's master. Let me know how you want to proceed with this. pierreant-p: This library is a slight fork from this:
http://requests.readthedocs.org/en/latest/
I think… | |||||
campbellbartonAuthorUnsubmitted Not Done Inline ActionsFrom what I understand the issue with importing uuid - with windows configuration, See: So looks like theres a workaround in Python we could apply, but Im not really sure how good/bad this is, or why Py devs are not considering this higher priority to resolve. campbellbarton: From what I understand the issue with importing uuid - with windows configuration,
See… | |||||
| import bpy | |||||
| import os | |||||
| import threading | |||||
| import time | |||||
| import re | |||||
| from bpy.app.handlers import persistent | |||||
| DEBUG_MODE = False # if True, no contact is made with the webserver | |||||
| 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/' | |||||
| # change a bytes int into a properly formatted string | |||||
| def format_size(size): | |||||
campbellbartonAuthorUnsubmitted Not Done Inline ActionsThis generic function could be added to: ./release/scripts/modules/bpy/utils.py campbellbarton: This generic function could be added to: `./release/scripts/modules/bpy/utils.py` | |||||
pierreant-pUnsubmitted Not Done Inline ActionsSure let me know if you do and I'll update the script. pierreant-p: Sure let me know if you do and I'll update the script. | |||||
| size /= 1024 | |||||
| size_suffix = "kB" | |||||
| if size > 1024: | |||||
| size /= 1024 | |||||
| size_suffix = "mB" | |||||
| if size >= 100: | |||||
Not Done Inline ActionsThis is adding a custom importer which executes on every import for every script that blender does once the addon loads, I wouldn't accept this in an official blender addon. All you need to do is load filepost and overwrite its choose_boundary function. eg: def choose_boundary():
from uuid import uuid4
return uuid4().hex
requests.packages.urllib3.filepost.choose_boundary = choose_boundarycampbellbarton: This is adding a custom importer which executes on **every import for every script** that… | |||||
| size = str(int(size)) | |||||
| else: | |||||
| size = "%.1f"%size | |||||
| size += " " + size_suffix | |||||
| return size | |||||
| # attempt to load token from presets | |||||
| @persistent | |||||
| def load_token(dummy=False): | |||||
| filepath = os.path.join(bpy.utils.user_resource('SCRIPTS'), "presets", | |||||
| "sketchfab.txt") | |||||
campbellbartonAuthorUnsubmitted Not Done Inline Actionsbetter have the name global uppercase, along with SKETCHFAB_API_URL since its referenced more then once. campbellbarton: better have the name global uppercase, along with `SKETCHFAB_API_URL` since its referenced more… | |||||
| try: | |||||
| file = open(filepath, 'r') | |||||
campbellbartonAuthorUnsubmitted Not Done Inline Actionsuse with open(...) as file: format here, same goes for all other uses of open. this is best practice for py nowadays campbellbarton: use `with open(...) as file:` format here, same goes for all other uses of open. //this is best… | |||||
| except: | |||||
| return | |||||
campbellbartonAuthorUnsubmitted Not Done Inline ActionsSuggest to handle this a bit differently.... something like... if not file exists:
return
try:
load the file
except:
import traceback
traceback.print_exc()This way if the file exists but can't be read, it wont fail silently - making it hard to figure out whats going on. campbellbarton: Suggest to handle this a bit differently.... something like...
if not file exists… | |||||
| try: | |||||
| token = file.readline() | |||||
| except: | |||||
| token = "" | |||||
| file.close() | |||||
| bpy.context.window_manager.sketchfab.token = token | |||||
| # change visibility statuses and pack images | |||||
| def prepare_assets(): | |||||
| props = bpy.context.window_manager.sketchfab | |||||
| hidden = [] | |||||
| images = [] | |||||
campbellbartonAuthorUnsubmitted Not Done Inline ActionsThis should probably be a set(), to de-duplicate on the fly. campbellbarton: This should probably be a `set()`, to de-duplicate on the fly. | |||||
| if props.models == 'selection' or props.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 | |||||
| if tex_slot.texture.type == 'IMAGE': | |||||
| images.append(tex_slot.texture.image) | |||||
| if (props.models == 'selection' and ob.type == 'MESH') or \ | |||||
| (props.lamps == 'selection' and ob.type == 'LAMP'): | |||||
| if not ob.select and not ob.hide: | |||||
| ob.hide = True | |||||
| hidden.append(ob) | |||||
| elif props.lamps == 'none' and ob.type == 'LAMP': | |||||
| if not ob.hide: | |||||
| ob.hide = True | |||||
| hidden.append(ob) | |||||
| packed = [] | |||||
| for img in images: | |||||
| if not img.packed_file: | |||||
| img.pack() | |||||
| packed.append(img) | |||||
campbellbartonAuthorUnsubmitted Not Done Inline ActionsIts possible pack fails, for example if the file is missing, currently this will give a rather ugly error popup and the script fails halfway through. See the C code for this - newPackedFile if (file < 0) BKE_reportf(reports, RPT_ERROR, "Unable to pack file, source path '%s' not found", name); So, best pass operator reports to prepare_assets and report a warning if pack fails for any files. We should probably have a general utility function for converting a python exception into a warning. nevertheless this should be handled somehow. campbellbarton: Its possible pack fails, for example if the file is missing, currently this will give a rather… | |||||
| return (hidden, packed) | |||||
| # restore original situation | |||||
| def restore(hidden, packed): | |||||
| for ob in hidden: | |||||
| ob.hide = False | |||||
| for img in packed: | |||||
| img.unpack(method='USE_ORIGINAL') | |||||
| # save a copy of the current blendfile | |||||
| def save_blend_copy(): | |||||
| filepath = bpy.data.filepath | |||||
| filename_pos = len(bpy.path.basename(bpy.data.filepath)) | |||||
campbellbartonAuthorUnsubmitted Not Done Inline Actionsstrange API use, better do os.path.dirname campbellbarton: strange API use, better do `os.path.dirname` | |||||
| filepath = filepath[:-filename_pos] | |||||
| filename = time.strftime("Sketchfab_%Y_%m_%d_%H_%M_%S.blend", | |||||
| time.localtime(time.time())) | |||||
| filepath += filename | |||||
campbellbartonAuthorUnsubmitted Not Done Inline Actionsbetter use os.path.join() campbellbarton: better use `os.path.join()` | |||||
| bpy.ops.wm.save_as_mainfile(filepath=filepath, compress=True, | |||||
| copy=True) | |||||
| size = os.path.getsize(filepath) | |||||
| return(filepath, filename, size) | |||||
| # remove file copy | |||||
| def terminate(filepath): | |||||
| os.remove(filepath) | |||||
| # save token to file | |||||
| def update_token(self, context): | |||||
| token = context.window_manager.sketchfab.token | |||||
| path = os.path.join(bpy.utils.user_resource('SCRIPTS'), "presets") | |||||
| if not os.path.exists(path): | |||||
| os.makedirs(path) | |||||
| filepath = os.path.join(path, "sketchfab.txt") | |||||
| file = open(filepath, 'w+') | |||||
campbellbartonAuthorUnsubmitted Not Done Inline Actions
campbellbarton: - as before, use context manager.
- `w+` opens a file for both reading and writing, looks like… | |||||
| file.write(token) | |||||
| file.close() | |||||
| def show_upload_result(msg, msg_type, result=None): | |||||
| props = bpy.context.window_manager.sketchfab | |||||
| props.message = msg | |||||
| props.message_type = msg_type | |||||
| if result: | |||||
| props.result = result | |||||
| # upload the blend-file to sketchfab | |||||
| def upload(filepath, filename): | |||||
| props = bpy.context.window_manager.sketchfab | |||||
| title = props.title | |||||
| if not title: | |||||
| title = bpy.path.basename(bpy.data.filepath).split('.')[0] | |||||
campbellbartonAuthorUnsubmitted Not Done Inline Actionsbetter use os.path.splitext campbellbarton: better use `os.path.splitext` | |||||
| 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 show_upload_result('Upload failed. Error: %s' % str(e), 'ERROR') | |||||
| result = r.json() | |||||
| if r.status_code != requests.codes.ok: | |||||
| return show_upload_result('Upload failed. Error: %s' % result['error'], 'ERROR') | |||||
| model_url = SKETCHFAB_MODEL_URL + result['result']['id'] | |||||
| return show_upload_result('Upload complete. %s' % model_url, 'INFO', model_url) | |||||
| # 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(): | |||||
| props = context.window_manager.sketchfab | |||||
| terminate(props.filepath) | |||||
| if context.area: | |||||
| context.area.tag_redraw() | |||||
| if not props.message_type: | |||||
| props.message_type = 'ERROR' | |||||
| self.report({props.message_type}, props.message) | |||||
| if props.message_type == 'INFO': | |||||
| bpy.ops.wm.call_menu(name="VIEW3D_MT_popup_result") | |||||
| context.window_manager.event_timer_remove(self._timer) | |||||
| self._thread.join() | |||||
| props.uploading = False | |||||
| return {'FINISHED'} | |||||
| return {'PASS_THROUGH'} | |||||
| def execute(self, context): | |||||
| props = context.window_manager.sketchfab | |||||
| if not props.token: | |||||
| self.report({'ERROR'}, "Token is missing") | |||||
| return {'CANCELLED'} | |||||
| props.uploading = True | |||||
| hidden, packed = prepare_assets() | |||||
| props.filepath, filename, size_blend = save_blend_copy() | |||||
| props.size = format_size(size_blend) | |||||
| restore(hidden, packed) | |||||
| self._thread = threading.Thread( | |||||
| target=upload, | |||||
| args=(props.filepath, filename) | |||||
| ) | |||||
| self._thread.start() | |||||
| context.window_manager.modal_handler_add(self) | |||||
| self._timer = context.window_manager.event_timer_add(1.0, | |||||
| context.window) | |||||
| return {'RUNNING_MODAL'} | |||||
| def cancel(self, context): | |||||
| context.window_manager.event_timer_remove(self._timer) | |||||
| self._thread.join() | |||||
| return {'CANCELLED'} | |||||
Not Done Inline Actions-b and --background are the same thing. campbellbarton: `-b` and `--background` are the same thing. | |||||
| # popup to say that something is already being uploaded | |||||
| class ExportSketchfabBusy(bpy.types.Operator): | |||||
| '''Upload your model to Sketchfab''' | |||||
| bl_idname = "export.sketchfab_busy" | |||||
| bl_label = "Uploading" | |||||
| def execute(self, context): | |||||
| self.report({'WARNING'}, "Please wait till current upload is finished") | |||||
| return {'FINISHED'} | |||||
| # menu class to display the url after uploading | |||||
| class VIEW3D_MT_popup_result(bpy.types.Menu): | |||||
campbellbartonAuthorUnsubmitted Not Done Inline ActionsI don't think this menu is needed, better use a dynamic popup? campbellbarton: I don't think this menu is needed, better use a dynamic popup?
See: http://www.blender. | |||||
| bl_label = "Upload successful" | |||||
| def draw(self, context): | |||||
| layout = self.layout | |||||
| result = context.window_manager.sketchfab.result | |||||
| layout.operator("wm.url_open", text="View online").url = result | |||||
| # user interface | |||||
| class VIEW3D_PT_sketchfab(bpy.types.Panel): | |||||
| bl_space_type = 'VIEW_3D' | |||||
| bl_region_type = 'UI' | |||||
| bl_label = "Sketchfab" | |||||
| def draw(self, context): | |||||
| props = context.window_manager.sketchfab | |||||
| if props.token_reload: | |||||
| props.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.alignment = 'RIGHT' | |||||
| row.operator('object.dialog_operator', text="Claim your token") | |||||
| if props.uploading: | |||||
| layout.operator("export.sketchfab_busy", | |||||
| text="Uploading " + props.size) | |||||
| else: | |||||
| layout.operator("export.sketchfab") | |||||
| # property group containing all properties for the user interface | |||||
| class SketchfabProps(bpy.types.PropertyGroup): | |||||
| description = bpy.props.StringProperty(name="Description", | |||||
campbellbartonAuthorUnsubmitted Not Done Inline Actionstypically we import all props used in the script... rather then bpy.props.StringProperty each time. campbellbarton: typically we import all props used in the script... rather then `bpy.props.StringProperty` each… | |||||
| description = "Description of the model (optional)", | |||||
| default = "") | |||||
| filepath = bpy.props.StringProperty(name="Filepath", | |||||
| description = "internal use", | |||||
| default = "") | |||||
| lamps = bpy.props.EnumProperty(name="Lamps", | |||||
| items = (('all', "All", "Export all lamps in the file"), | |||||
| ('none', "None", "Don't export any lamps"), | |||||
campbellbartonAuthorUnsubmitted Not Done Inline Actionsour convention us to use all-caps for enum values. (this goes for all enums here). campbellbarton: our convention us to use all-caps for enum values. (this goes for all enums here). | |||||
| ('selection', "Selection", "Only export selected lamps")), | |||||
| description = "Determines which lamps are exported", | |||||
| default = 'all') | |||||
| message = bpy.props.StringProperty(name="Message", | |||||
| description = "internal use", | |||||
| default = "") | |||||
| message_type = bpy.props.StringProperty(name="Message type", | |||||
| description = "internal use", | |||||
| default = "") | |||||
| models = bpy.props.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') | |||||
| result = bpy.props.StringProperty(name="Result", | |||||
| description = "internal use, stores the url of the uploaded model", | |||||
| default = "") | |||||
| size = bpy.props.StringProperty(name="Size", | |||||
| description = "Current filesize being uploaded", | |||||
| default = "") | |||||
| private = bpy.props.BoolProperty(name="Private", | |||||
| description = "Upload as private (requires a pro account)", | |||||
| default = False) | |||||
| password = bpy.props.StringProperty(name="Password", | |||||
| description = "Password-protect your model (requires a pro account)", | |||||
| default = "") | |||||
| tags = bpy.props.StringProperty(name="Tags", | |||||
| description = "List of tags, separated by spaces (optional)", | |||||
| default = "") | |||||
| title = bpy.props.StringProperty(name="Title", | |||||
| description = "Title of the model (determined automatically if \ | |||||
| left empty)", | |||||
| default = "") | |||||
| token = bpy.props.StringProperty(name="Api Key", | |||||
| description = "You can find this on your dashboard at the Sketchfab \ | |||||
| website", | |||||
| default = "", | |||||
| update = update_token) | |||||
| token_reload = bpy.props.BoolProperty(name="Reload of token necessary?", | |||||
| description = "internal use", | |||||
| default = True) | |||||
| token_reload = bpy.props.BoolProperty(name="Reload of token necessary?", | |||||
| description = "internal use", | |||||
| default = True) | |||||
| uploading = bpy.props.BoolProperty(name="Busy uploading", | |||||
| description = "internal use", | |||||
| default = False) | |||||
| class DialogOperator(bpy.types.Operator): | |||||
| bl_idname = "object.dialog_operator" | |||||
| bl_label = "Enter your email to get a sketchfab token" | |||||
| email = bpy.props.StringProperty(name="Email", | |||||
| default="you@example.com") | |||||
| def execute(self, context): | |||||
| 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) | |||||
| # registration | |||||
| classes = [ExportSketchfab, | |||||
| ExportSketchfabBusy, | |||||
| SketchfabProps, | |||||
| DialogOperator, | |||||
| VIEW3D_MT_popup_result, | |||||
| VIEW3D_PT_sketchfab] | |||||
| def register(): | |||||
| for c in classes: | |||||
| bpy.utils.register_class(c) | |||||
| bpy.types.WindowManager.sketchfab = bpy.props.PointerProperty( | |||||
| type = SketchfabProps) | |||||
| load_token() | |||||
| bpy.app.handlers.load_post.append(load_token) | |||||
| def unregister(): | |||||
| for c in classes: | |||||
| bpy.utils.unregister_class(c) | |||||
| try: | |||||
| del bpy.types.WindowManager.sketchfab | |||||
| except: | |||||
| pass | |||||
| if __name__ == "__main__": | |||||
| register() | |||||
should have typical shortened GPL header we have at the top of all bundled addons.