Files
Avatar-Toolkit/core/updater.py
T
Yusarina 346f73ebb0 Bug Fixes
- Fixes issue where console was getting spammed re armature selection.
- Fixes apply as rest pose issues and improves the system. https://github.com/teamneoneko/Avatar-Toolkit/issues/52
- Fixes Apply pose to shapekey, it does what it's meant to do now. https://github.com/teamneoneko/Avatar-Toolkit/issues/52
- Fixes error in updater when there no updates to list.
2024-11-26 02:42:17 +00:00

301 lines
10 KiB
Python

import bpy
import os
import ssl
import json
import urllib
import shutil
import pathlib
import zipfile
import time
from threading import Thread
from bpy.app.handlers import persistent
from ..functions.translations import t
from .addon_preferences import get_preference, get_current_version, save_preference
from .register import register_wrap
from ..ui.panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from typing import Dict, List, Tuple, Optional, Set, Any
GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
is_checking_for_update: bool = False
update_needed: bool = False
latest_version: Optional[str] = None
latest_version_str: str = ''
version_list: Optional[Dict[str, List[str]]] = None
main_dir: str = os.path.dirname(os.path.dirname(__file__))
downloads_dir: str = os.path.join(main_dir, "downloads")
@register_wrap
class AvatarToolkit_OT_CheckForUpdate(bpy.types.Operator):
bl_idname = 'avatar_toolkit.check_for_update'
bl_label = t('CheckForUpdateButton.label')
bl_description = t('CheckForUpdateButton.desc')
bl_options = {'INTERNAL'}
def execute(self, context: bpy.types.Context) -> Set[str]:
check_for_update_background()
return {'FINISHED'}
@register_wrap
class AvatarToolkit_OT_UpdateToLatest(bpy.types.Operator):
bl_idname = 'avatar_toolkit.update_latest'
bl_label = t('UpdateToLatestButton.label')
bl_description = t('UpdateToLatestButton.desc')
bl_options = {'INTERNAL'}
def execute(self, context: bpy.types.Context) -> Set[str]:
update_now(latest=True)
return {'FINISHED'}
@register_wrap
class AvatarToolkit_OT_UpdateNotificationPopup(bpy.types.Operator):
bl_idname = "avatar_toolkit.update_notification_popup"
bl_label = t('UpdateNotificationPopup.label')
bl_description = t('UpdateNotificationPopup.desc')
bl_options = {'INTERNAL'}
def execute(self, context: bpy.types.Context) -> Set[str]:
update_now(latest=True)
self.report({'INFO'}, "Update started. Please wait for the process to complete.")
return {'FINISHED'}
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
return context.window_manager.invoke_props_dialog(self, width=300)
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
col = layout.column(align=True)
col.label(text=t('UpdateNotificationPopup.newUpdate', default="New update available: {version}").format(version=latest_version_str))
@register_wrap
class AvatarToolkit_PT_UpdaterPanel(bpy.types.Panel):
bl_label = t("Updater.label")
bl_idname = "OBJECT_PT_avatar_toolkit_updater"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 9
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
draw_updater_panel(context, layout)
@register_wrap
class AvatarToolkit_OT_RestartBlenderPopup(bpy.types.Operator):
bl_idname = "avatar_toolkit.restart_blender_popup"
bl_label = t('RestartBlenderPopup.label', default="Restart Blender")
bl_description = t('RestartBlenderPopup.desc', default="Restart Blender to complete the update")
bl_options = {'INTERNAL'}
def execute(self, context: bpy.types.Context) -> Set[str]:
return {'FINISHED'}
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
return context.window_manager.invoke_props_dialog(self, width=300)
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
col = layout.column(align=True)
col.label(text=t('RestartBlenderPopup.message', default="Update successful! Please restart Blender."))
@persistent
def check_for_update_on_start(dummy: Any) -> None:
if get_preference("check_for_updates_on_startup", True):
current_time = time.time()
last_check = get_preference("last_update_check", 0)
if current_time - last_check > 86400: # 24 hours
check_for_update_background()
save_preference("last_update_check", current_time)
def check_for_update_background() -> None:
global is_checking_for_update
if is_checking_for_update:
return
is_checking_for_update = True
Thread(target=check_for_update).start()
def check_for_update() -> None:
global update_needed, latest_version, latest_version_str, version_list
if not get_github_releases():
bpy.app.timers.register(lambda: finish_update_checking(error=t('check_for_update.cantCheck')))
return
update_needed = check_for_update_available()
if update_needed:
print('Update found!')
bpy.app.timers.register(lambda: bpy.ops.avatar_toolkit.update_notification_popup('INVOKE_DEFAULT') or None)
else:
print('No update found.')
bpy.app.timers.register(finish_update_checking)
def get_github_releases() -> bool:
global version_list
version_list = {}
try:
ssl._create_default_https_context = ssl._create_unverified_context
with urllib.request.urlopen(f'https://api.github.com/repos/{GITHUB_REPO}/releases') as url:
data = json.loads(url.read().decode())
except urllib.error.URLError:
print('URL ERROR')
return False
for version in data:
version_tag = version['tag_name']
version_list[version_tag] = [
version['zipball_url'],
version['body'],
version['published_at'].split('T')[0]
]
return True
def check_for_update_available() -> bool:
global latest_version, latest_version_str
if not version_list:
return False
latest_version = max(version_list.keys(), key=lambda v: [int(x) for x in v.split('.')])
latest_version_str = latest_version
current_version = get_current_version()
print(f"Current version: {current_version}") # Debugging statement
if current_version is None:
print("Current version is None. Cannot check for updates.")
return False
try:
# Validate that the version string contains only numeric parts
current_version_parts = [int(x) for x in current_version.split('.')]
latest_version_parts = [int(x) for x in latest_version.split('.')]
except ValueError as e:
print(f"Error parsing version numbers: {e}")
return False
return latest_version_parts > current_version_parts
def finish_update_checking(error: str = '') -> None:
global is_checking_for_update
is_checking_for_update = False
if update_needed:
bpy.ops.avatar_toolkit.update_notification_popup('INVOKE_DEFAULT')
ui_refresh()
return None # Important for bpy.app.timers
def update_now(latest: bool = False) -> None:
if not version_list:
print("No version list available. Please check for updates first.")
return
if latest:
update_link = version_list[latest_version_str][0]
else:
update_link = version_list[bpy.context.scene.avatar_toolkit_updater_version_list][0]
download_file(update_link)
ui_refresh()
def download_file(update_url: str) -> None:
update_zip_file = os.path.join(downloads_dir, "avatar-toolkit-update.zip")
if os.path.isdir(downloads_dir):
shutil.rmtree(downloads_dir)
pathlib.Path(downloads_dir).mkdir(exist_ok=True)
try:
ssl._create_default_https_context = ssl._create_unverified_context
urllib.request.urlretrieve(update_url, update_zip_file)
except urllib.error.URLError:
finish_update(error=t('download_file.cantConnect'))
return
if not os.path.isfile(update_zip_file):
finish_update(error=t('download_file.cantFindZip'))
return
with zipfile.ZipFile(update_zip_file, "r") as zip_ref:
zip_ref.extractall(downloads_dir)
os.remove(update_zip_file)
extracted_zip_dir = find_init_directory(downloads_dir)
if not extracted_zip_dir:
finish_update(error=t('download_file.cantFindAvatarToolkit'))
return
clean_addon_dir()
move_files(extracted_zip_dir, main_dir)
shutil.rmtree(downloads_dir)
finish_update()
def find_init_directory(path: str) -> Optional[str]:
for root, dirs, files in os.walk(path):
if "__init__.py" in files:
return root
return None
def clean_addon_dir() -> None:
for item in os.listdir(main_dir):
item_path = os.path.join(main_dir, item)
if item.startswith('.') or item in ['resources', 'downloads']:
continue
if os.path.isfile(item_path):
os.remove(item_path)
elif os.path.isdir(item_path):
shutil.rmtree(item_path)
def move_files(from_dir: str, to_dir: str) -> None:
for item in os.listdir(from_dir):
s = os.path.join(from_dir, item)
d = os.path.join(to_dir, item)
if os.path.isdir(s):
shutil.copytree(s, d, dirs_exist_ok=True)
else:
shutil.copy2(s, d)
def finish_update(error: str = '') -> None:
if error:
print(f"Update failed: {error}")
else:
print("Update successful!")
save_preference("version", latest_version_str)
bpy.ops.avatar_toolkit.restart_blender_popup('INVOKE_DEFAULT')
ui_refresh()
def get_version_list(self, context: bpy.types.Context) -> List[Tuple[str, str, str]]:
return [(v, v, '') for v in version_list.keys()] if version_list else []
def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
col = layout.column(align=True)
if is_checking_for_update:
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname, text=t('Updater.CheckForUpdateButton.label'))
elif update_needed:
col.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname, text=t('Updater.UpdateToLatestButton.label', name=latest_version_str))
else:
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname, text=t('Updater.CheckForUpdateButton.label_alt'))
col.separator()
row = col.row(align=True)
row.prop(context.scene, 'avatar_toolkit_updater_version_list', text='')
row.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname, text=t('Updater.UpdateToSelectedButton.label'))
col.separator()
col.label(text=t('Updater.currentVersion').format(name=get_current_version()))
def ui_refresh() -> None:
for windowManager in bpy.data.window_managers:
for window in windowManager.windows:
for area in window.screen.areas:
area.tag_redraw()