diff --git a/package_manager/__init__.py b/package_manager/__init__.py index e1f8a3f..4352674 100644 --- a/package_manager/__init__.py +++ b/package_manager/__init__.py @@ -30,10 +30,18 @@ bl_info = { import bpy +import addon_utils from bpy.types import Operator, AddonPreferences from bpy.props import StringProperty, BoolProperty, IntProperty, CollectionProperty -from . import networking +if "bpy" in locals(): + import imp + try: + imp.reload(networking) + except NameError: + from . import networking +else: + from . import networking class PackageManagerAddon(bpy.types.PropertyGroup): source = StringProperty() @@ -49,6 +57,7 @@ class PackageManagerAddon(bpy.types.PropertyGroup): warning = StringProperty() support = StringProperty() filename = StringProperty() + module_name = StringProperty() class PackageManagerPreferences(AddonPreferences): # this must match the addon name, use '__package__' @@ -56,15 +65,74 @@ class PackageManagerPreferences(AddonPreferences): bl_idname = __name__ pm_addons = CollectionProperty(type=PackageManagerAddon) - pm_addons_index = IntProperty() def draw(self, context): layout = self.layout layout.operator("wm.update_index", text="Update List", icon='FILE_REFRESH') - rows = 1 if len(self.pm_addons) == 0 else 6 + rows = 1 if len(self.pm_addons) == 0 else 4 layout.template_list("UI_UL_list", "addons_list", self, "pm_addons", self, "pm_addons_index", rows=rows) + + if len(self.pm_addons) == 0: + # No add-ons, return + return + + # Display selected add-on + addon = self.pm_addons[self.pm_addons_index] + + installed = False + for module in addon_utils.modules(): + if module.__name__ == addon.module_name: + installed = True + break + + col_box = layout.column() + box = col_box.box() + colsub = box.column() + + split = colsub.row().split(percentage=0.25) + split.label(text="Installed: %s" % ("Yes" if installed else "No")) + split.separator() + split.separator() + split.operator("wm.addon_download_install", + text="Install from Web", + icon='URL').addon = addon.module_name + + if addon.description: + split = colsub.row().split(percentage=0.15) + split.label(text="Description:") + split.label(text=addon.description) + if addon.location: + split = colsub.row().split(percentage=0.15) + split.label(text="Location:") + split.label(text=addon.location) + if addon.author: + split = colsub.row().split(percentage=0.15) + split.label(text="Author:") + split.label(text=addon.author, translate=False) + if addon.version: + split = colsub.row().split(percentage=0.15) + split.label(text="Version:") + split.label(text=addon.version, translate=False) + if addon.warning: + split = colsub.row().split(percentage=0.15) + split.label(text="Warning:") + split.label(text=' ' + addon.warning, icon='ERROR') + + tot_row = bool(addon.wiki_url) + + if tot_row: + split = colsub.row().split(percentage=0.15) + split.label(text="Internet:") + if addon.wiki_url: + split.operator("wm.url_open", text="Documentation", icon='HELP').url = addon.wiki_url + split.operator("wm.url_open", text="Report a Bug", icon='URL').url = addon.get( + "tracker_url", + "http://developer.blender.org/maniphest/task/create/?project=3&type=Bug") + + for i in range(4 - tot_row): + split.separator() def register(): networking.register() diff --git a/package_manager/networking.py b/package_manager/networking.py index d963314..73656b3 100644 --- a/package_manager/networking.py +++ b/package_manager/networking.py @@ -16,61 +16,172 @@ # ======================= END GPL LICENSE BLOCK ======================== import bpy +import addon_utils import json import urllib.request +import logging +import shutil +from bpy.props import StringProperty +logging.basicConfig(format='%(asctime)-15s %(levelname)8s %(name)s %(message)s', + level=logging.INFO) +log = logging.getLogger('networking') -class UpdateIndex(bpy.types.Operator): - """Update the list of add-ons available for download""" +class WM_OT_update_index(bpy.types.Operator): + """Check for updated list of add-ons available for download""" bl_idname = "wm.update_index" - bl_label = "Update list of add-ons" + bl_label = "Check for updated list of add-ons" def execute(self, context): + # Download the index.json file try: req = urllib.request.urlopen("https://git.blender.org/gitweb/gitweb.cgi/" "blender-package-manager-addon.git/blob_plain/HEAD:/addons/index.json") - index_file = req.read() - del req + index_file = req.read().decode('utf-8') + req.close() except urllib.error.HTTPError as err: self.report({'ERROR'}, "Error requesting update: " + str(err.code) + " " + err.reason) return {'CANCELLED'} + # Parse downloaded file try: addon_list = json.loads(index_file) - except ValueError: + except ValueError as err: self.report({'ERROR'}, "Error: JSON file could not parse.") + log.warning("ValueError: %s", err) return {'CANCELLED'} - for addon_list["addons"] as name, content: - addon = PackageManagerAddon() - addon.name = name - addon.source = content["source"] - addon.description = content["description"] - addon.author = content["author"] - addon.wiki_url = content["wiki_url"] - addon.tracker_url = content["tracker_url"] - addon.location = content["location"] - addon.category = content["category"] - for content["version"] as item: - # TODO: add multi-version functionality - for item as version, value: - addon.version = key - addon.blender = value["blender"] - addon.support = value["support"].upper() - try: - addon.warning = content["warning"] - except Exception: - pass - addon.filename = value["filename"] + # Get the add-on preferences + prefs = bpy.context.user_preferences.addons.get("package_manager").preferences + + # Clear previous list of add-ons + prefs.pm_addons.clear() + prefs.pm_addons_index = 0 - print(index_file) + user_path = bpy.utils.user_resource('SCRIPTS', path="addons") + installed = addon_utils.modules() + + # Loop through every add-on in the parsed json + for name, content in addon_list["addons"].items(): + # Skip add-ons not installed to USER directory + # TODO: support above later + for a in installed: + if name == a.__name__ and user_path not in a.__file__: + log.warning("Not listing add-on %s, as it is installed to " + "location other than USER path", name) + break + else: + # Add new add-on to the list + addon = prefs.pm_addons.add() + + self.load_addon_data(addon, name, content) return {'FINISHED'} + + def load_addon_data(self, addon, module_name, content): + addon.name = content["name"] + addon.blender = '.'.join(map(str, content["blender"])) + addon.module_name = module_name + + if "author" in content: + addon.author = content["author"] + + if "category" in content: + addon.category = content["category"] + + if "description" in content: + addon.description = content["description"] + + if "filename" in content: + addon.filename = content["filename"] + + if "location" in content: + addon.location = content["location"] + + if "source" in content: + addon.source = content["source"] + + if "support" in content: + addon.support = content["support"] + + if "tracker_url" in content: + addon.tracker_url = content["tracker_url"] + + if "version" in content: + # TODO: add multi-version functionality + addon.version = '.'.join(map(str, content["version"])) + + if "wiki_url" in content: + addon.wiki_url = content["wiki_url"] + + if "warning" in content: + addon.warning = content["warning"] + + +class WM_OT_addon_download_install(bpy.types.Operator): + """Download and install add-on""" + bl_idname = "wm.addon_download_install" + bl_label = "Download and install selected add-on" + + addon = bpy.props.StringProperty() + + def execute(self, context): + if self.addon is None: + return {'CANCELLED'} + + # Get the add-on preferences + prefs = bpy.context.user_preferences.addons.get("package_manager").preferences + for addon in prefs.pm_addons: + if addon.module_name == self.addon: + break + else: + print("failed :(") + return {'CANCELLED'} + + # TODO: specify filetype + + if self.download(self.addon): + if self.install(self.addon): + return {'FINISHED'} + + return {'CANCELLED'} + + def download(self, addon, filetype=".py"): + filename = addon + filetype + + download_path = bpy.utils.user_resource('SCRIPTS', + path="addons/package_manager/download%s" % filetype) + + try: + req = urllib.request.urlopen("http://localhost:8000/%s" % filename) + with open(download_path, 'wb') as download_file: + shutil.copyfileobj(req, download_file) + req.close() + except urllib.error.HTTPError as err: + log.warning("Download failed with HTTPError: %s %s", + str(err.code), err.reason) + return False + + return True + + def install(self, addon, filetype=".py"): + filename = addon + filetype if filetype == ".py" else "" + + download_path = bpy.utils.user_resource('SCRIPTS', + path="addons/package_manager/download%s" % filetype) + addon_path = bpy.utils.user_resource('SCRIPTS', path="addons/%s" % filename) + + if filetype == ".py": + shutil.move(download_path, addon_path) + + return True def register(): - bpy.utils.register_class(UpdateIndex) + bpy.utils.register_class(WM_OT_update_index) + bpy.utils.register_class(WM_OT_addon_download_install) def unregister(): - bpy.utils.unregister_class(UpdateIndex) \ No newline at end of file + bpy.utils.unregister_class(WM_OT_update_index) + bpy.utils.unregister_class(WM_OT_addon_download_install) \ No newline at end of file