save startup blend for animation tab & whatnot
This commit is contained in:
2026-04-08 12:10:18 -06:00
parent 57a652524a
commit 692e200ffe
180 changed files with 12336 additions and 3431 deletions
+194 -50
View File
@@ -16,16 +16,16 @@
#
# ##### END GPL LICENSE BLOCK #####
from __future__ import annotations
import addon_utils
import copy
import json
import logging
import os
import shutil
import tempfile
import time
import urllib.request
from typing import Optional
from . import (
append_link,
@@ -56,6 +56,12 @@ from bpy.props import (
bk_logger = logging.getLogger(__name__)
STALE_DOWNLOAD_TIMEOUT = (
20.0 # seconds without progress before we treat a download as stalled
)
download_tasks = {}
def get_blenderkit_repository():
"""Find the BlenderKit extensions repository index.
@@ -123,7 +129,7 @@ def get_addon_installation_status(asset_data):
) and addon_module.startswith("bl_ext."):
is_enabled = True
bk_logger.info(
f"Found enabled addon with extension format: {addon_module}"
"Found enabled addon with extension format: %s", addon_module
)
break
@@ -141,11 +147,12 @@ def get_addon_installation_status(asset_data):
) and addon_module.__name__.startswith("bl_ext."):
is_installed = True
bk_logger.info(
f"Found installed addon with extension format: {addon_module.__name__}"
"Found installed addon with extension format: %s",
addon_module.__name__,
)
break
except Exception as e:
bk_logger.warning(f"Error checking addon_utils.modules(): {e}")
bk_logger.warning("Error checking addon_utils.modules(): %s", e)
# If found through addon_utils, we know it's installed
# But we need to double-check enabled status using the correct module name
@@ -160,10 +167,10 @@ def get_addon_installation_status(asset_data):
# Check if this specific module name is enabled
is_enabled = addon_module.__name__ in enabled_addons
if is_enabled:
bk_logger.info(f"Found enabled addon: {addon_module.__name__}")
bk_logger.info("Found enabled addon: %s", addon_module.__name__)
break
except Exception as e:
bk_logger.warning(f"Error double-checking enabled status: {e}")
bk_logger.warning("Error double-checking enabled status: %s", e)
# Method 3: If not found through traditional addon system, check extensions system
if not is_installed:
@@ -173,7 +180,7 @@ def get_addon_installation_status(asset_data):
"blenderkit_extensions_repo_cache", {}
)
for cache_key, pkg_data in bk_ext_cache.items():
for _cache_key, pkg_data in bk_ext_cache.items():
if isinstance(pkg_data, dict) and pkg_data.get("id") == extension_id:
# Check if it's actually installed in the extension system
is_installed = pkg_data.get("installed", False)
@@ -182,12 +189,11 @@ def get_addon_installation_status(asset_data):
is_enabled = pkg_data.get("enabled", False)
break
except Exception as e:
bk_logger.warning(f"Error checking extension cache: {e}")
bk_logger.warning("Error checking extension cache: %s", e)
# Method 4: Check through Blender's extension repositories directly
if not is_installed:
try:
# Look for BlenderKit repository and check its packages
for repo in bpy.context.preferences.extensions.repos:
if not repo.enabled:
@@ -203,7 +209,7 @@ def get_addon_installation_status(asset_data):
# For now, we'll rely on the previous methods
break
except Exception as e:
bk_logger.warning(f"Error checking extension repositories: {e}")
bk_logger.warning("Error checking extension repositories: %s", e)
# Debug: Show some enabled addons for reference
blenderkit_addons = [
@@ -252,7 +258,7 @@ def install_addon_from_local_file(asset_data, file_path, enable_on_install=True)
reports.add_report(error_msg, type="ERROR")
raise Exception(error_msg)
bk_logger.info(f"Installing addon '{addon_name}' from local file: {file_path}")
bk_logger.info("Installing addon '%s' from local file: %s", addon_name, file_path)
status = get_addon_installation_status(asset_data)
if status["installed"]:
@@ -288,7 +294,19 @@ def install_addon_from_local_file(asset_data, file_path, enable_on_install=True)
)
download_tasks = {}
def _reset_progress_for_asset_ids(asset_ids):
"""Reset UI progress bars for the given asset ids."""
if not asset_ids:
return
search_results = search.get_search_results()
if search_results is None:
return
for result in search_results:
if result.get("id") in asset_ids:
result["downloaded"] = 0
INT32_MIN = -2_147_483_648
INT32_MAX = 2_147_483_647
@@ -348,7 +366,7 @@ def check_unused():
for l in bpy.data.libraries:
if l not in used_libs and l.getn("asset_data"):
bk_logger.info(f"attempt to remove this library: {l.filepath}")
bk_logger.info("attempt to remove this library: %s", l.filepath)
# have to unlink all groups, since the file is a 'user' even if the groups aren't used at all...
for user_id in l.users_id:
if type(user_id) == bpy.types.Collection:
@@ -364,7 +382,7 @@ def get_temp_enabled_addons():
temp_addons_json = prefs.temp_enabled_addons
return json.loads(temp_addons_json)
except Exception as e:
bk_logger.warning(f"Error reading temporary addons from preferences: {e}")
bk_logger.warning("Error reading temporary addons from preferences: %s", e)
return []
@@ -374,9 +392,9 @@ def set_temp_enabled_addons(addon_list):
try:
prefs = bpy.context.preferences.addons[__package__].preferences
prefs.temp_enabled_addons = json.dumps(addon_list)
bk_logger.info(f"Saved {len(addon_list)} temporary addons to preferences")
bk_logger.info("Saved %d temporary addons to preferences", len(addon_list))
except Exception as e:
bk_logger.error(f"Error saving temporary addons to preferences: {e}")
bk_logger.error("Error saving temporary addons to preferences: %s", e)
def add_temp_enabled_addon(pkg_id):
@@ -385,7 +403,7 @@ def add_temp_enabled_addon(pkg_id):
if pkg_id not in temp_enabled:
temp_enabled.append(pkg_id)
set_temp_enabled_addons(temp_enabled)
bk_logger.info(f"Added {pkg_id} to temporary addons list")
bk_logger.info("Added %s to temporary addons list", pkg_id)
def cleanup_temp_enabled_addons():
@@ -398,24 +416,24 @@ def cleanup_temp_enabled_addons():
bk_logger.info("No temporarily enabled addons to clean up")
return
bk_logger.info(f"Cleaning up {len(temp_enabled)} temporarily enabled addons")
bk_logger.info("Cleaning up %d temporarily enabled addons", len(temp_enabled))
# Disable all temporarily enabled addons using preferences API
for pkg_id in temp_enabled:
try:
full_module_name = f"bl_ext.www_blenderkit_com.{pkg_id}"
bpy.ops.preferences.addon_disable(module=full_module_name)
bk_logger.info(f"Disabled temporarily enabled addon: {pkg_id}")
bk_logger.info("Disabled temporarily enabled addon: %s", pkg_id)
except Exception as e:
bk_logger.warning(
f"Failed to disable temporarily enabled addon {pkg_id}: {e}"
"Failed to disable temporarily enabled addon %s: %s", pkg_id, e
)
# Clear the list in preferences
set_temp_enabled_addons([])
bk_logger.info("Temporary addon cleanup completed")
except Exception as e:
bk_logger.error(f"Error during temporary addon cleanup: {e}")
bk_logger.error("Error during temporary addon cleanup: %s", e)
@persistent
@@ -466,7 +484,7 @@ def refresh_addon_search_results_status():
asset_data["enabled"] = False
except Exception as e:
bk_logger.warning(f"Error refreshing addon search results status: {e}")
bk_logger.warning("Error refreshing addon search results status: %s", e)
@persistent
@@ -617,7 +635,7 @@ def _sanitize_for_idprops(value):
return value
def udpate_asset_data_in_dicts(asset_data):
def update_asset_data_in_dicts(asset_data):
"""
updates asset data in all relevant dictionaries, after a threaded download task \
- where the urls were retrieved, and now they can be reused
@@ -705,6 +723,8 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None,
user_preferences = bpy.context.preferences.addons[__package__].preferences
user_preferences.download_counter += 1
asset_main = None
if asset_data["assetType"] == "scene":
sprops = wm.blenderkit_scene
@@ -758,7 +778,7 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None,
# also, if it was successful, no other operations are needed , basically all asset data is already ready from the original asset
if new_obs:
# update here assets rated/used because there might be new download urls?
udpate_asset_data_in_dicts(asset_data)
update_asset_data_in_dicts(asset_data)
bpy.ops.ed.undo_push(
"INVOKE_REGION_WIN",
message="add %s to scene" % asset_data["name"],
@@ -772,7 +792,7 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None,
if downloaders:
for downloader in downloaders:
# this cares for adding particle systems directly to target mesh, but I had to block it now,
# because of the sluggishnes of it. Possibly re-enable when it's possible to do this faster?
# because of the sluggishness of it. Possibly re-enable when it's possible to do this faster?
if (
"particle_plants" in asset_data["tags"]
and kwargs["target_object"] != ""
@@ -854,6 +874,7 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None,
lib["asset_data"] = asset_data
elif asset_data["assetType"] == "brush":
brush = None
inscene = False
for b in bpy.data.brushes:
if b.blenderkit.id == asset_data["id"]:
@@ -910,9 +931,11 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None,
)
# TODO add grease pencil brushes!
# bpy.context.tool_settings.image_paint.brush = brush
asset_main = brush
if brush is not None:
asset_main = brush
elif asset_data["assetType"] == "material":
material = None
inscene = False
sprops = wm.blenderkit_mat
@@ -927,12 +950,14 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None,
file_names[-1], matname=asset_data["name"], link=link, fake_user=False
)
target_object = bpy.data.objects[kwargs["target_object"]]
assign_material(target_object, material, kwargs["material_target_slot"])
if material is not None:
target_object = bpy.data.objects[kwargs["target_object"]]
assign_material(target_object, material, kwargs["material_target_slot"])
asset_main = material
asset_main = material
elif asset_data["assetType"] == "nodegroup":
nodegroup = None
inscene = False
sprops = wm.blenderkit_nodegroup
for g in bpy.data.node_groups:
@@ -979,11 +1004,12 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None,
model_location=kwargs.get("model_location", (0, 0, 0)),
model_rotation=kwargs.get("model_rotation", (0, 0, 0)),
)
bk_logger.info(f"appended nodegroup: {nodegroup}")
asset_main = nodegroup
if nodegroup is not None:
bk_logger.info("appended nodegroup: %s", nodegroup)
asset_main = nodegroup
asset_data["resolution"] = kwargs["resolution"]
udpate_asset_data_in_dicts(asset_data)
update_asset_data_in_dicts(asset_data)
if asset_main is not None:
update_asset_metadata(asset_main, asset_data)
@@ -1000,6 +1026,36 @@ def update_asset_metadata(asset_main, asset_data):
asset_main.blenderkit.id = asset_data["id"]
asset_main.blenderkit.description = asset_data["description"]
asset_main.blenderkit.tags = utils.list2string(asset_data["tags"])
asset_main.blenderkit.is_private = (
"PRIVATE" if asset_data["isPrivate"] else "PUBLIC"
)
asset_main.blenderkit.verification_status = asset_data.get(
"verificationStatus", "UPLOADING"
).upper()
# manufacturer
if asset_data.get("dictParameters"):
dp = asset_data["dictParameters"]
if dp.get("manufacturer"):
asset_main.blenderkit.manufacturer = dp["manufacturer"]
else:
asset_main.blenderkit.manufacturer = ""
if dp.get("designer"):
asset_main.blenderkit.designer = dp["designer"]
else:
asset_main.blenderkit.designer = ""
if dp.get("designCollection"):
asset_main.blenderkit.design_collection = dp["designCollection"]
else:
asset_main.blenderkit.design_collection = ""
if dp.get("designVariant"):
asset_main.blenderkit.design_variant = dp["designVariant"]
else:
asset_main.blenderkit.design_variant = ""
if dp.get("designYear"):
asset_main.blenderkit.use_design_year = True
asset_main.blenderkit.design_year = int(float(dp["designYear"]))
# BUG #554: categories needs update, but are not in asset_data
sanitized = _sanitize_for_idprops(asset_data)
# TODO consider reducing stored fields for filesize.
@@ -1022,11 +1078,11 @@ def replace_resolution_linked(file_paths, asset_data):
bk_logger.debug("try to re-link library")
if not os.path.isfile(file_paths[-1]):
bk_logger.debug("library file doesnt exist")
bk_logger.debug("library file doesn't exist")
break
l.filepath = os.path.join(os.path.dirname(l.filepath), file_name)
l.name = file_name
udpate_asset_data_in_dicts(asset_data)
update_asset_data_in_dicts(asset_data)
def replace_resolution_appended(file_paths, asset_data, resolution):
@@ -1060,7 +1116,7 @@ def replace_resolution_appended(file_paths, asset_data, resolution):
for pf in i.packed_files:
pf.filepath = fp
i.reload()
udpate_asset_data_in_dicts(asset_data)
update_asset_data_in_dicts(asset_data)
# TODO: keep this until we check resolution replacement and other features from this one are supported in daemon.
@@ -1179,6 +1235,13 @@ def handle_download_task(task: client_tasks.Task):
"""
global download_tasks
# If the task was already pruned/cancelled, ignore late reports from the client.
if task.task_id not in download_tasks:
bk_logger.debug(
"Ignoring late download task %s (no longer tracked)", task.task_id
)
return
if task.status == "finished":
# we still write progress since sometimes the progress bars wouldn't end on 100%
download_write_progress(task.task_id, task)
@@ -1188,7 +1251,7 @@ def handle_download_task(task: client_tasks.Task):
download_tasks.pop(task.task_id)
return
except Exception as e:
bk_logger.exception(f"Asset appending/linking has failed")
bk_logger.exception("Asset appending/linking has failed: %s", e)
task.message = f"Append failed: {e}"
task.status = "error"
@@ -1205,15 +1268,92 @@ def clear_downloads():
download_tasks.clear()
def cancel_running_downloads(reason: str = ""):
"""Cancel all running downloads for this Blender process and reset local UI state."""
global download_tasks
if not download_tasks:
return
task_ids = list(download_tasks.keys())
asset_ids = set()
for task in download_tasks.values():
if not isinstance(task, dict):
continue
asset_id = task.get("asset_data", {}).get("id")
if asset_id:
asset_ids.add(asset_id)
suffix = f" ({reason})" if reason else ""
bk_logger.info("Cancelling %d running downloads%s", len(task_ids), suffix)
for task_id in task_ids:
try:
client_lib.cancel_download(task_id)
except Exception as e:
bk_logger.warning("Failed to cancel download %s: %s", task_id, e)
clear_downloads()
_reset_progress_for_asset_ids(asset_ids)
def prune_stalled_downloads(
max_idle_seconds: float = STALE_DOWNLOAD_TIMEOUT, now: Optional[float] = None
) -> None:
"""Cancel downloads that have not reported progress for too long."""
if not download_tasks:
return
now = now if now is not None else time.monotonic()
stalled_task_ids = []
stalled_asset_ids = set()
for task_id, task in list(download_tasks.items()):
last_report_time = task.get("last_report_time") or task.get("started_at")
if last_report_time is None:
task["last_report_time"] = now
continue
if now - last_report_time < max_idle_seconds:
continue
stalled_task_ids.append(task_id)
asset_info = task.get("asset_data", {})
asset_id = asset_info.get("id")
asset_name = asset_info.get("name", "Asset")
reports.add_report(
f"Download for {asset_name} stalled and was cancelled. Please try again.",
type="ERROR",
)
if asset_id:
stalled_asset_ids.add(asset_id)
if not stalled_task_ids:
return
for task_id in stalled_task_ids:
try:
client_lib.cancel_download(task_id)
except Exception as e:
bk_logger.warning("Failed to cancel stalled download %s: %s", task_id, e)
download_tasks.pop(task_id, None)
_reset_progress_for_asset_ids(stalled_asset_ids)
def download_write_progress(task_id, task):
"""writes progress from client_lib reports to addon tasks list"""
global download_tasks
task_addon = download_tasks.get(task.task_id)
if task_addon is None:
bk_logger.warning(f"couldn't write download progress to {task.progress}")
return
return # task was likely cancelled/stalled and removed; ignore late progress
task_addon["progress"] = task.progress
task_addon["text"] = task.message
task_addon["last_report_time"] = time.monotonic()
# go through search results to write progress to display progress bars
sr = search.get_search_results()
@@ -1350,6 +1490,7 @@ def download(asset_data, **kwargs):
if "unpack_files" in kwargs: # for add-on download
prefs["unpack_files"] = kwargs["unpack_files"]
now = time.monotonic()
data = {
"asset_data": asset_data,
"PREFS": prefs,
@@ -1362,6 +1503,9 @@ def download(asset_data, **kwargs):
data["download_dirs"] = paths.get_download_dirs(asset_data["assetType"])
if "downloaders" in kwargs:
data["downloaders"] = kwargs["downloaders"]
data.setdefault("downloaders", [])
data["started_at"] = now
data["last_report_time"] = now
response = client_lib.asset_download(data)
download_tasks[response["task_id"]] = data
@@ -1379,7 +1523,7 @@ def check_downloading(asset_data, **kwargs) -> bool:
p_asset_data = task["asset_data"]
if p_asset_data["id"] == asset_data["id"]:
at = asset_data["assetType"]
if at in ("model", "material"):
if at in ("model", "material", "printable", "scene"):
downloader = {
"location": kwargs["model_location"],
"rotation": kwargs["model_rotation"],
@@ -1449,7 +1593,7 @@ def try_finished_append(asset_data, **kwargs):
try:
os.remove(file_path)
except Exception as e1:
bk_logger.error(f"removing file {file_path} failed: {e1}")
bk_logger.error("removing file %s failed: %s", file_path, e1)
raise e
# Update downloaded status in search results
@@ -1619,9 +1763,9 @@ def start_download(asset_data, **kwargs) -> bool:
try_finished_append(asset_data, **kwargs)
return False
except Exception as e:
bk_logger.info(f"Failed to append asset: {e}, continuing with download")
bk_logger.info("Failed to append asset: %s, continuing with download", e)
if asset_data["assetType"] in ("model", "material"):
if asset_data["assetType"] in ("model", "material", "printable", "scene"):
downloader = {
"location": kwargs["model_location"],
"rotation": kwargs["model_rotation"],
@@ -1965,7 +2109,7 @@ class BlenderkitAddonChoiceOperator(bpy.types.Operator):
self.report({"INFO"}, f"Successfully enabled '{addon_name}'")
refresh_addon_search_results_status()
except Exception as e:
bk_logger.error(f"Failed to enable addon: {e}")
bk_logger.error("Failed to enable addon: %s", e)
reports.add_report(
f"Failed to enable '{addon_name}': {e}", type="ERROR"
)
@@ -1985,7 +2129,7 @@ class BlenderkitAddonChoiceOperator(bpy.types.Operator):
self.report({"INFO"}, f"Successfully disabled '{addon_name}'")
refresh_addon_search_results_status()
except Exception as e:
bk_logger.error(f"Failed to disable addon: {e}")
bk_logger.error("Failed to disable addon: %s", e)
reports.add_report(
f"Failed to disable '{addon_name}': {e}", type="ERROR"
)
@@ -2000,7 +2144,7 @@ class BlenderkitAddonChoiceOperator(bpy.types.Operator):
f"Temporary enable operation failed - returned: {result}"
)
except Exception as e:
bk_logger.error(f"Failed to temp enable addon: {e}")
bk_logger.error("Failed to temp enable addon: %s", e)
reports.add_report(
f"Failed to enable '{addon_name}': {e}", type="ERROR"
)
@@ -2016,7 +2160,7 @@ class BlenderkitAddonChoiceOperator(bpy.types.Operator):
refresh_addon_search_results_status()
except Exception as e:
bk_logger.error(f"Addon operation failed for '{addon_name}': {e}")
bk_logger.error("Addon operation failed for '%s': %s", addon_name, e)
error_msg = f"Failed to {selected_action.lower().replace('_', ' ')} '{addon_name}': {e}"
reports.add_report(error_msg, type="ERROR")
self.report({"ERROR"}, error_msg)
@@ -2309,7 +2453,7 @@ class BlenderkitDownloadOperator(bpy.types.Operator):
return {"FINISHED"}
# replace resolution needs to replace all instances of the resolution in the scene
# and deleting originals has to be thus done after the downlaod
# and deleting originals has to be thus done after the download
kwargs = {
"cast_parent": self.cast_parent,
@@ -2333,7 +2477,7 @@ class BlenderkitDownloadOperator(bpy.types.Operator):
return {"FINISHED"}
def draw(self, context):
# this timer is there to not let double clicks thorugh the popups down to the asset bar.
# this timer is there to not let double clicks through the popups down to the asset bar.
ui_panels.last_time_overlay_panel_active = time.time()
layout = self.layout
if self.invoke_resolution: