Package Manager add-on now functional, with limitations
- For now, add-ons are downloaded from http://localhost:8000/. - Add-ons on the index.json (from blender.org gitweb) are only displayed if they are not installed locally, or installed in the USER path. - Only .py add-ons are supported for now, but .zip add-ons will be supported next commit.
This commit is contained in:
@@ -30,10 +30,18 @@ bl_info = {
|
|||||||
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
import addon_utils
|
||||||
from bpy.types import Operator, AddonPreferences
|
from bpy.types import Operator, AddonPreferences
|
||||||
from bpy.props import StringProperty, BoolProperty, IntProperty, CollectionProperty
|
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):
|
class PackageManagerAddon(bpy.types.PropertyGroup):
|
||||||
source = StringProperty()
|
source = StringProperty()
|
||||||
@@ -49,6 +57,7 @@ class PackageManagerAddon(bpy.types.PropertyGroup):
|
|||||||
warning = StringProperty()
|
warning = StringProperty()
|
||||||
support = StringProperty()
|
support = StringProperty()
|
||||||
filename = StringProperty()
|
filename = StringProperty()
|
||||||
|
module_name = StringProperty()
|
||||||
|
|
||||||
class PackageManagerPreferences(AddonPreferences):
|
class PackageManagerPreferences(AddonPreferences):
|
||||||
# this must match the addon name, use '__package__'
|
# this must match the addon name, use '__package__'
|
||||||
@@ -56,15 +65,74 @@ class PackageManagerPreferences(AddonPreferences):
|
|||||||
bl_idname = __name__
|
bl_idname = __name__
|
||||||
|
|
||||||
pm_addons = CollectionProperty(type=PackageManagerAddon)
|
pm_addons = CollectionProperty(type=PackageManagerAddon)
|
||||||
|
|
||||||
pm_addons_index = IntProperty()
|
pm_addons_index = IntProperty()
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.operator("wm.update_index", text="Update List", icon='FILE_REFRESH')
|
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",
|
layout.template_list("UI_UL_list", "addons_list", self, "pm_addons",
|
||||||
self, "pm_addons_index", rows=rows)
|
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():
|
def register():
|
||||||
networking.register()
|
networking.register()
|
||||||
|
@@ -16,61 +16,172 @@
|
|||||||
# ======================= END GPL LICENSE BLOCK ========================
|
# ======================= END GPL LICENSE BLOCK ========================
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
import addon_utils
|
||||||
import json
|
import json
|
||||||
import urllib.request
|
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):
|
class WM_OT_update_index(bpy.types.Operator):
|
||||||
"""Update the list of add-ons available for download"""
|
"""Check for updated list of add-ons available for download"""
|
||||||
bl_idname = "wm.update_index"
|
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):
|
def execute(self, context):
|
||||||
|
# Download the index.json file
|
||||||
try:
|
try:
|
||||||
req = urllib.request.urlopen("https://git.blender.org/gitweb/gitweb.cgi/"
|
req = urllib.request.urlopen("https://git.blender.org/gitweb/gitweb.cgi/"
|
||||||
"blender-package-manager-addon.git/blob_plain/HEAD:/addons/index.json")
|
"blender-package-manager-addon.git/blob_plain/HEAD:/addons/index.json")
|
||||||
index_file = req.read()
|
index_file = req.read().decode('utf-8')
|
||||||
del req
|
req.close()
|
||||||
except urllib.error.HTTPError as err:
|
except urllib.error.HTTPError as err:
|
||||||
self.report({'ERROR'}, "Error requesting update: "
|
self.report({'ERROR'}, "Error requesting update: "
|
||||||
+ str(err.code) + " " + err.reason)
|
+ str(err.code) + " " + err.reason)
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
# Parse downloaded file
|
||||||
try:
|
try:
|
||||||
addon_list = json.loads(index_file)
|
addon_list = json.loads(index_file)
|
||||||
except ValueError:
|
except ValueError as err:
|
||||||
self.report({'ERROR'}, "Error: JSON file could not parse.")
|
self.report({'ERROR'}, "Error: JSON file could not parse.")
|
||||||
|
log.warning("ValueError: %s", err)
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
for addon_list["addons"] as name, content:
|
# Get the add-on preferences
|
||||||
addon = PackageManagerAddon()
|
prefs = bpy.context.user_preferences.addons.get("package_manager").preferences
|
||||||
addon.name = name
|
|
||||||
addon.source = content["source"]
|
# Clear previous list of add-ons
|
||||||
addon.description = content["description"]
|
prefs.pm_addons.clear()
|
||||||
addon.author = content["author"]
|
prefs.pm_addons_index = 0
|
||||||
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"]
|
|
||||||
|
|
||||||
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'}
|
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():
|
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():
|
def unregister():
|
||||||
bpy.utils.unregister_class(UpdateIndex)
|
bpy.utils.unregister_class(WM_OT_update_index)
|
||||||
|
bpy.utils.unregister_class(WM_OT_addon_download_install)
|
Reference in New Issue
Block a user