Skip to content
Snippets Groups Projects
search.py 53.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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
    
        # 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)
    
                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()
    
                if r['assetType'] == 'hdr':
    
                    # to display hdr thumbnails correctly, we use non-color, otherwise looks shifted
    
                    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
    
                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:
    
                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)
    
            if fs:
                for f in fs:
    
                    if f['fileType'].find('resolution') > -1:
                        t += 'Asset has lower resolutions available\n'
    
                        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'])