Skip to content
Snippets Groups Projects
search.py 53.3 KiB
Newer Older
Vilem Duha's avatar
Vilem Duha committed
# ##### 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 #####

Vilém Duha's avatar
Vilém Duha committed
from blenderkit import paths, utils, categories, ui, colors, bkit_oauth, version_checker, tasks_queue, rerequests, \
    resolutions
Vilem Duha's avatar
Vilem Duha committed
import blenderkit
from bpy.app.handlers import persistent

from bpy.props import (  # TODO only keep the ones actually used when cleaning
    IntProperty,
    FloatProperty,
    FloatVectorProperty,
    StringProperty,
    EnumProperty,
    BoolProperty,
    PointerProperty,
)
from bpy.types import (
    Operator,
    Panel,
    AddonPreferences,
    PropertyGroup,
    UIList
)

import requests, os, random
import time
import threading
import platform
Vilem Duha's avatar
Vilem Duha committed
import bpy
Vilém Duha's avatar
Vilém Duha committed
import copy
Vilém Duha's avatar
Vilém Duha committed
import json
import math
import logging
bk_logger = logging.getLogger('blenderkit')

Vilem Duha's avatar
Vilem Duha committed
search_start_time = 0
prev_time = 0

Vilem Duha's avatar
Vilem Duha committed
def check_errors(rdata):
    if rdata.get('statusCode') and int(rdata.get('statusCode')) > 299:
        utils.p(rdata)
Vilem Duha's avatar
Vilem Duha committed
        if rdata.get('detail') == 'Invalid token.':
            user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
            if user_preferences.api_key != '':
                if user_preferences.enable_oauth:
                    bkit_oauth.refresh_token_thread()
                return False, rdata.get('detail')
            return False, 'Use login panel to connect your profile.'
        else:
            return False, rdata.get('detail')
Vilem Duha's avatar
Vilem Duha committed
    return True, ''


search_threads = []
thumb_sml_download_threads = {}
thumb_full_download_threads = {}
reports = ''

rtips = ['Click or drag model or material in scene to link/append ',
         "Please rate responsively and plentifully. This helps us distribute rewards to the authors.",
         "Click on brushes to link them into scene.",
         "All materials and brushes are free.",
         "Storage for public assets is unlimited.",
         "Locked models are available if you subscribe to Full plan.",
         "Login to upload your own models, materials or brushes.",
         "Use 'A' key over asset bar to search assets by same author.",
         "Use 'W' key over asset bar to open Authors webpage.", ]

    ''' this timer gets run every time the token needs refresh. It refreshes tokens and also categories.'''
    utils.p('refresh timer')
    user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
    fetch_server_data()
    categories.load_categories()

    return max(3600, user_preferences.api_key_life - 3600)
def update_ad(ad):
    if not ad.get('assetBaseId'):
        try:
            ad['assetBaseId'] = ad['asset_base_id']  # this should stay ONLY for compatibility with older scenes
            ad['assetType'] = ad['asset_type']  # this should stay ONLY for compatibility with older scenes
Vilém Duha's avatar
Vilém Duha committed
            ad['verificationStatus'] = ad[
                'verification_status']  # this should stay ONLY for compatibility with older scenes
            ad['author'] = {}
            ad['author']['id'] = ad['author_id']  # this should stay ONLY for compatibility with older scenes
            ad['canDownload'] = ad['can_download']  # this should stay ONLY for compatibility with older scenes
        except Exception as e:
            bk_logger.error('BLenderKit failed to update older asset data')
    return ad
