Files
blender-portable-repo/extensions/user_default/blenderkit/ratings_utils.py
T
2026-03-17 14:30:01 -06:00

432 lines
15 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 logging
from typing import Optional, Union
# mainly update functions and callbacks for ratings properties, here to avoid circular imports.
import bpy
from bpy.props import (
BoolProperty,
EnumProperty,
FloatProperty,
IntProperty,
StringProperty,
)
from bpy.types import Context, PropertyGroup
from . import (
client_lib,
client_tasks,
datas,
global_vars,
icons,
reports,
tasks_queue,
utils,
)
bk_logger = logging.getLogger(__name__)
def handle_get_rating_task(task: client_tasks.Task):
"""Handle incomming get_rating task by saving the results into global_vars."""
if task.status == "created":
return
if task.status == "error":
return bk_logger.warning(f"{task.task_type} task failed: {task.message}")
asset_id = task.data["asset_id"]
ratings = task.result["results"]
if len(ratings) == 0:
store_rating_local(asset_id, "quality", None)
store_rating_local(asset_id, "working_hours", None)
return
for rating in ratings:
store_rating_local(asset_id, rating["ratingType"], rating["score"])
def handle_get_ratings_task(task: client_tasks.Task):
"""Handle incomming get_ratings task. This is a special task used only by validators which fetches the ratings
in big batch right after the search results come into the Client. This is used only to signal problems in the
Goroutine which fetches the ratings. The individual ratings are then sent as normal 'get_rating' tasks.
"""
if task.status == "error": # only reason this task type exists right now
return bk_logger.warning(f"{task.task_type} task failed: {task.message}")
def handle_get_bookmarks_task(task: client_tasks.Task):
"""Handle incomming get_bookmarks task by saving the results into global_vars.
This is different from standard ratings - the results come from elastic search API
instead of ratings API.
"""
if task.status == "created":
return
if task.status == "error":
bk_logger.warning(f"Could not load bookmarks: {task.message}")
return
for asset in task.result["results"]:
store_rating_local(asset["id"], "bookmarks", 1)
bk_logger.info("Bookmarks loaded")
def handle_send_rating_task(task: client_tasks.Task):
"""Handle send rating task."""
if task.status == "created":
return
if task.status == "error":
return reports.add_report(
task.message, type="ERROR", details=task.message_detailed
)
if task.status == "finished":
if utils.profile_is_validator():
return reports.add_report(task.message, type="VALIDATOR")
def store_rating_local(
asset_id: str, rating_type: str = "quality", value: Optional[int] = None
):
"""Store the rating locally in the global_vars.
- rating_type can be: "quality", "working_hours", "bookmarks"
- value set None to create empty rating and prevent add-on from fetching it again next time
"""
allowed_rating_types = ["quality", "working_hours", "bookmarks"]
if rating_type not in allowed_rating_types:
raise ValueError(f"rating_type must be one of {allowed_rating_types}")
rating = global_vars.RATINGS.get(asset_id, datas.AssetRating())
rating.working_hours_fetched = True
rating.quality_fetched = True
setattr(rating, rating_type, value)
global_vars.RATINGS[asset_id] = rating
def get_rating_local(asset_id: str) -> Optional[datas.AssetRating]:
"""Get the rating locally from global_vars.RATINGS."""
return global_vars.RATINGS.get(asset_id)
def ensure_rating(asset_id: str):
"""Ensure the rating is available. First check if it is available in local cache. If it is not then download it from the server.
If the rating is present, we need to check if rating.quality_fetched and rating.working_hours_fetched are not False
because bookmarked assets will have rating created, but for them the quality and wh was not fetched (bookmarked are get from search
and these data does not contain quality and working_hours - and even bookmarked but that can be deduced from searching for bookmarked).
"""
rating = get_rating_local(asset_id)
if rating is None:
client_lib.get_rating(asset_id)
return
if not rating.quality_fetched or rating.working_hours_fetched:
client_lib.get_rating(asset_id)
def update_ratings_quality(self, context: Context):
if not (hasattr(self, "rating_quality")):
# first option is for rating of assets that are from scene
asset = self.id_data
bkit_ratings = asset.bkit_ratings
asset_id = asset["asset_data"]["id"]
else:
# this part is for operator rating:
bkit_ratings = self
asset_id = self.asset_id
local_rating = get_rating_local(self.asset_id)
if local_rating is None:
local_rating = datas.AssetRating(quality=0)
if local_rating.quality == self.rating_quality:
return store_rating_local(
asset_id, rating_type="quality", value=bkit_ratings.rating_quality
)
store_rating_local(
asset_id, rating_type="quality", value=bkit_ratings.rating_quality
)
if self.rating_quality_lock is True:
return
args = (asset_id, "quality", bkit_ratings.rating_quality)
tasks_queue.add_task((client_lib.send_rating, args), wait=0.5, only_last=True)
def update_ratings_work_hours(self, context: Context):
if not (hasattr(self, "rating_work_hours")):
# first option is for rating of assets that are from scene
asset = self.id_data
bkit_ratings = asset.bkit_ratings
asset_id = asset["asset_data"]["id"]
else:
# this part is for operator rating:
bkit_ratings = self
asset_id = self.asset_id
local_rating = get_rating_local(self.asset_id)
if local_rating is None: # rating was not available online
local_rating = datas.AssetRating(working_hours=0)
if local_rating.working_hours == self.rating_work_hours:
return store_rating_local(
asset_id, rating_type="working_hours", value=bkit_ratings.rating_work_hours
)
store_rating_local(
asset_id, rating_type="working_hours", value=bkit_ratings.rating_work_hours
)
if self.rating_work_hours_lock is True:
return
args = (asset_id, "working_hours", bkit_ratings.rating_work_hours)
tasks_queue.add_task((client_lib.send_rating, args), wait=0.5, only_last=True)
def update_quality_ui(self, context: Context):
"""Converts the _ui the enum into actual quality number."""
user_preferences = bpy.context.preferences.addons[__package__].preferences # type: ignore
api_key = user_preferences.api_key # type: ignore
# we need to check for matching value not to update twice/call the popup twice.
if api_key == "" and self.rating_quality != int(self.rating_quality_ui):
bpy.ops.wm.blenderkit_login( # type: ignore
"INVOKE_DEFAULT",
message="Please login/signup to rate assets. Clicking OK takes you to web login.",
)
return
self.rating_quality = int(self.rating_quality_ui)
def update_ratings_work_hours_ui(self, context: Context):
user_preferences = bpy.context.preferences.addons[__package__].preferences # type: ignore
api_key = user_preferences.api_key # type: ignore
if api_key == "" and self.rating_work_hours != float(self.rating_work_hours_ui):
bpy.ops.wm.blenderkit_login( # type: ignore
"INVOKE_DEFAULT",
message="Please login/signup to rate assets. Clicking OK takes you to web login.",
)
return
self.rating_work_hours = float(self.rating_work_hours_ui)
def stars_enum_callback(self, context):
"""regenerates the enum property used to display rating stars, so that there are filled/empty stars correctly."""
items = []
for a in range(0, 11):
if a == 0:
icon = "REMOVE"
elif self.rating_quality < a:
icon = "SOLO_OFF"
else:
icon = "SOLO_ON"
# has to have something before the number in the value, otherwise fails on registration.
items.append((f"{a}", " ", "", icon, a))
return items
def wh_enum_callback(self, context):
"""Regenerates working hours enum."""
if self.asset_type in ("model", "scene", "printable", "nodegroup"):
possible_wh_values = [
0,
0.5,
1,
2,
3,
4,
5,
6,
8,
10,
15,
20,
30,
50,
100,
150,
200,
250,
]
elif self.asset_type == "hdr":
possible_wh_values = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
else: # for material, brush assets
possible_wh_values = [0, 0.2, 0.5, 1, 2, 3, 4, 5]
work_hours = self.rating_work_hours
if work_hours < 1:
work_hours = int(work_hours * 10) / 10
else:
work_hours = int(work_hours)
if work_hours not in possible_wh_values:
closest_index = 0
closest_diff = abs(possible_wh_values[0] - work_hours)
for i in range(1, len(possible_wh_values)):
diff = abs(possible_wh_values[i] - work_hours)
if diff < closest_diff:
closest_diff = diff
closest_index = i
possible_wh_values[closest_index] = work_hours
items = []
items.append(("0", " ", "", "REMOVE", 0))
pcoll = icons.icon_collections["main"]
for w in possible_wh_values:
if w > 0:
if w < 1:
icon_name = f"BK{int(w*10)/10}"
else:
icon_name = f"BK{int(w)}"
if icon_name not in pcoll:
icon_name = "bar_slider_up"
icon = pcoll[icon_name]
# index of the item(last value) is multiplied by 10 to get always integer values that aren't zero
items.append((f"{w}", " ", "", icon.icon_id, int(w * 10)))
return items
class RatingProperties(PropertyGroup):
message: StringProperty( # type: ignore
name="message",
description="message",
default="Rating asset",
options={"SKIP_SAVE"},
)
asset_id: StringProperty( # type: ignore
name="Asset Base Id",
description="Unique id of the asset (hidden)",
default="",
options={"SKIP_SAVE"},
)
asset_name: StringProperty( # type: ignore
name="Asset Name",
description="Name of the asset (hidden)",
default="",
options={"SKIP_SAVE"},
)
asset_type: StringProperty( # type: ignore
name="Asset type", description="asset type", default="", options={"SKIP_SAVE"}
)
### QUALITY RATING
rating_quality_lock: BoolProperty( # type: ignore
name="Quality Lock",
description="Quality is locked -> rating is not sent online",
default=False,
options={"SKIP_SAVE"},
)
rating_quality: IntProperty( # type: ignore
name="Quality",
description="quality of the material",
default=0,
min=-1,
max=10,
update=update_ratings_quality,
options={"SKIP_SAVE"},
)
# the following enum is only to ease interaction - enums support 'drag over' and enable to draw the stars easily.
rating_quality_ui: EnumProperty( # type: ignore
name="Quality",
items=stars_enum_callback,
description="Rate the quality of the asset from 1 to 10 stars.\nShortcut: Hover over asset in the asset bar and press 'R' to show rating menu",
default=0,
update=update_quality_ui,
options={"SKIP_SAVE"},
)
### WORK HOURS RATING
rating_work_hours_lock: BoolProperty( # type: ignore
name="Work Hours Lock",
description="Work hours are locked -> rating is not sent online",
default=False,
options={"SKIP_SAVE"},
)
rating_work_hours: FloatProperty( # type: ignore
name="Work Hours",
description="nonUI How many hours did this work take?\nShortcut: Hover over asset in the asset bar and press 'R' to show rating menu.",
default=0.00,
min=0.0,
max=300,
update=update_ratings_work_hours,
options={"SKIP_SAVE"},
)
rating_work_hours_ui: EnumProperty( # type: ignore
name="Work Hours",
description="UI How many hours did this work take?\nShortcut: Hover over asset in the asset bar and press 'R' to show rating menu",
items=wh_enum_callback,
default=0,
update=update_ratings_work_hours_ui,
options={"SKIP_SAVE"},
)
def prefill_ratings(self) -> None:
"""Pre-fill the quality and work hours ratings if available.
Locks the ratings locks so that the update function is not called and ratings are not sent online.
"""
if not utils.user_logged_in():
return
rating = get_rating_local(self.asset_id)
if rating is None:
return
if rating.quality is None and rating.working_hours is None:
return
if self.rating_quality != 0:
return # return if the rating was already filled
if self.rating_work_hours != 0:
return # return if the rating was already filled
if rating.quality is not None:
self.rating_quality_lock = True
self.rating_quality = int(rating.quality)
self.rating_quality_lock = False
if rating.working_hours is not None:
wh: Union[float, int]
if rating.working_hours >= 1:
wh = int(rating.working_hours)
else:
wh = round(rating.working_hours, 1)
whs = str(wh)
self.rating_work_hours_lock = True
self.rating_work_hours = round(rating.working_hours, 2)
try:
# when the value is not in the enum, it throws an error
if whs == "0.0":
whs = "0"
self.rating_work_hours_ui = whs
except Exception as e:
bk_logger.warning(f"exception setting rating_work_hours_ui: {e}")
self.rating_work_hours = round(rating.working_hours, 2)
self.rating_work_hours_lock = False
bpy.context.area.tag_redraw()
# class RatingPropsCollection(PropertyGroup):
# ratings = CollectionProperty(type = RatingProperties)