774 lines
27 KiB
Python
774 lines
27 KiB
Python
# ##### 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 2
|
|
# 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, write to the Free Software Foundation,
|
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# ##### END GPL LICENSE BLOCK #####
|
|
|
|
import dataclasses
|
|
import logging
|
|
import os
|
|
import platform
|
|
import shutil
|
|
import subprocess
|
|
from os import path
|
|
from typing import Optional
|
|
from http.client import responses as http_responses
|
|
|
|
|
|
import bpy
|
|
import requests
|
|
|
|
from . import datas, global_vars, reports, utils
|
|
|
|
|
|
bk_logger = logging.getLogger(__name__)
|
|
NO_PROXIES = {"http": "", "https": ""}
|
|
TIMEOUT = (0.1, 1)
|
|
|
|
|
|
def get_address() -> str:
|
|
"""Get address of the BlenderKit-Client."""
|
|
return f"http://127.0.0.1:{get_port()}"
|
|
|
|
|
|
def get_port() -> str:
|
|
"""Get the most probable port of currently running BlenderKit-Client.
|
|
After add-on registration and if all goes well, the port is the same as
|
|
"""
|
|
return global_vars.CLIENT_PORTS[0]
|
|
|
|
|
|
def get_api_version() -> str:
|
|
"""Get version of API Client is expected to use. To keep stuff simple the API version is derrived from Client's version.
|
|
From Client version vX.Y.Z we remove the .Z part to effectively get the vX.Y version of the API. For nonbreaking changes
|
|
we increase the patch version of the Client. If the change breaks the API, then increase of minor/major version is expected.
|
|
"""
|
|
splitted = global_vars.CLIENT_VERSION.split(".")
|
|
return ".".join(splitted[:-1])
|
|
|
|
|
|
def get_base_url() -> str:
|
|
"""The base URL on which we will interact with the BlenderKit Client. Consists from address with port + version API path.
|
|
All requests to Client goes to URLs starting with base URL in format: 127.0.0.1:{port}/vX.Y
|
|
"""
|
|
address = get_address()
|
|
vapi = get_api_version()
|
|
return f"{address}/{vapi}"
|
|
|
|
|
|
def ensure_minimal_data(data: Optional[dict] = None) -> dict:
|
|
"""Ensure that the data send to the BlenderKit-Client contains:
|
|
- app_id is the process ID of the Blender instance, so BlenderKit-client can return reports to the correct instance.
|
|
- api_key is the authentication token for the BlenderKit server, so BlenderKit-Client can authenticate the user.
|
|
- addon_version is the version of the BlenderKit add-on, so BlenderKit-client has understanding of the version of the add-on making the request.
|
|
"""
|
|
if data is None:
|
|
data = {}
|
|
|
|
av = global_vars.VERSION
|
|
addon_version = f"{av[0]}.{av[1]}.{av[2]}.{av[3]}"
|
|
if "api_key" not in data:
|
|
# for BG instances, where preferences are not available
|
|
data.setdefault(
|
|
"api_key", bpy.context.preferences.addons[__package__].preferences.api_key # type: ignore
|
|
)
|
|
data.setdefault("app_id", os.getpid())
|
|
data.setdefault("platform_version", platform.platform())
|
|
data.setdefault("addon_version", addon_version)
|
|
|
|
return data
|
|
|
|
|
|
def ensure_minimal_data_class(data_class):
|
|
"""Ensure that the data send to the BlenderKit-Client contains:
|
|
- app_id is the process ID of the Blender instance, so BlenderKit-client can return reports to the correct instance.
|
|
- api_key is the authentication token for the BlenderKit server, so BlenderKit-Client can authenticate the user.
|
|
- addon_version is the version of the BlenderKit add-on, so BlenderKit-client has understanding of the version of the add-on making the request.
|
|
"""
|
|
if data_class == None:
|
|
data_class = dataclasses.dataclass()
|
|
|
|
av = global_vars.VERSION
|
|
if hasattr(data_class, "api_key"):
|
|
# for BG instances, where preferences are not available
|
|
api_key = bpy.context.preferences.addons[__package__].preferences.api_key
|
|
setattr(data_class, "api_key", api_key)
|
|
setattr(data_class, "app_id", os.getpid())
|
|
setattr(data_class, "platform_version", platform.platform())
|
|
setattr(data_class, "addon_version", f"{av[0]}.{av[1]}.{av[2]}.{av[3]}")
|
|
return data_class
|
|
|
|
|
|
def reorder_ports(port: str = ""):
|
|
"""Reorder CLIENT_PORTS so the specified port is first.
|
|
If no port is specified, the current first port is moved to back so second becomes the first.
|
|
"""
|
|
if port == "":
|
|
i = 1
|
|
else:
|
|
i = global_vars.CLIENT_PORTS.index(port)
|
|
global_vars.CLIENT_PORTS = (
|
|
global_vars.CLIENT_PORTS[i:] + global_vars.CLIENT_PORTS[:i]
|
|
)
|
|
bk_logger.info(
|
|
f"Ports reordered so first port is now {global_vars.CLIENT_PORTS[0]} (previous index was {i})"
|
|
)
|
|
|
|
|
|
def get_reports(app_id: str):
|
|
"""Get reports for all tasks of app_id Blender instance at once.
|
|
If few last calls failed, then try to get reports also from other than default ports.
|
|
"""
|
|
data = ensure_minimal_data({"app_id": app_id})
|
|
data["project_name"] = utils.get_project_name()
|
|
data["blender_version"] = utils.get_blender_version()
|
|
|
|
# on 10, there is second BlenderKit-Client start
|
|
if global_vars.CLIENT_FAILED_REPORTS < 10:
|
|
url = f"{get_base_url()}/report"
|
|
return request_report(url, data)
|
|
|
|
last_exception = None
|
|
for port in global_vars.CLIENT_PORTS:
|
|
vapi = get_api_version()
|
|
url = f"http://127.0.0.1:{port}/{vapi}/report"
|
|
try:
|
|
report = request_report(url, data)
|
|
bk_logger.warning(
|
|
f"Got reports from BlenderKit-Client on port {port}, setting it as default for this instance"
|
|
)
|
|
reorder_ports(port)
|
|
return report
|
|
except Exception as e:
|
|
bk_logger.info(f"Failed to get BlenderKit-Client reports: {e}")
|
|
last_exception = e
|
|
if last_exception is not None:
|
|
raise last_exception
|
|
|
|
|
|
def request_report(url: str, data: dict) -> dict:
|
|
"""Make HTTP request to /report endpoint. If all goes well a JSON dict is returned.
|
|
If something goes south, this function raises requests.HTTPError or requests.JSONDecodeError.
|
|
"""
|
|
with requests.Session() as session:
|
|
resp = session.get(url, json=data, timeout=TIMEOUT, proxies=NO_PROXIES)
|
|
if resp.status_code != 200:
|
|
# not using resp.raise_for_status() for better message
|
|
raise requests.HTTPError(
|
|
f"{http_responses[resp.status_code]}: {resp.text}", response=resp
|
|
)
|
|
return resp.json()
|
|
|
|
|
|
### ASSETS
|
|
# SEARCH
|
|
def asset_search(search_data: datas.SearchData):
|
|
"""Search for specified asset."""
|
|
bk_logger.info(f"Starting search request: {search_data.urlquery}")
|
|
|
|
search_data = ensure_minimal_data_class(search_data)
|
|
with requests.Session() as session:
|
|
url = get_base_url() + "/blender/asset_search"
|
|
resp = session.post(
|
|
url, json=datas.asdict(search_data), timeout=TIMEOUT, proxies=NO_PROXIES
|
|
)
|
|
bk_logger.debug("Got search response")
|
|
return resp.json()
|
|
|
|
|
|
# DOWNLOAD
|
|
def asset_download(data):
|
|
"""Download specified asset."""
|
|
data = ensure_minimal_data(data)
|
|
with requests.Session() as session:
|
|
url = get_base_url() + "/blender/asset_download"
|
|
resp = session.post(url, json=data, timeout=TIMEOUT, proxies=NO_PROXIES)
|
|
return resp.json()
|
|
|
|
|
|
def cancel_download(task_id: str):
|
|
"""Cancel the specified task with ID on the BlenderKit-Client."""
|
|
data = ensure_minimal_data({"task_id": task_id})
|
|
with requests.Session() as session:
|
|
url = get_base_url() + "/blender/cancel_download"
|
|
resp = session.get(url, json=data, timeout=TIMEOUT, proxies=NO_PROXIES)
|
|
return resp
|
|
|
|
|
|
# UPLOAD
|
|
def asset_upload(upload_data, export_data, upload_set):
|
|
"""Upload specified asset."""
|
|
data = {
|
|
"PREFS": utils.get_preferences_as_dict(),
|
|
"upload_data": upload_data,
|
|
"export_data": export_data,
|
|
"upload_set": upload_set,
|
|
}
|
|
data = ensure_minimal_data(data)
|
|
with requests.Session() as session:
|
|
url = get_base_url() + "/blender/asset_upload"
|
|
bk_logger.debug(f"making a request to: {url}")
|
|
resp = session.post(url, json=data, timeout=TIMEOUT, proxies=NO_PROXIES)
|
|
return resp
|
|
|
|
|
|
### PROFILES
|
|
def download_gravatar_image(author_data: datas.UserProfile) -> requests.Response:
|
|
"""Fetch gravatar image for specified user. Find it on disk or download it from server."""
|
|
data = {
|
|
"id": author_data.id,
|
|
"avatar128": author_data.avatar128,
|
|
"gravatarHash": author_data.gravatarHash,
|
|
}
|
|
data = ensure_minimal_data(data)
|
|
with requests.Session() as session:
|
|
url = get_base_url() + "/profiles/download_gravatar_image"
|
|
resp = session.get(url, json=data, timeout=TIMEOUT, proxies=NO_PROXIES)
|
|
return resp
|
|
|
|
|
|
def get_user_profile() -> requests.Response:
|
|
"""Fetch profile of currently logged-in user.
|
|
This creates task on BlenderKit-Client to fetch data which are later handled once available.
|
|
"""
|
|
data = ensure_minimal_data()
|
|
with requests.Session() as session:
|
|
return session.get(
|
|
f"{get_base_url()}/profiles/get_user_profile",
|
|
json=data,
|
|
timeout=TIMEOUT,
|
|
proxies=NO_PROXIES,
|
|
)
|
|
|
|
|
|
### COMMENTS
|
|
def get_comments(asset_id, api_key=""):
|
|
"""Get all comments on the asset."""
|
|
data = ensure_minimal_data({"asset_id": asset_id})
|
|
with requests.Session() as session:
|
|
return session.post(
|
|
f"{get_base_url()}/comments/get_comments",
|
|
json=data,
|
|
timeout=TIMEOUT,
|
|
proxies=NO_PROXIES,
|
|
)
|
|
|
|
|
|
def create_comment(asset_id, comment_text, api_key, reply_to_id=0):
|
|
"""Create a new comment."""
|
|
data = {
|
|
"asset_id": asset_id,
|
|
"comment_text": comment_text,
|
|
"reply_to_id": reply_to_id,
|
|
}
|
|
data = ensure_minimal_data(data)
|
|
with requests.Session() as session:
|
|
return session.post(
|
|
f"{get_base_url()}/comments/create_comment",
|
|
json=data,
|
|
timeout=TIMEOUT,
|
|
proxies=NO_PROXIES,
|
|
)
|
|
|
|
|
|
def feedback_comment(asset_id, comment_id, api_key, flag="like"):
|
|
"""Feedback the comment - by default with like. Other flags can be used also."""
|
|
data = {
|
|
"asset_id": asset_id,
|
|
"comment_id": comment_id,
|
|
"flag": flag,
|
|
}
|
|
data = ensure_minimal_data(data)
|
|
with requests.Session() as session:
|
|
return session.post(
|
|
f"{get_base_url()}/comments/feedback_comment",
|
|
json=data,
|
|
timeout=TIMEOUT,
|
|
proxies=NO_PROXIES,
|
|
)
|
|
|
|
|
|
def mark_comment_private(asset_id, comment_id, api_key, is_private=False):
|
|
"""Mark the comment as private or public."""
|
|
data = {
|
|
"asset_id": asset_id,
|
|
"comment_id": comment_id,
|
|
"is_private": is_private,
|
|
}
|
|
data = ensure_minimal_data(data)
|
|
with requests.Session() as session:
|
|
return session.post(
|
|
f"{get_base_url()}/comments/mark_comment_private",
|
|
json=data,
|
|
timeout=TIMEOUT,
|
|
proxies=NO_PROXIES,
|
|
)
|
|
|
|
|
|
### NOTIFICATIONS
|
|
def mark_notification_read(notification_id):
|
|
"""Mark the notification as read on the server."""
|
|
data = ensure_minimal_data({"notification_id": notification_id})
|
|
with requests.Session() as session:
|
|
return session.post(
|
|
f"{get_base_url()}/notifications/mark_notification_read",
|
|
json=data,
|
|
timeout=TIMEOUT,
|
|
proxies=NO_PROXIES,
|
|
)
|
|
|
|
|
|
### REPORTS
|
|
def report_usages(data: dict):
|
|
"""Report usages of assets in current scene via BlenderKit-Client to the server."""
|
|
data = ensure_minimal_data(data)
|
|
with requests.Session() as session:
|
|
return session.post(
|
|
f"{get_base_url()}/report_usages",
|
|
json=data,
|
|
timeout=TIMEOUT,
|
|
proxies=NO_PROXIES,
|
|
)
|
|
|
|
|
|
# RATINGS
|
|
def get_rating(asset_id: str):
|
|
data = ensure_minimal_data({"asset_id": asset_id})
|
|
with requests.Session() as session:
|
|
return session.get(
|
|
f"{get_base_url()}/ratings/get_rating",
|
|
json=data,
|
|
timeout=TIMEOUT,
|
|
proxies=NO_PROXIES,
|
|
)
|
|
|
|
|
|
def send_rating(asset_id: str, rating_type: str, rating_value: str):
|
|
data = {
|
|
"asset_id": asset_id,
|
|
"rating_type": rating_type,
|
|
"rating_value": rating_value,
|
|
}
|
|
data = ensure_minimal_data(data)
|
|
with requests.Session() as session:
|
|
return session.post(
|
|
f"{get_base_url()}/ratings/send_rating",
|
|
json=data,
|
|
timeout=TIMEOUT,
|
|
proxies=NO_PROXIES,
|
|
)
|
|
|
|
|
|
# BOOKMARKS
|
|
def get_bookmarks():
|
|
data = ensure_minimal_data()
|
|
with requests.Session() as session:
|
|
return session.get(
|
|
f"{get_base_url()}/ratings/get_bookmarks",
|
|
json=data,
|
|
timeout=TIMEOUT,
|
|
proxies=NO_PROXIES,
|
|
)
|
|
|
|
|
|
### BLOCKING WRAPPERS
|
|
def get_download_url(asset_data, scene_id, api_key):
|
|
"""Get download url from server. This is a blocking wrapper, will not return until results are available.
|
|
Returns: (bool, str, str) - can_download, download_url, filename.
|
|
"""
|
|
data = {
|
|
"resolution": "blend",
|
|
"asset_data": asset_data,
|
|
"api_key": api_key, # needs to be here, because prefs are not available in BG instances
|
|
"PREFS": {
|
|
"api_key": api_key,
|
|
"scene_id": scene_id,
|
|
},
|
|
}
|
|
data = ensure_minimal_data(data)
|
|
with requests.Session() as session:
|
|
resp = session.get(
|
|
f"{get_base_url()}/wrappers/get_download_url",
|
|
json=data,
|
|
timeout=TIMEOUT,
|
|
proxies=NO_PROXIES,
|
|
)
|
|
resp = resp.json()
|
|
return (resp["can_download"], resp["download_url"], resp["filename"])
|
|
|
|
|
|
def complete_upload_file_blocking(
|
|
api_key, asset_id, filepath, filetype: str, fileindex: int
|
|
) -> bool:
|
|
"""Complete file upload in just one step, blocks until upload is finished. Useful for background scripts."""
|
|
data = {
|
|
"api_key": api_key,
|
|
"assetId": asset_id,
|
|
"fileType": filetype,
|
|
"fileIndex": fileindex,
|
|
"filePath": filepath,
|
|
"originalFilename": os.path.basename(filepath), # teoreticky asi nemusi byt
|
|
}
|
|
data = ensure_minimal_data(data)
|
|
with requests.Session() as session:
|
|
resp = session.get(
|
|
f"{get_base_url()}/wrappers/complete_upload_file_blocking",
|
|
json=data,
|
|
timeout=(1, 600),
|
|
proxies=NO_PROXIES,
|
|
)
|
|
|
|
print("complete_upload_file_blocking resp:", resp)
|
|
return resp.ok
|
|
|
|
|
|
def blocking_file_download(url: str, filepath: str, api_key: str) -> requests.Response:
|
|
"""Upload file to server. This is a blocking wrapper, will not return until results are available."""
|
|
data = {
|
|
"url": url,
|
|
"filepath": filepath,
|
|
}
|
|
data = ensure_minimal_data(data)
|
|
with requests.Session() as session:
|
|
return session.get(
|
|
f"{get_base_url()}/wrappers/blocking_file_download",
|
|
json=data,
|
|
timeout=(1, 600),
|
|
proxies=NO_PROXIES,
|
|
)
|
|
|
|
|
|
def blocking_request(
|
|
url: str,
|
|
method: str = "GET",
|
|
headers: Optional[dict] = None,
|
|
json_data: Optional[dict] = None,
|
|
timeout: tuple = TIMEOUT,
|
|
) -> requests.Response:
|
|
"""Make blocking HTTP request through BlenderKit-Client.
|
|
Will not return until results are available."""
|
|
if headers is None:
|
|
headers = {}
|
|
data = {
|
|
"url": url,
|
|
"method": method,
|
|
"headers": headers,
|
|
}
|
|
if json_data is not None:
|
|
data["json"] = json_data
|
|
with requests.Session() as session:
|
|
return session.get(
|
|
f"{get_base_url()}/wrappers/blocking_request",
|
|
json=data,
|
|
timeout=timeout,
|
|
proxies=NO_PROXIES,
|
|
)
|
|
|
|
|
|
### REQUEST WRAPPERS
|
|
def nonblocking_request(
|
|
url: str,
|
|
method: str,
|
|
headers: Optional[dict] = None,
|
|
json_data: Optional[dict] = None,
|
|
messages: Optional[dict] = None,
|
|
) -> requests.Response:
|
|
"""Make non-blocking HTTP request through BlenderKit-Client.
|
|
This function will return ASAP, not returning any actual data.
|
|
"""
|
|
if headers is None:
|
|
headers = {}
|
|
if messages is None:
|
|
messages = {}
|
|
data = {
|
|
"url": url,
|
|
"method": method,
|
|
"headers": headers,
|
|
"messages": messages,
|
|
}
|
|
data = ensure_minimal_data(data)
|
|
if json_data is not None:
|
|
data["json"] = json_data
|
|
with requests.Session() as session:
|
|
return session.get(
|
|
f"{get_base_url()}/wrappers/nonblocking_request",
|
|
json=data,
|
|
timeout=TIMEOUT,
|
|
proxies=NO_PROXIES,
|
|
)
|
|
|
|
|
|
### AUTHORIZATION
|
|
def send_oauth_verification_data(code_verifier, state: str):
|
|
"""Send OAUTH2 Code Verifier and State parameters to BlenderKit-Client.
|
|
So it can later use them to authenticate the redirected response from the browser.
|
|
"""
|
|
data = ensure_minimal_data(
|
|
{
|
|
"code_verifier": code_verifier,
|
|
"state": state,
|
|
}
|
|
)
|
|
with requests.Session() as session:
|
|
resp = session.post(
|
|
f"{get_base_url()}/oauth2/verification_data",
|
|
json=data,
|
|
timeout=TIMEOUT,
|
|
proxies=NO_PROXIES,
|
|
)
|
|
return resp
|
|
|
|
|
|
def refresh_token(refresh_token, old_api_key):
|
|
"""Refresh authentication token. BlenderKit-Client will use refresh token to get new API key token to replace the old_api_key.
|
|
old_api_key is used later to replace token only in Blender instances with the same api_key. (User can be logged into multiple accounts.)
|
|
"""
|
|
bk_logger.info("Calling API token refresh")
|
|
data = ensure_minimal_data({"refresh_token": refresh_token})
|
|
with requests.Session() as session:
|
|
url = get_base_url() + "/refresh_token"
|
|
resp = session.get(
|
|
url,
|
|
json=data,
|
|
timeout=TIMEOUT,
|
|
proxies=NO_PROXIES,
|
|
)
|
|
return resp
|
|
|
|
|
|
def oauth2_logout():
|
|
"""Logout from OAUTH2. BlenderKit-Client will revoke the token on the server."""
|
|
data = ensure_minimal_data()
|
|
data["refresh_token"] = global_vars.PREFS["api_key_refresh"]
|
|
with requests.Session() as session:
|
|
url = get_base_url() + "/oauth2/logout"
|
|
resp = session.get(url, json=data, timeout=TIMEOUT, proxies=NO_PROXIES)
|
|
return resp
|
|
|
|
|
|
def unsubscribe_addon():
|
|
"""Unsubscribe the add-on from the BlenderKit-Client. Called when the add-on is disabled, uninstalled or when Blender is closed."""
|
|
data = ensure_minimal_data()
|
|
with requests.Session() as session:
|
|
url = get_base_url() + "/blender/unsubscribe_addon"
|
|
resp = session.get(url, json=data, timeout=TIMEOUT, proxies=NO_PROXIES)
|
|
return resp
|
|
|
|
|
|
def shutdown_client():
|
|
"""Request to shutdown the BlenderKit-Client."""
|
|
data = ensure_minimal_data()
|
|
with requests.Session() as session:
|
|
url = get_base_url() + "/shutdown"
|
|
resp = session.get(url, data=data, timeout=TIMEOUT, proxies=NO_PROXIES)
|
|
return resp
|
|
|
|
|
|
def handle_client_status_task(task):
|
|
if global_vars.CLIENT_RUNNING is False:
|
|
wm = bpy.context.window_manager
|
|
wm.blenderkitUI.logo_status = "logo"
|
|
global_vars.CLIENT_RUNNING = True
|
|
|
|
|
|
def check_blenderkit_client_return_code() -> tuple[int, str]:
|
|
"""Check the return code for the started BlenderKit-Client. If the return code returned from process.poll() is None - returned by this func as -1, it means Client still runs - we consider this a success!
|
|
However if the return code from poll() is present, it failed to start and we check the return code value. If the return code is known,
|
|
we print information to user about the reason. So they do not need to dig in the Client log.
|
|
"""
|
|
# Return codes - as defined in main.go
|
|
rcServerStartOtherError = 40
|
|
rcServerStartOtherNetworkingError = 41
|
|
rcServerStartOtherSyscallError = 42
|
|
rcServerStartSyscallEADDRINUSE = 43
|
|
rcServerStartSyscallEACCES = 44
|
|
if global_vars.client_process is None:
|
|
return -2, "Unexpectedly global_vars.client_process is None"
|
|
|
|
exit_code = global_vars.client_process.poll()
|
|
if exit_code is None:
|
|
return -1, "BlenderKit-Client process is running."
|
|
|
|
# need to initialize msg, was throwing an error
|
|
msg = f"Unknown error."
|
|
if exit_code == rcServerStartOtherError:
|
|
msg = f"Other starting problem."
|
|
if exit_code == rcServerStartOtherNetworkingError:
|
|
msg = f"Other networking problem."
|
|
if exit_code == rcServerStartOtherSyscallError:
|
|
msg = f"Other syscall error."
|
|
|
|
if exit_code == rcServerStartSyscallEADDRINUSE: # This is known solution
|
|
return (
|
|
exit_code,
|
|
"Address already in use: please change the port in add-on preferences.",
|
|
)
|
|
if exit_code == rcServerStartSyscallEACCES: # This needs verification
|
|
return (
|
|
exit_code,
|
|
"Access denied: change port in preferences, check permissions and antivirus rights.",
|
|
)
|
|
|
|
message = (
|
|
f"{msg} Please report a bug and paste content of log {get_client_log_path()}"
|
|
)
|
|
return exit_code, message
|
|
|
|
|
|
def start_blenderkit_client():
|
|
"""Start BlenderKit-client in separate process.
|
|
1. Check if binary is available at global_dir/client/vX.Y.Z/blenderkit-client-<os>-<arch>(.exe)
|
|
2. Copy the binary from add-on directory to global_dir/client/vX.Y.Z/
|
|
3. Start the BlenderKit-Client process which serves as bridge between BlenderKit add-on and BlenderKit server.
|
|
"""
|
|
ensure_client_binary_installed()
|
|
log_path = get_client_log_path()
|
|
client_binary_path, client_version = get_client_binary_path()
|
|
|
|
creation_flags = 0
|
|
if platform.system() == "Windows":
|
|
creation_flags = subprocess.CREATE_NO_WINDOW
|
|
|
|
try:
|
|
with open(log_path, "wb") as log:
|
|
global_vars.client_process = subprocess.Popen(
|
|
args=[
|
|
client_binary_path,
|
|
"--port",
|
|
get_port(),
|
|
"--server",
|
|
global_vars.SERVER,
|
|
"--proxy_which",
|
|
global_vars.PREFS.get("proxy_which", ""),
|
|
"--proxy_address",
|
|
global_vars.PREFS.get("proxy_address", ""),
|
|
"--trusted_ca_certs",
|
|
global_vars.PREFS.get("trusted_ca_certs", ""),
|
|
"--ssl_context",
|
|
global_vars.PREFS.get("ssl_context", ""),
|
|
"--version",
|
|
f"{global_vars.VERSION[0]}.{global_vars.VERSION[1]}.{global_vars.VERSION[2]}.{global_vars.VERSION[3]}",
|
|
"--software",
|
|
"Blender",
|
|
"--pid",
|
|
str(os.getpid()),
|
|
],
|
|
stdout=log,
|
|
stderr=log,
|
|
creationflags=creation_flags,
|
|
)
|
|
except Exception as e:
|
|
msg = f"Error: BlenderKit-Client {client_version} failed to start on {get_address()}:{e}"
|
|
reports.add_report(msg, type="ERROR")
|
|
raise (e)
|
|
|
|
bk_logger.info(f"BlenderKit-Client {client_version} starting on {get_address()}")
|
|
|
|
|
|
def decide_client_binary_name() -> str:
|
|
"""Decide the name of the BlenderKit-Client binary based on the current operating system and architecture.
|
|
We unify the OS and CPU architecture naming to make it more accessible for general public.
|
|
Darwin is renamed to MacOS. The CPU architecture is aligned to x86_64 or arm64.
|
|
Possible return values:
|
|
- blenderkit-client-windows-x86_64.exe
|
|
- blenderkit-client-windows-arm64.exe
|
|
- blenderkit-client-linux-x86_64
|
|
- blenderkit-client-linux-arm64
|
|
- blenderkit-client-macos-x86_64
|
|
- blenderkit-client-macos-arm64
|
|
"""
|
|
os_name = platform.system().lower()
|
|
if os_name == "darwin": # more user-friendly name for macOS
|
|
os_name = "macos"
|
|
|
|
architecture = platform.machine().lower()
|
|
if architecture == "amd64": # align Windows convention
|
|
architecture = "x86_64"
|
|
elif architecture == "aarch64": # align Linux convention
|
|
architecture = "arm64"
|
|
|
|
if os_name == "windows":
|
|
return f"blenderkit-client-{os_name}-{architecture}.exe".lower()
|
|
|
|
return f"blenderkit-client-{os_name}-{architecture}".lower()
|
|
|
|
|
|
def get_client_directory() -> str:
|
|
"""Get the path to the BlenderKit-Client directory located in global_dir."""
|
|
global_dir = bpy.context.preferences.addons[__package__].preferences.global_dir # type: ignore
|
|
directory = path.join(global_dir, "client")
|
|
return directory
|
|
|
|
|
|
def get_client_log_path() -> str:
|
|
"""Get path to BlenderKit-Client log file in global_dir/client.
|
|
If the port is the default port 62485, the log file is named default.log,
|
|
otherwise it is named client-<port>.log.
|
|
"""
|
|
port = get_port()
|
|
if port == "62485":
|
|
log_path = os.path.join(get_client_directory(), f"default.log")
|
|
else:
|
|
log_path = os.path.join(get_client_directory(), f"client-{get_port()}.log")
|
|
return path.abspath(log_path)
|
|
|
|
|
|
def get_preinstalled_client_path() -> str:
|
|
"""Get the path to the preinstalled BlenderKit-Client binary - located in add-on directory.
|
|
This is the binary that is shipped with the add-on. It is copied to global_dir/client/vX.Y.Z on first run.
|
|
"""
|
|
addon_dir = path.dirname(__file__)
|
|
binary_name = decide_client_binary_name()
|
|
binary_path = path.join(
|
|
addon_dir, "client", global_vars.CLIENT_VERSION, binary_name
|
|
)
|
|
return path.abspath(binary_path)
|
|
|
|
|
|
def get_client_binary_path():
|
|
"""Get the path to the BlenderKit-Client binary located in global_dir/client/bin/vX.Y.Z.
|
|
This is the binary that is used to start the client process.
|
|
We do not start from the add-on because it might block update or delete of the add-on.
|
|
Returns: (str, str) - path to the Client binary, version of the Client binary
|
|
"""
|
|
client_dir = get_client_directory()
|
|
binary_name = decide_client_binary_name()
|
|
ver_string = global_vars.CLIENT_VERSION
|
|
binary_path = path.join(client_dir, "bin", ver_string, binary_name)
|
|
return path.abspath(binary_path), ver_string
|
|
|
|
|
|
def ensure_client_binary_installed():
|
|
"""Ensure that the BlenderKit-Client binary is installed in global_dir/client/bin/vX.Y.Z.
|
|
If not, copy the binary from the add-on directory blenderkit/client.
|
|
As side effect, this function also creates the global_dir/client/bin/vX.Y.Z directory.
|
|
"""
|
|
client_binary_path, _ = get_client_binary_path()
|
|
if path.exists(client_binary_path):
|
|
return
|
|
|
|
preinstalled_client_path = get_preinstalled_client_path()
|
|
bk_logger.info(f"Copying BlenderKit-Client binary {preinstalled_client_path}")
|
|
os.makedirs(path.dirname(client_binary_path), exist_ok=True)
|
|
shutil.copy(preinstalled_client_path, client_binary_path)
|
|
os.chmod(client_binary_path, 0o711)
|
|
bk_logger.info(f"BlenderKit-Client binary copied to {client_binary_path}")
|
|
|
|
|
|
def get_addon_dir():
|
|
"""Get the path to the add-on directory."""
|
|
addon_dir = path.dirname(__file__)
|
|
return addon_dir
|