# ====================== 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 3 # 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, see . # # ======================= END GPL LICENSE BLOCK ======================== import bpy import addon_utils import json import logging import os import shutil import urllib.request import zipfile from bpy.props import StringProperty log = logging.getLogger('networking') INDEX_DOWNLOAD_URL = ("https://git.blender.org/gitweb/gitweb.cgi/" "blender-package-manager-addon.git/blob_plain/HEAD:/addons/index.json") 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 = "Check for updated list of add-ons" def execute(self, context): # Download the index.json file try: req = urllib.request.urlopen(INDEX_DOWNLOAD_URL) index_file = req.read().decode('utf-8') req.close() except urllib.error.HTTPError as err: self.report({'ERROR'}, "Error requesting update: %s %s" % (str(err.code), err.reason)) return {'CANCELLED'} # Parse downloaded file try: addon_list = json.loads(index_file) except ValueError as err: self.report({'ERROR'}, "Error: JSON file could not parse.") log.warning("ValueError: %s", err) return {'CANCELLED'} # 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 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 path # TODO: support above later for a in installed: if name == a.__name__ and user_path not in a.__file__: log.info("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 addon.download_url = content["download_url"] 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 "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 "warning" in content: addon.warning = content["warning"] if "wiki_url" in content: addon.wiki_url = content["wiki_url"] 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 # Verify add-on is in list and find its download url download_url = "" for addon in prefs.pm_addons: if addon.module_name == self.addon: download_url = addon.download_url break else: return {'CANCELLED'} ext = os.path.splitext(download_url)[1] # Download and install the selected add-on if self.download(self.addon, download_url): if self.install(self.addon, ext): return {'FINISHED'} return {'CANCELLED'} def download(self, addon, download_url): filetype = os.path.splitext(download_url)[1] download_path = bpy.utils.user_resource('SCRIPTS', path="addons/package_manager/" "download%s" % filetype) # Download add-on and save to disk try: req = urllib.request.urlopen(download_url) 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): 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) # Copy downloaded add-on to USER scripts path if filetype == ".py": shutil.move(download_path, addon_path) elif filetype == ".zip": # Remove existing add-on if os.path.exists(addon_path): shutil.rmtree(addon_path) with zipfile.ZipFile(download_path,"r") as zipped_addon: zipped_addon.extractall(bpy.utils.user_resource('SCRIPTS', path="addons")) return True def register(): bpy.utils.register_class(WM_OT_update_index) bpy.utils.register_class(WM_OT_addon_download_install) def unregister(): bpy.utils.unregister_class(WM_OT_update_index) bpy.utils.unregister_class(WM_OT_addon_download_install)