def update_assets_data():  # updates assets data on scene load.
    '''updates some properties that were changed on scenes with older assets.
    The properties were mainly changed from snake_case to CamelCase to fit the data that is coming from the server.
    '''
    data = bpy.data

    datablocks = [
        bpy.data.objects,
        bpy.data.materials,
        bpy.data.brushes,
    ]
    for dtype in datablocks:
        for block in dtype:
            if block.get('asset_data') != None:
                update_ad(block['asset_data'])

    dicts = [
        'assets used',
        # 'assets rated',# assets rated stores only true/false, not asset data.
Vilém Duha's avatar
Vilém Duha committed
    for s in bpy.data.scenes:
Vilém Duha's avatar
Vilém Duha committed
            if not d:
                continue;

            for asset_id in d.keys():
                update_ad(d[asset_id])
Vilém Duha's avatar
Vilém Duha committed
                # bpy.context.scene['assets used'][ad] = ad
Vilem Duha's avatar
Vilem Duha committed
@persistent
def scene_load(context):
    '''
    Loads categories , checks timers registration, and updates scene asset data.
    Should (probably)also update asset data from server (after user consent)
    '''
Vilem Duha's avatar
Vilem Duha committed
    wm = bpy.context.window_manager
    fetch_server_data()
    categories.load_categories()
    if not bpy.app.timers.is_registered(refresh_token_timer):
        bpy.app.timers.register(refresh_token_timer, persistent=True, first_interval=36000)
Vilem Duha's avatar
Vilem Duha committed


def fetch_server_data():
    ''' download categories , profile, and refresh token if needed.'''
    if not bpy.app.background:
        user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
        api_key = user_preferences.api_key
        # Only refresh new type of tokens(by length), and only one hour before the token timeouts.
        if user_preferences.enable_oauth and \
Vilém Duha's avatar
Vilém Duha committed
                len(user_preferences.api_key) < 38 and len(user_preferences.api_key) > 0 and \
                user_preferences.api_key_timeout < time.time() + 3600:
            bkit_oauth.refresh_token_thread()
        if api_key != '' and bpy.context.window_manager.get('bkit profile') == None:
        if bpy.context.window_manager.get('bkit_categories') is None:
            categories.fetch_categories_thread(api_key)
Vilém Duha's avatar
Vilém Duha committed
last_clipboard = ''
def check_clipboard():
    '''
    Checks clipboard for an exact string containing asset ID.
    The string is generated on www.blenderkit.com as for example here:
    https://www.blenderkit.com/get-blenderkit/54ff5c85-2c73-49e9-ba80-aec18616a408/
    '''

    # clipboard monitoring to search assets from web
    if platform.system() != 'Linux':
        global last_clipboard
        if bpy.context.window_manager.clipboard != last_clipboard:
            last_clipboard = bpy.context.window_manager.clipboard
            instr = 'asset_base_id:'
            # first check if contains asset id, then asset type
            if last_clipboard[:len(instr)] == instr:
                atstr = 'asset_type:'
                ati = last_clipboard.find(atstr)
                # this only checks if the asset_type keyword is there but let's the keywords update function do the parsing.
                if ati > -1:
                    search_props = utils.get_search_props()
                    search_props.search_keywords = last_clipboard
                    # don't run search after this - assigning to keywords runs the search_update function.
def parse_result(r):
    '''
    needed to generate some extra data in the result(by now)
    Parameters
    ----------
    r - search result, also called asset_data
    '''
    scene = bpy.context.scene

    # TODO remove this fix when filesSize is fixed.
    # this is a temporary fix for too big numbers from the server.
Vilém Duha's avatar
Vilém Duha committed
    # try:
    #     r['filesSize'] = int(r['filesSize'] / 1024)
    # except:
    #     utils.p('asset with no files-size')
    asset_type = r['assetType']
    if len(r['files']) > 0:
Vilém Duha's avatar
Vilém Duha committed
        r['available_resolutions'] = []
Vilém Duha's avatar
Vilém Duha committed
        durl, tname, small_tname = '', '', ''
        for f in r['files']:
            if f['fileType'] == 'thumbnail':
                tname = paths.extract_filename_from_url(f['fileThumbnailLarge'])
                small_tname = paths.extract_filename_from_url(f['fileThumbnail'])
                allthumbs.append(tname)  # TODO just first thumb is used now.

            tdict = {}
            for i, t in enumerate(allthumbs):
                tdict['thumbnail_%i'] = t
            if f['fileType'] == 'blend':
                durl = f['downloadUrl'].split('?')[0]
                # fname = paths.extract_filename_from_url(f['filePath'])

Vilém Duha's avatar
Vilém Duha committed
            if f['fileType'].find('resolution') > -1:
                r['available_resolutions'].append(resolutions.resolutions[f['fileType']])
        r['max_resolution'] = 0
        if r['available_resolutions']:  # should check only for non-empty sequences
Vilém Duha's avatar
Vilém Duha committed
            r['max_resolution'] = max(r['available_resolutions'])

        tooltip = generate_tooltip(r)
        # for some reason, the id was still int on some occurances. investigate this.
        r['author']['id'] = str(r['author']['id'])

        # some helper props, but generally shouldn't be renaming/duplifiying original properties,
        # so blender's data is same as on server.
        asset_data = {'thumbnail': tname,
                      'thumbnail_small': small_tname,
                      # 'thumbnails':allthumbs,
Vilém Duha's avatar
Vilém Duha committed
                      # 'download_url': durl, #made obsolete since files are stored in orig form.
                      # 'id': r['id'],
                      # 'asset_base_id': r['assetBaseId'],#this should stay ONLY for compatibility with older scenes
                      # 'name': r['name'],
                      # 'asset_type': r['assetType'], #this should stay ONLY for compatibility with older scenes
                      'tooltip': tooltip,
                      # 'tags': r['tags'],
                      # 'can_download': r.get('canDownload', True),#this should stay ONLY for compatibility with older scenes
                      # 'verification_status': r['verificationStatus'],#this should stay ONLY for compatibility with older scenes
                      # 'author_id': r['author']['id'],#this should stay ONLY for compatibility with older scenes
                      # 'author': r['author']['firstName'] + ' ' + r['author']['lastName']
                      # 'description': r['description'],
                      }
        asset_data['downloaded'] = 0

        # parse extra params needed for blender here
        params = utils.params_to_dict(r['parameters'])

        if asset_type == 'model':
            if params.get('boundBoxMinX') != None:
                bbox = {
                    'bbox_min': (
                        float(params['boundBoxMinX']),
                        float(params['boundBoxMinY']),
                        float(params['boundBoxMinZ'])),
                    'bbox_max': (
                        float(params['boundBoxMaxX']),
                        float(params['boundBoxMaxY']),
                        float(params['boundBoxMaxZ']))
                }

            else:
                bbox = {
                    'bbox_min': (-.5, -.5, 0),
                    'bbox_max': (.5, .5, 1)
                }
            asset_data.update(bbox)
        if asset_type == 'material':
            asset_data['texture_size_meters'] = params.get('textureSizeMeters', 1.0)
        asset_data.update(tdict)
Vilém Duha's avatar
Vilém Duha committed

        au = scene.get('assets used', {})
        if au == {}:
            scene['assets used'] = au
        if r['assetBaseId'] in au.keys():
            asset_data['downloaded'] = 100
Vilém Duha's avatar
Vilém Duha committed
            # transcribe all urls already fetched from the server
            r_previous = au[r['assetBaseId']]
            if r_previous.get('files'):
                for f in r_previous['files']:
                    if f.get('url'):
                        for f1 in r['files']:
                            if f1['fileType'] == f['fileType']:
                                f1['url'] = f['url']
        # attempt to switch to use original data gradually, since the parsing as itself should become obsolete.
        asset_data.update(r)
        return asset_data
Vilém Duha's avatar
Vilém Duha committed

# @bpy.app.handlers.persistent
def timer_update():
    # this makes a first search after opening blender. showing latest assets.
Vilém Duha's avatar
Vilém Duha committed
    # utils.p('timer search')

    global first_time
    preferences = bpy.context.preferences.addons['blenderkit'].preferences
Vilém Duha's avatar
Vilém Duha committed
    if first_time and not bpy.app.background:  # first time
Vilém Duha's avatar
Vilém Duha committed
        if preferences.show_on_start:
Vilém Duha's avatar
Vilém Duha committed
            # TODO here it should check if there are some results, and only open assetbar if this is the case, not search.
Vilém Duha's avatar
Vilém Duha committed
            # if bpy.context.scene.get('search results') is None:
            # preferences.first_run = False
        if preferences.tips_on_start:
            utils.get_largest_area()
            ui.update_ui_size(ui.active_area_pointer, ui.active_region_pointer)
            ui.add_report(text='BlenderKit Tip: ' + random.choice(rtips), timeout=12, color=colors.GREEN)
Vilém Duha's avatar
Vilém Duha committed
        return 3.0
    # if preferences.first_run:
    #     search()
    #     preferences.first_run = False
Vilém Duha's avatar
Vilém Duha committed

    # check_clipboard()
Vilém Duha's avatar
Vilém Duha committed

Vilem Duha's avatar
Vilem Duha committed
    global search_threads
Vilém Duha's avatar
Vilém Duha committed
    if len(search_threads) == 0:
        return 1.0
Vilém Duha's avatar
Vilém Duha committed
    # don't do anything while dragging - this could switch asset during drag, and make results list length different,
    # causing a lot of throuble.
Vilém Duha's avatar
Vilém Duha committed
    if bpy.context.scene.blenderkitUI.dragging:
        return 0.5
Vilém Duha's avatar
Vilém Duha committed
    for thread in search_threads:
        # TODO this doesn't check all processes when one gets removed,
        # but most of the time only one is running anyway
Vilem Duha's avatar
Vilem Duha committed
        if not thread[0].is_alive():
            search_threads.remove(thread)  #
            icons_dir = thread[1]
            scene = bpy.context.scene
            # these 2 lines should update the previews enum and set the first result as active.
            s = bpy.context.scene
            asset_type = thread[2]
            if asset_type == 'model':
                props = scene.blenderkit_models
Vilém Duha's avatar
Vilém Duha committed
                # json_filepath = os.path.join(icons_dir, 'model_searchresult.json')
Vilem Duha's avatar
Vilem Duha committed
            if asset_type == 'scene':
                props = scene.blenderkit_scene
Vilém Duha's avatar
Vilém Duha committed
                # json_filepath = os.path.join(icons_dir, 'scene_searchresult.json')
            if asset_type == 'hdr':
                props = scene.blenderkit_HDR
                # json_filepath = os.path.join(icons_dir, 'scene_searchresult.json')
Vilem Duha's avatar
Vilem Duha committed
            if asset_type == 'material':
                props = scene.blenderkit_mat
Vilém Duha's avatar
Vilém Duha committed
                # json_filepath = os.path.join(icons_dir, 'material_searchresult.json')
Vilem Duha's avatar
Vilem Duha committed
            if asset_type == 'brush':
                props = scene.blenderkit_brush
Vilém Duha's avatar
Vilém Duha committed
                # json_filepath = os.path.join(icons_dir, 'brush_searchresult.json')
            search_name = f'bkit {asset_type} search'
Vilem Duha's avatar
Vilem Duha committed

            s[search_name] = []

            global reports
            if reports != '':
                props.report = str(reports)
                return .2
Vilém Duha's avatar
Vilém Duha committed

            rdata = thread[0].result
Vilem Duha's avatar
Vilem Duha committed

            result_field = []
            ok, error = check_errors(rdata)
            if ok:
                bpy.ops.object.run_assetbar_fix_context()
Vilem Duha's avatar
Vilem Duha committed
                for r in rdata['results']:
                    if asset_data != None:
                        result_field.append(asset_data)
                        # results = rdata['results']
Vilem Duha's avatar
Vilem Duha committed
                s[search_name] = result_field
                s['search results'] = result_field
Vilém Duha's avatar
Vilém Duha committed
                s[search_name + ' orig'] = copy.deepcopy(rdata)
                s['search results orig'] = s[search_name + ' orig']

Vilem Duha's avatar
Vilem Duha committed
                load_previews()
                ui_props = bpy.context.scene.blenderkitUI
                if len(result_field) < ui_props.scrolloffset:
                    ui_props.scrolloffset = 0
                props.is_searching = False
                props.search_error = False
                props.report = 'Found %i results. ' % (s['search results orig']['count'])
Vilem Duha's avatar
Vilem Duha committed
                if len(s['search results']) == 0:
                    tasks_queue.add_task((ui.add_report, ('No matching results found.',)))
Vilém Duha's avatar
Vilém Duha committed
                # undo push
                bpy.ops.wm.undo_push_context(message='Get BlenderKit search')
                bk_logger.error(error)
Vilem Duha's avatar
Vilem Duha committed
                props.report = error
                props.search_error = True

Vilem Duha's avatar
Vilem Duha committed
            # print('finished search thread')
Vilem Duha's avatar
Vilem Duha committed
            mt('preview loading finished')
Vilem Duha's avatar
Vilem Duha committed


def load_previews():
    scene = bpy.context.scene
    # FIRST START SEARCH
    props = scene.blenderkitUI
Vilém Duha's avatar
Vilém Duha committed
    directory = paths.get_temp_dir('%s_search' % props.asset_type.lower())
Vilem Duha's avatar
Vilem Duha committed
    s = bpy.context.scene
    results = s.get('search results')
    #
    if results is not None:
        inames = []
        tpaths = []

        i = 0
        for r in results:
            tpath = os.path.join(directory, r['thumbnail_small'])
            if not r['thumbnail_small']:
                tpath = paths.get_addon_thumbnail_path('thumbnail_not_available.jpg')
Vilem Duha's avatar
Vilem Duha committed

            iname = utils.previmg_name(i)

            # if os.path.exists(tpath):  # sometimes we are unlucky...
            img = bpy.data.images.get(iname)
Vilém Duha's avatar
Vilém Duha committed
            if img is None and os.path.exists(tpath):
                img = bpy.data.images.load(tpath)
                img.name = iname
            elif img.filepath != tpath:
                # had to add this check for autopacking files...
                if img.packed_file is not None:
                    img.unpack(method='USE_ORIGINAL')
                img.filepath = tpath
                img.reload()
Vilém Duha's avatar
Vilém Duha committed
            if r['assetType'] == 'hdr':
                # to display hdr thumbnails correctly, we use non-color, otherwise looks shifted
Vilém Duha's avatar
Vilém Duha committed
                img.colorspace_settings.name = 'Non-Color'
            else:
                img.colorspace_settings.name = 'sRGB'
Vilem Duha's avatar
Vilem Duha committed
            i += 1
Vilem Duha's avatar
Vilem Duha committed
    # print('previews loaded')
Vilem Duha's avatar
Vilem Duha committed


#  line splitting for longer texts...
def split_subs(text, threshold=40):
Vilem Duha's avatar
Vilem Duha committed
    if text == '':
        return []
    # temporarily disable this, to be able to do this in drawing code

Vilem Duha's avatar
Vilem Duha committed
    text = text.rstrip()
    text = text.replace('\r\n', '\n')

Vilem Duha's avatar
Vilem Duha committed
    lines = []
Vilem Duha's avatar
Vilem Duha committed
    while len(text) > threshold:
        # first handle if there's an \n line ending
        i_rn = text.find('\n')
        if 1 < i_rn < threshold:
            i = i_rn
            text = text.replace('\n', '', 1)
        else:
            i = text.rfind(' ', 0, threshold)
            i1 = text.rfind(',', 0, threshold)
            i2 = text.rfind('.', 0, threshold)
            i = max(i, i1, i2)
            if i <= 0:
                i = threshold
Vilem Duha's avatar
Vilem Duha committed
        lines.append(text[:i])
        text = text[i:]
    lines.append(text)
    return lines


def list_to_str(input):
    output = ''
    for i, text in enumerate(input):
        output += text
        if i < len(input) - 1:
            output += ', '
    return output


def writeblock(t, input, width=40):  # for longer texts
    dlines = split_subs(input, threshold=width)
Vilem Duha's avatar
Vilem Duha committed
    for i, l in enumerate(dlines):
        t += '%s\n' % l
    return t


def writeblockm(tooltip, mdata, key='', pretext=None, width=40):  # for longer texts
Vilem Duha's avatar
Vilem Duha committed
    if mdata.get(key) == None:
        return tooltip
    else:
        intext = mdata[key]
        if type(intext) == list:
            intext = list_to_str(intext)
        if type(intext) == float:
            intext = round(intext, 3)
Vilem Duha's avatar
Vilem Duha committed
        intext = str(intext)
        if intext.rstrip() == '':
            return tooltip
        if pretext == None:
            pretext = key
        if pretext != '':
            pretext = pretext + ': '
        text = pretext + intext
        dlines = split_subs(text, threshold=width)
Vilem Duha's avatar
Vilem Duha committed
        for i, l in enumerate(dlines):
            tooltip += '%s\n' % l

    return tooltip


def fmt_length(prop):
    prop = str(round(prop, 2)) + 'm'
    return prop


def has(mdata, prop):
    if mdata.get(prop) is not None and mdata[prop] is not None and mdata[prop] is not False:
        return True
    else:
        return False


def generate_tooltip(mdata):
Vilem Duha's avatar
Vilem Duha committed
    if type(mdata['parameters']) == list:
Vilém Duha's avatar
Vilém Duha committed
        mparams = utils.params_to_dict(mdata['parameters'])
Vilem Duha's avatar
Vilem Duha committed
    else:
        mparams = mdata['parameters']
    t = ''
    t = writeblock(t, mdata['displayName'], width=col_w)
    t = writeblockm(t, mdata, key='description', pretext='', width=col_w)
Vilem Duha's avatar
Vilem Duha committed
    if mdata['description'] != '':
        t += '\n'

    bools = (('rig', None), ('animated', None), ('manifold', 'non-manifold'), ('scene', None), ('simulation', None),
             ('uv', None))
    for b in bools:
        if mparams.get(b[0]):
            mdata['tags'].append(b[0])
        elif b[1] != None:
            mdata['tags'].append(b[1])

    bools_data = ('adult',)
    for b in bools_data:
        if mdata.get(b) and mdata[b]:
            mdata['tags'].append(b)
    t = writeblockm(t, mparams, key='designer', pretext='designer', width=col_w)
    t = writeblockm(t, mparams, key='manufacturer', pretext='manufacturer', width=col_w)
    t = writeblockm(t, mparams, key='designCollection', pretext='design collection', width=col_w)

    # t = writeblockm(t, mparams, key='engines', pretext='engine', width = col_w)
    # t = writeblockm(t, mparams, key='model_style', pretext='style', width = col_w)
    # t = writeblockm(t, mparams, key='material_style', pretext='style', width = col_w)
    # t = writeblockm(t, mdata, key='tags', width = col_w)
    # t = writeblockm(t, mparams, key='condition', pretext='condition', width = col_w)
    # t = writeblockm(t, mparams, key='productionLevel', pretext='production level', width = col_w)
Vilem Duha's avatar
Vilem Duha committed
    if has(mdata, 'purePbr'):
        t = writeblockm(t, mparams, key='pbrType', pretext='pbr', width=col_w)
    t = writeblockm(t, mparams, key='designYear', pretext='design year', width=col_w)
Vilem Duha's avatar
Vilem Duha committed

    if has(mparams, 'dimensionX'):
        t += 'size: %s, %s, %s\n' % (fmt_length(mparams['dimensionX']),
                                     fmt_length(mparams['dimensionY']),
                                     fmt_length(mparams['dimensionZ']))
    if has(mparams, 'faceCount'):
        t += 'face count: %s, render: %s\n' % (mparams['faceCount'], mparams['faceCountRender'])

    # write files size - this doesn't reflect true file size, since files size is computed from all asset files, including resolutions.
    if mdata.get('filesSize'):
Vilém Duha's avatar
Vilém Duha committed
        fs = utils.files_size_to_text(mdata['filesSize'])
        t += f'files size: {fs}\n'
    # t = writeblockm(t, mparams, key='meshPolyType', pretext='mesh type', width = col_w)
    # t = writeblockm(t, mparams, key='objectCount', pretext='nubmber of objects', width = col_w)
    # t = writeblockm(t, mparams, key='materials', width = col_w)
    # t = writeblockm(t, mparams, key='modifiers', width = col_w)
    # t = writeblockm(t, mparams, key='shaders', width = col_w)
Vilem Duha's avatar
Vilem Duha committed

    if has(mparams, 'textureSizeMeters'):
        t += 'texture size: %s\n' % fmt_length(mparams['textureSizeMeters'])
Vilem Duha's avatar
Vilem Duha committed

    if has(mparams, 'textureResolutionMax') and mparams['textureResolutionMax'] > 0:
        if not mparams.get('textureResolutionMin'):  # for HDR's
Vilém Duha's avatar
Vilém Duha committed
            t = writeblockm(t, mparams, key='textureResolutionMax', pretext='Resolution', width=col_w)
        elif mparams.get('textureResolutionMin') == mparams['textureResolutionMax']:
            t = writeblockm(t, mparams, key='textureResolutionMin', pretext='texture resolution', width=col_w)
Vilem Duha's avatar
Vilem Duha committed
        else:
Vilém Duha's avatar
Vilém Duha committed
            t += 'tex resolution: %i - %i\n' % (mparams.get('textureResolutionMin'), mparams['textureResolutionMax'])
Vilem Duha's avatar
Vilem Duha committed

    if has(mparams, 'thumbnailScale'):
        t = writeblockm(t, mparams, key='thumbnailScale', pretext='preview scale', width=col_w)
Vilem Duha's avatar
Vilem Duha committed

    # t += 'uv: %s\n' % mdata['uv']
    t = writeblockm(t, mdata, key='license', width=col_w)
Vilém Duha's avatar
Vilém Duha committed
    if utils.profile_is_validator():
        if fs:
            resolutions = 'resolutions:'
            for f in fs:
                if f['fileType'].find('resolution') > -1:
                    resolutions += f['fileType'][11:] + ' '
            resolutions += '\n'
            t += resolutions

        t = writeblockm(t, mdata, key='isFree', width=col_w)
Vilém Duha's avatar
Vilém Duha committed
        if fs:
            for f in fs:
                if f['fileType'].find('resolution') > -1:
                    t += 'Asset has lower resolutions available\n'
Vilém Duha's avatar
Vilém Duha committed
                    break;
Vilem Duha's avatar
Vilem Duha committed
    # generator is for both upload preview and search, this is only after search
    # if mdata.get('versionNumber'):
    #     # t = writeblockm(t, mdata, key='versionNumber', pretext='version', width = col_w)
    #     a_id = mdata['author'].get('id')
    #     if a_id != None:
    #         adata = bpy.context.window_manager['bkit authors'].get(str(a_id))
    #         if adata != None:
    #             t += generate_author_textblock(adata)

    # t += '\n'
    rc = mdata.get('ratingsCount')
    if utils.profile_is_validator() and rc:

        if rc:
            rcount = min(rc['quality'], rc['workingHours'])
        else:
            rcount = 0
        if rcount < 10:
            t += f"Please rate this asset, \nit doesn't have enough ratings.\n"
        else:
            t += f"Quality rating: {int(mdata['ratingsAverage']['quality']) * '*'}\n"
            t += f"Hours saved rating: {int(mdata['ratingsAverage']['workingHours'])}\n"
        if utils.profile_is_validator():
            t += f"Ratings count {rc['quality']}*/{rc['workingHours']}wh value " \
                 f"{mdata['ratingsAverage']['quality']}*/{mdata['ratingsAverage']['workingHours']}wh\n"
Vilém Duha's avatar
Vilém Duha committed
    if len(t.split('\n')) < 11:
        t += '\n'
        t += get_random_tip(mdata)
        t += '\n'
Vilem Duha's avatar
Vilem Duha committed
    return t

def get_random_tip(mdata):
    t = ''
    tip = 'Tip: ' + random.choice(rtips)
    t = writeblock(t, tip)
    return t
    # at = mdata['assetType']
    # if at == 'brush' or at == 'texture':
    #     t += 'click to link %s' % mdata['assetType']
    # if at == 'model' or at == 'material':
    #     tips = ['Click or drag in scene to link/append %s' % mdata['assetType'],
    #             "'A' key to search assets by same author",
    #             "'W' key to open Authors webpage",
    #             ]
    #     tip = 'Tip: ' + random.choice(tips)
    #     t = writeblock(t, tip)
Vilem Duha's avatar
Vilem Duha committed
def generate_author_textblock(adata):
Vilem Duha's avatar
Vilem Duha committed
    if adata not in (None, ''):
        col_w = 40
        if len(adata['firstName'] + adata['lastName']) > 0:
            t = 'Author:\n'
            t += '%s %s\n' % (adata['firstName'], adata['lastName'])
            t += '\n'
            if adata.get('aboutMeUrl') is not None:
                t = writeblockm(t, adata, key='aboutMeUrl', pretext='', width=col_w)
                t += '\n'
            if adata.get('aboutMe') is not None:
                t = writeblockm(t, adata, key='aboutMe', pretext='', width=col_w)
                t += '\n'
Vilem Duha's avatar
Vilem Duha committed
    return t


def get_items_models(self, context):
    global search_items_models
    return search_items_models


def get_items_brushes(self, context):
    global search_items_brushes
    return search_items_brushes


def get_items_materials(self, context):
    global search_items_materials
    return search_items_materials


def get_items_textures(self, context):
    global search_items_textures
    return search_items_textures


class ThumbDownloader(threading.Thread):
    query = None

    def __init__(self, url, path):
        super(ThumbDownloader, self).__init__()
        self.url = url
        self.path = path
        self._stop_event = threading.Event()

    def stop(self):
        self._stop_event.set()

    def stopped(self):
        return self._stop_event.is_set()

    def run(self):
        r = rerequests.get(self.url, stream=False)
Vilem Duha's avatar
Vilem Duha committed
        if r.status_code == 200:
            with open(self.path, 'wb') as f:
                f.write(r.content)
            # ORIGINALLY WE DOWNLOADED THUMBNAILS AS STREAM, BUT THIS WAS TOO SLOW.
            # with open(path, 'wb') as f:
            #     for chunk in r.iter_content(1048576*4):
            #         f.write(chunk)


def write_gravatar(a_id, gravatar_path):
    '''
    Write down gravatar path, as a result of thread-based gravatar image download.
    This should happen on timer in queue.
    '''
    # print('write author', a_id, type(a_id))
Vilem Duha's avatar
Vilem Duha committed
    authors = bpy.context.window_manager['bkit authors']
    if authors.get(a_id) is not None:
        adata = authors.get(a_id)
        adata['gravatarImg'] = gravatar_path
Vilem Duha's avatar
Vilem Duha committed


def fetch_gravatar(adata):
Vilém Duha's avatar
Vilém Duha committed
    # utils.p('fetch gravatar')
    if adata.get('gravatarHash') is not None:
Vilém Duha's avatar
Vilém Duha committed
        gravatar_path = paths.get_temp_dir(subdir='bkit_g/') + adata['gravatarHash'] + '.jpg'

        if os.path.exists(gravatar_path):
            tasks_queue.add_task((write_gravatar, (adata['id'], gravatar_path)))
            return;

        url = "https://www.gravatar.com/avatar/" + adata['gravatarHash'] + '?d=404'
        r = rerequests.get(url, stream=False)
        if r.status_code == 200:
            with open(gravatar_path, 'wb') as f:
                f.write(r.content)
            tasks_queue.add_task((write_gravatar, (adata['id'], gravatar_path)))
        elif r.status_code == '404':
            adata['gravatarHash'] = None
            utils.p('gravatar for author not available.')
Vilem Duha's avatar
Vilem Duha committed

fetching_gravatars = {}

Vilem Duha's avatar
Vilem Duha committed

def get_author(r):
    ''' Writes author info (now from search results) and fetches gravatar if needed.'''
    global fetching_gravatars

Vilem Duha's avatar
Vilem Duha committed
    a_id = str(r['author']['id'])
    preferences = bpy.context.preferences.addons['blenderkit'].preferences
    authors = bpy.context.window_manager.get('bkit authors', {})
    if authors == {}:
        bpy.context.window_manager['bkit authors'] = authors
    a = authors.get(a_id)
    if a is None:  # or a is '' or (a.get('gravatarHash') is not None and a.get('gravatarImg') is None):
        a = r['author']
        a['id'] = a_id
        a['tooltip'] = generate_author_textblock(a)

        authors[a_id] = a
        if fetching_gravatars.get(a['id']) is None:
            fetching_gravatars[a['id']] = True

        thread = threading.Thread(target=fetch_gravatar, args=(a.copy(),), daemon=True)
Vilem Duha's avatar
Vilem Duha committed
        thread.start()
    return a


def write_profile(adata):
    utils.p('writing profile information')
    user = adata['user']
    # we have to convert to MiB here, numbers too big for python int type
    if user.get('sumAssetFilesSize') is not None:
        user['sumAssetFilesSize'] /= (1024 * 1024)
    if user.get('sumPrivateAssetFilesSize') is not None:
        user['sumPrivateAssetFilesSize'] /= (1024 * 1024)
    if user.get('remainingPrivateQuota') is not None:
        user['remainingPrivateQuota'] /= (1024 * 1024)
    if adata.get('canEditAllAssets') is True:
        user['exmenu'] = True
    else:
        user['exmenu'] = False

Vilem Duha's avatar
Vilem Duha committed
    bpy.context.window_manager['bkit profile'] = adata


def request_profile(api_key):
    a_url = paths.get_api_url() + 'me/'
    headers = utils.get_headers(api_key)
    r = rerequests.get(a_url, headers=headers)
    adata = r.json()
    if adata.get('user') is None:
        utils.p(adata)
        utils.p('getting profile failed')
        return None
    return adata


Vilem Duha's avatar
Vilem Duha committed
def fetch_profile(api_key):
    utils.p('fetch profile')
    try:
        adata = request_profile(api_key)
        if adata is not None:
            tasks_queue.add_task((write_profile, (adata,)))
Vilem Duha's avatar
Vilem Duha committed
    except Exception as e:
        bk_logger.error(e)
Vilem Duha's avatar
Vilem Duha committed


def get_profile():
    preferences = bpy.context.preferences.addons['blenderkit'].preferences
    a = bpy.context.window_manager.get('bkit profile')
    thread = threading.Thread(target=fetch_profile, args=(preferences.api_key,), daemon=True)
    thread.start()
Vilem Duha's avatar
Vilem Duha committed
    return a

Vilém Duha's avatar
Vilém Duha committed

Vilem Duha's avatar
Vilem Duha committed
class Searcher(threading.Thread):
    query = None

Vilém Duha's avatar
Vilém Duha committed
    def __init__(self, query, params, orig_result):
Vilem Duha's avatar
Vilem Duha committed
        super(Searcher, self).__init__()
        self.query = query
        self.params = params
        self._stop_event = threading.Event()
Vilém Duha's avatar
Vilém Duha committed
        self.result = orig_result
Vilem Duha's avatar
Vilem Duha committed

    def stop(self):
        self._stop_event.set()

    def stopped(self):
        return self._stop_event.is_set()

    def query_to_url(self):
Vilém Duha's avatar
Vilém Duha committed
        query = self.query
        params = self.params
        # build a new request
        url = paths.get_api_url() + 'search/'

        # build request manually
        # TODO use real queries
        requeststring = '?query='
        #
        if query.get('query') not in ('', None):
            requeststring += query['query'].lower()
        for i, q in enumerate(query):
            if q != 'query':
                requeststring += '+'
                requeststring += q + ':' + str(query[q]).lower()

        # result ordering: _score - relevance, score - BlenderKit score
        order = []
        if params['free_first']:
            order = ['-is_free',]
Vilém Duha's avatar
Vilém Duha committed
        if query.get('query') is None and query.get('category_subtree') == None:
            # assumes no keywords and no category, thus an empty search that is triggered on start.
            # orders by last core file upload
Vilém Duha's avatar
Vilém Duha committed
            if query.get('verification_status') == 'uploaded':
                # for validators, sort uploaded from oldest
                order.append('created')
Vilém Duha's avatar
Vilém Duha committed
            else:
                order.append('-last_upload')
        elif query.get('author_id') is not None and utils.profile_is_validator():
            order.append('-created')
Vilém Duha's avatar
Vilém Duha committed
        else:
            if query.get('category_subtree') is not None:
                order.append('-score,_score')
Vilém Duha's avatar
Vilém Duha committed
            else:
                order.append('_score')
        requeststring += '+order:' + ','.join(order)
Vilém Duha's avatar
Vilém Duha committed

        requeststring += '&addon_version=%s' % params['addon_version']
        if params.get('scene_uuid') is not None:
            requeststring += '&scene_uuid=%s' % params['scene_uuid']
        # print('params', params)
Vilém Duha's avatar
Vilém Duha committed
        urlquery = url + requeststring
        return urlquery

Vilem Duha's avatar
Vilem Duha committed
    def run(self):
        maxthreads = 50
Vilem Duha's avatar
Vilem Duha committed
        query = self.query
        params = self.params
        global reports

        t = time.time()
        mt('search thread started')
        tempdir = paths.get_temp_dir('%s_search' % query['asset_type'])
Vilém Duha's avatar
Vilém Duha committed
        # json_filepath = os.path.join(tempdir, '%s_searchresult.json' % query['asset_type'])
        headers = utils.get_headers(params['api_key'])
Vilem Duha's avatar
Vilem Duha committed
        rdata = {}
        rdata['results'] = []

        if params['get_next']:
Vilém Duha's avatar
Vilém Duha committed
            urlquery = self.result['next']
Vilem Duha's avatar
Vilem Duha committed
        if not params['get_next']:
Vilém Duha's avatar
Vilém Duha committed
            urlquery = self.query_to_url()
            utils.p(urlquery)
            r = rerequests.get(urlquery, headers=headers)  # , params = rparameters)
Vilem Duha's avatar
Vilem Duha committed
            reports = ''
Vilem Duha's avatar
Vilem Duha committed
            # utils.p(r.text)
Vilem Duha's avatar
Vilem Duha committed
        except requests.exceptions.RequestException as e:
            bk_logger.error(e)
Vilem Duha's avatar
Vilem Duha committed
            reports = e
            # props.report = e
            return
Vilém Duha's avatar
Vilém Duha committed
        mt('search response is back ')
Vilem Duha's avatar
Vilem Duha committed
        try:
            rdata = r.json()
        except Exception as e:
Vilem Duha's avatar
Vilem Duha committed
            reports = r.text
            bk_logger.error(e)
Vilem Duha's avatar
Vilem Duha committed

        mt('data parsed ')
        if not rdata.get('results'):
            utils.pprint(rdata)
            # if the result was converted to json and didn't return results,
            # it means it's a server error that has a clear message.
            # That's why it gets processed in the update timer, where it can be passed in messages to user.
            self.result = rdata
            return
Vilem Duha's avatar
Vilem Duha committed
        # print('number of results: ', len(rdata.get('results', [])))
Vilem Duha's avatar
Vilem Duha committed
        if self.stopped():
Vilém Duha's avatar
Vilém Duha committed
            utils.p('stopping search : ' + str(query))
Vilem Duha's avatar
Vilem Duha committed
            return

        mt('search finished')
        i = 0

        thumb_small_urls = []
        thumb_small_filepaths = []
        thumb_full_urls = []
        thumb_full_filepaths = []
        # END OF PARSING
        for d in rdata.get('results', []):
Vilem Duha's avatar
Vilem Duha committed

Vilem Duha's avatar
Vilem Duha committed
            for f in d['files']:
                # TODO move validation of published assets to server, too manmy checks here.
                if f['fileType'] == 'thumbnail' and f['fileThumbnail'] != None and f['fileThumbnailLarge'] != None:
                    if f['fileThumbnail'] == None:
                        f['fileThumbnail'] = 'NONE'
                    if f['fileThumbnailLarge'] == None:
                        f['fileThumbnailLarge'] = 'NONE'

                    thumb_small_urls.append(f['fileThumbnail'])
                    thumb_full_urls.append(f['fileThumbnailLarge'])

                    imgname = paths.extract_filename_from_url(f['fileThumbnail'])
                    imgpath = os.path.join(tempdir, imgname)
                    thumb_small_filepaths.append(imgpath)

                    imgname = paths.extract_filename_from_url(f['fileThumbnailLarge'])
                    imgpath = os.path.join(tempdir, imgname)
                    thumb_full_filepaths.append(imgpath)

        sml_thbs = zip(thumb_small_filepaths, thumb_small_urls)
        full_thbs = zip(thumb_full_filepaths, thumb_full_urls)

        # we save here because a missing thumbnail check is in the previous loop
        # we can also prepend previous results. These have downloaded thumbnails already...
Vilem Duha's avatar
Vilem Duha committed
        if params['get_next']:
Vilém Duha's avatar
Vilém Duha committed
            rdata['results'][0:0] = self.result['results']
        self.result = rdata
        # with open(json_filepath, 'w') as outfile:
        #     json.dump(rdata, outfile)
Vilem Duha's avatar
Vilem Duha committed

        killthreads_sml = []
        for k in thumb_sml_download_threads.keys():
            if k not in thumb_small_filepaths:
                killthreads_sml.append(k)  # do actual killing here?

        killthreads_full = []
        for k in thumb_full_download_threads.keys():
            if k not in thumb_full_filepaths:
                killthreads_full.append(k)  # do actual killing here?
        # TODO do the killing/ stopping here! remember threads might have finished inbetween!

        if self.stopped():
Vilém Duha's avatar
Vilém Duha committed
            utils.p('stopping search : ' + str(query))
Vilem Duha's avatar
Vilem Duha committed
            return

        # this loop handles downloading of small thumbnails
        for imgpath, url in sml_thbs:
            if imgpath not in thumb_sml_download_threads and not os.path.exists(imgpath):
                thread = ThumbDownloader(url, imgpath)
                # thread = threading.Thread(target=download_thumbnail, args=([url, imgpath]),
                #                           daemon=True)
                thread.start()
                thumb_sml_download_threads[imgpath] = thread
                # threads.append(thread)

                if len(thumb_sml_download_threads) > maxthreads:
                    while len(thumb_sml_download_threads) > maxthreads:
                        threads_copy = thumb_sml_download_threads.copy()  # because for loop can erase some of the items.
                        for tk, thread in threads_copy.items():
                            if not thread.is_alive():
                                thread.join()
Vilem Duha's avatar
Vilem Duha committed
                                # utils.p(x)
Vilem Duha's avatar
Vilem Duha committed
                                del (thumb_sml_download_threads[tk])
Vilem Duha's avatar
Vilem Duha committed
                                # utils.p('fetched thumbnail ', i)
Vilem Duha's avatar
Vilem Duha committed
                                i += 1
        if self.stopped():
Vilém Duha's avatar
Vilém Duha committed
            utils.p('stopping search : ' + str(query))
Vilem Duha's avatar
Vilem Duha committed
            return
        idx = 0
        while len(thumb_sml_download_threads) > 0:
            threads_copy = thumb_sml_download_threads.copy()  # because for loop can erase some of the items.
            for tk, thread in threads_copy.items():
                if not thread.is_alive():
                    thread.join()
                    del (thumb_sml_download_threads[tk])
                    i += 1

        if self.stopped():
Vilém Duha's avatar
Vilém Duha committed
            utils.p('stopping search : ' + str(query))
Vilem Duha's avatar
Vilem Duha committed
            return

        # start downloading full thumbs in the end
        for imgpath, url in full_thbs:
            if imgpath not in thumb_full_download_threads and not os.path.exists(imgpath):
                thread = ThumbDownloader(url, imgpath)
                # thread = threading.Thread(target=download_thumbnail, args=([url, imgpath]),
                #                           daemon=True)
                thread.start()
                thumb_full_download_threads[imgpath] = thread
        mt('thumbnails finished')


def build_query_common(query, props):
    '''add shared parameters to query'''
    query_common = {}
Vilém Duha's avatar
Vilém Duha committed
    if props.search_keywords != '':
        query_common["query"] = props.search_keywords

    if props.search_verification_status != 'ALL':
        query_common['verification_status'] = props.search_verification_status.lower()
    if props.unrated_only:
        query["quality_count"] = 0

    if props.search_file_size:
        query_common["files_size_gte"] = props.search_file_size_min * 1024 * 1024
        query_common["files_size_lte"] = props.search_file_size_max * 1024 * 1024
    query.update(query_common)
Vilem Duha's avatar
Vilem Duha committed


def build_query_model():
    '''use all search input to request results from server'''

    props = bpy.context.scene.blenderkit_models
    query = {
        "asset_type": 'model',
        # "engine": props.search_engine,
        # "adult": props.search_adult,
Vilem Duha's avatar
Vilem Duha committed
    }
    if props.search_style != 'ANY':
        if props.search_style != 'OTHER':
            query["model_style"] = props.search_style
Vilem Duha's avatar
Vilem Duha committed
        else:
            query["model_style"] = props.search_style_other
    # the 'free_only' parametr gets moved to the search command and is used for ordering the assets as free first
    # if props.free_only:
    #     query["is_free"] = True
    # if props.search_advanced:
    if props.search_condition != 'UNSPECIFIED':
        query["condition"] = props.search_condition
    if props.search_design_year:
        query["designYear_gte"] = props.search_design_year_min
        query["designYear_lte"] = props.search_design_year_max
    if props.search_polycount:
        query["faceCount_gte"] = props.search_polycount_min
        query["faceCount_lte"] = props.search_polycount_max
    if props.search_texture_resolution:
        query["textureResolutionMax_gte"] = props.search_texture_resolution_min
        query["textureResolutionMax_lte"] = props.search_texture_resolution_max
Vilém Duha's avatar
Vilém Duha committed

Vilem Duha's avatar
Vilem Duha committed
    build_query_common(query, props)

    return query


def build_query_scene():
    '''use all search input to request results from server'''

    props = bpy.context.scene.blenderkit_scene
    query = {
        "asset_type": 'scene',
        # "engine": props.search_engine,
Vilem Duha's avatar
Vilem Duha committed
        # "adult": props.search_adult,
    }
    build_query_common(query, props)
    return query


def build_query_HDR():
    '''use all search input to request results from server'''

    props = bpy.context.scene.blenderkit_HDR
    query = {
        "asset_type": 'hdr',
        # "engine": props.search_engine,
        # "adult": props.search_adult,
    }
    build_query_common(query, props)
    return query


Vilem Duha's avatar
Vilem Duha committed
def build_query_material():
    props = bpy.context.scene.blenderkit_mat
    query = {
        "asset_type": 'material',

    }
    # if props.search_engine == 'NONE':
    #     query["engine"] = ''
    # if props.search_engine != 'OTHER':
    #     query["engine"] = props.search_engine
    # else:
    #     query["engine"] = props.search_engine_other
Vilem Duha's avatar
Vilem Duha committed
    if props.search_style != 'ANY':
        if props.search_style != 'OTHER':
            query["style"] = props.search_style
        else:
            query["style"] = props.search_style_other
    if props.search_procedural == 'TEXTURE_BASED':
        # todo this procedural hack should be replaced with the parameter
        query["textureResolutionMax_gte"] = 0
        # query["procedural"] = False
        if props.search_texture_resolution:
            query["textureResolutionMax_gte"] = props.search_texture_resolution_min
            query["textureResolutionMax_lte"] = props.search_texture_resolution_max



    elif props.search_procedural == "PROCEDURAL":
        # todo this procedural hack should be replaced with the parameter
        query["files_size_lte"] = 1024 * 1024
        # query["procedural"] = True
Vilém Duha's avatar
Vilém Duha committed

Vilem Duha's avatar
Vilem Duha committed
    build_query_common(query, props)

    return query


def build_query_texture():
    props = bpy.context.scene.blenderkit_tex
    query = {
        "asset_type": 'texture',

    }

    if props.search_style != 'ANY':
        if props.search_style != 'OTHER':
            query["search_style"] = props.search_style
        else:
            query["search_style"] = props.search_style_other

    build_query_common(query, props)

    return query


def build_query_brush():
    props = bpy.context.scene.blenderkit_brush

    brush_type = ''
    if bpy.context.sculpt_object is not None:
        brush_type = 'sculpt'

    elif bpy.context.image_paint_object:  # could be just else, but for future p
        brush_type = 'texture_paint'

    query = {
        "asset_type": 'brush',

        "mode": brush_type
Vilem Duha's avatar
Vilem Duha committed
    }

    build_query_common(query, props)

    return query


def mt(text):
    global search_start_time, prev_time
    alltime = time.time() - search_start_time
    since_last = time.time() - prev_time
    prev_time = time.time()
Vilem Duha's avatar
Vilem Duha committed
    utils.p(text, alltime, since_last)
Vilém Duha's avatar
Vilém Duha committed
def add_search_process(query, params, orig_result):
Vilem Duha's avatar
Vilem Duha committed
    global search_threads

    while (len(search_threads) > 0):
        old_thread = search_threads.pop(0)
        old_thread[0].stop()
        # TODO CARE HERE FOR ALSO KILLING THE Thumbnail THREADS.?
        #  AT LEAST NOW SEARCH DONE FIRST WON'T REWRITE AN OLDER ONE
Vilem Duha's avatar
Vilem Duha committed

    tempdir = paths.get_temp_dir('%s_search' % query['asset_type'])
Vilém Duha's avatar
Vilém Duha committed
    thread = Searcher(query, params, orig_result)
Vilem Duha's avatar
Vilem Duha committed
    thread.start()

Vilém Duha's avatar
Vilém Duha committed
    search_threads.append([thread, tempdir, query['asset_type'], {}])  # 4th field is for results
Vilém Duha's avatar
Vilém Duha committed
    mt('search thread started')


def get_search_simple(parameters, filepath=None, page_size=100, max_results=100000000, api_key=''):
    '''
    Searches and returns the


    Parameters
    ----------
    parameters - dict of blenderkit elastic parameters
    filepath - a file to save the results. If None, results are returned
    page_size - page size for retrieved results
    max_results - max results of the search
    api_key - BlenderKit api key

    Returns
    -------
    Returns search results as a list, and optionally saves to filepath

    '''
    headers = utils.get_headers(api_key)
    url = paths.get_api_url() + 'search/'
    requeststring = url + '?query='
    for p in parameters.keys():
        requeststring += f'+{p}:{parameters[p]}'

    requeststring += '&page_size=' + str(page_size)
    bk_logger.debug(requeststring)
Vilém Duha's avatar
Vilém Duha committed
    response = rerequests.get(requeststring, headers=headers)  # , params = rparameters)
    # print(r.json())
    search_results = response.json()

    results = []
    results.extend(search_results['results'])
    page_index = 2
    page_count = math.ceil(search_results['count'] / page_size)
    while search_results.get('next') and len(results) < max_results:
        bk_logger.info(f'getting page {page_index} , total pages {page_count}')
Vilém Duha's avatar
Vilém Duha committed
        response = rerequests.get(search_results['next'], headers=headers)  # , params = rparameters)
        search_results = response.json()
        # print(search_results)
        results.extend(search_results['results'])
        page_index += 1

    if not filepath:
        return results

    with open(filepath, 'w') as s:
        json.dump(results, s)
    bk_logger.info(f'retrieved {len(results)} assets from elastic search')
Vilém Duha's avatar
Vilém Duha committed
    return results
def search(category='', get_next=False, author_id=''):
Vilem Duha's avatar
Vilem Duha committed
    ''' initialize searching'''
    global search_start_time
    user_preferences = bpy.context.preferences.addons['blenderkit'].preferences

Vilem Duha's avatar
Vilem Duha committed
    search_start_time = time.time()
    # mt('start')
Vilem Duha's avatar
Vilem Duha committed
    scene = bpy.context.scene
    ui_props = scene.blenderkitUI
    if ui_props.asset_type == 'MODEL':
Vilem Duha's avatar
Vilem Duha committed
        if not hasattr(scene, 'blenderkit'):
            return;
        props = scene.blenderkit_models
        query = build_query_model()

    if ui_props.asset_type == 'SCENE':
Vilem Duha's avatar
Vilem Duha committed
        if not hasattr(scene, 'blenderkit_scene'):
            return;
        props = scene.blenderkit_scene
        query = build_query_scene()
    if ui_props.asset_type == 'HDR':
        if not hasattr(scene, 'blenderkit_HDR'):
            return;
        props = scene.blenderkit_HDR
        query = build_query_HDR()

    if ui_props.asset_type == 'MATERIAL':
Vilem Duha's avatar
Vilem Duha committed
        if not hasattr(scene, 'blenderkit_mat'):
            return;
Vilem Duha's avatar
Vilem Duha committed
        props = scene.blenderkit_mat
        query = build_query_material()
    if ui_props.asset_type == 'TEXTURE':
Vilem Duha's avatar
Vilem Duha committed
        if not hasattr(scene, 'blenderkit_tex'):
            return;
        # props = scene.blenderkit_tex
        # query = build_query_texture()

    if ui_props.asset_type == 'BRUSH':
Vilem Duha's avatar
Vilem Duha committed
        if not hasattr(scene, 'blenderkit_brush'):
            return;
        props = scene.blenderkit_brush
        query = build_query_brush()

    if props.is_searching and get_next == True:
        return;
Vilem Duha's avatar
Vilem Duha committed
    if category != '':
        query['category_subtree'] = category
    if author_id != '':
        query['author_id'] = author_id

Vilém Duha's avatar
Vilém Duha committed
    elif props.own_only:
        # if user searches for [another] author, 'only my assets' is invalid. that's why in elif.
        profile = bpy.context.window_manager.get('bkit profile')
        if profile is not None:
            query['author_id'] = str(profile['user']['id'])

Vilem Duha's avatar
Vilem Duha committed
    # utils.p('searching')
Vilem Duha's avatar
Vilem Duha committed
    props.is_searching = True

    params = {
        'scene_uuid': bpy.context.scene.get('uuid', None),
        'addon_version': version_checker.get_addon_version(),
        'api_key': user_preferences.api_key,
        'get_next': get_next,
        'free_first': props.free_only
    # if free_only:
    #     query['keywords'] += '+is_free:true'
Vilém Duha's avatar
Vilém Duha committed
    orig_results = scene.get(f'bkit {ui_props.asset_type.lower()} search orig', {})
    if orig_results != {}:
Vilém Duha's avatar
Vilém Duha committed
        # ensure it's a copy in dict for what we are passing to thread:
Vilém Duha's avatar
Vilém Duha committed
        orig_results = orig_results.to_dict()
    add_search_process(query, params, orig_results)
    tasks_queue.add_task((ui.add_report, ('BlenderKit searching....', 2)))
Vilem Duha's avatar
Vilem Duha committed
    props.report = 'BlenderKit searching....'


def search_update(self, context):
Vilem Duha's avatar
Vilem Duha committed
    utils.p('search updater')
    # if self.search_keywords != '':
    ui_props = bpy.context.scene.blenderkitUI
    if ui_props.down_up != 'SEARCH':
        ui_props.down_up = 'SEARCH'

    # here we tweak the input if it comes form the clipboard. we need to get rid of asset type and set it in UI
Vilém Duha's avatar
Vilém Duha committed
    sprops = utils.get_search_props()
    instr = 'asset_base_id:'
    atstr = 'asset_type:'
    kwds = sprops.search_keywords
    idi = kwds.find(instr)
    ati = kwds.find(atstr)
    # if the asset type already isn't there it means this update function
    # was triggered by it's last iteration and needs to cancel
    if ati > -1:
        at = kwds[ati:].lower()
        # uncertain length of the remaining string -  find as better method to check the presence of asset type
        if at.find('model') > -1:
            ui_props.asset_type = 'MODEL'
        elif at.find('material') > -1:
            ui_props.asset_type = 'MATERIAL'
        elif at.find('brush') > -1:
            ui_props.asset_type = 'BRUSH'
        elif at.find('scene') > -1:
            ui_props.asset_type = 'SCENE'
        elif at.find('hdr') > -1:
            ui_props.asset_type = 'HDR'
Vilém Duha's avatar
Vilém Duha committed
        # now we trim the input copypaste by anything extra that is there,
        # this is also a way for this function to recognize that it already has parsed the clipboard
        # the search props can have changed and this needs to transfer the data to the other field
        # this complex behaviour is here for the case where the user needs to paste manually into blender?
        sprops = utils.get_search_props()
        sprops.search_keywords = kwds[:ati].rstrip()
        # return here since writing into search keywords triggers this update function once more.
Vilem Duha's avatar
Vilem Duha committed


class SearchOperator(Operator):
    """Tooltip"""
    bl_idname = "view3d.blenderkit_search"
    bl_label = "BlenderKit asset search"
    bl_description = "Search online for assets"
Vilém Duha's avatar
Vilém Duha committed
    bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
Vilem Duha's avatar
Vilem Duha committed
    own: BoolProperty(name="own assets only",
                      description="Find all own assets",
                      default=False)
Vilem Duha's avatar
Vilem Duha committed
    category: StringProperty(
        name="category",
        description="search only subtree of this category",
        options={'SKIP_SAVE'}
    author_id: StringProperty(
        name="Author ID",
        description="Author ID - search only assets by this author",
        options={'SKIP_SAVE'}
Vilem Duha's avatar
Vilem Duha committed
    get_next: BoolProperty(name="next page",
                           description="get next page from previous search",
                           options={'SKIP_SAVE'}
                           )
    keywords: StringProperty(
        name="Keywords",
        description="Keywords",
        options={'SKIP_SAVE'}
Vilem Duha's avatar
Vilem Duha committed
    @classmethod
    def poll(cls, context):
        return True

    def execute(self, context):
        # TODO ; this should all get transferred to properties of the search operator, so sprops don't have to be fetched here at all.
        sprops = utils.get_search_props()
        if self.author_id != '':
            sprops.search_keywords = ''
        if self.keywords != '':
            sprops.search_keywords = self.keywords

        search(category=self.category, get_next=self.get_next, author_id=self.author_id)
        # bpy.ops.view3d.blenderkit_asset_bar()
Vilem Duha's avatar
Vilem Duha committed

        return {'FINISHED'}


classes = [
    SearchOperator
]


def register_search():
    bpy.app.handlers.load_post.append(scene_load)

    for c in classes:
        bpy.utils.register_class(c)

    user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
    if user_preferences.use_timers:
        bpy.app.timers.register(timer_update)
Vilem Duha's avatar
Vilem Duha committed


def unregister_search():
    bpy.app.handlers.load_post.remove(scene_load)

    for c in classes:
        bpy.utils.unregister_class(c)
    if bpy.app.timers.is_registered(timer_update):
        bpy.app.timers.unregister(timer_update)