Skip to content
Snippets Groups Projects
search.py 54 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, image_utils, ratings_utils
    
    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 unicodedata
    
    import urllib
    
    Vilém Duha's avatar
    Vilém Duha committed
    import queue
    
    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')
    
        if rdata.get('statusCode') is None and rdata.get('results') is None:
            return False, 'Connection error'
    
    Vilem Duha's avatar
    Vilem Duha committed
        return True, ''
    
    
    search_threads = []
    thumb_sml_download_threads = {}
    thumb_full_download_threads = {}
    
    Vilém Duha's avatar
    Vilém Duha committed
    reports_queue = queue.Queue()
    
    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 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:
    
    Vilém Duha's avatar
    Vilém Duha committed
                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
    
    def purge_search_results():
        ''' clean up search results on save/load.'''
    
        s = bpy.context.scene
    
        sr_props = [
            'search results',
            'search results orig',
        ]
        asset_types = ['model', 'material', 'scene', 'hdr', 'brush']
        for at in asset_types:
            sr_props.append('bkit {at} search')
            sr_props.append('bkit {at} search orig')
        for sr_prop in sr_props:
            if s.get(sr_prop):
                del (s[sr_prop])
    
    
    
    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
    
        purge_search_results()
    
    Vilem Duha's avatar
    Vilem Duha committed
        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, force=False)
    
    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')
    
        if len(r['files']) > 0:  # TODO remove this condition so all assets are parsed.
    
            get_author(r)
    
    
    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 = '', '', ''
    
                tname = paths.extract_filename_from_url(r['thumbnailLargeUrlNonsquared'])
    
            else:
                tname = paths.extract_filename_from_url(r['thumbnailMiddleUrl'])
            small_tname = paths.extract_filename_from_url(r['thumbnailSmallUrl'])
            allthumbs.append(tname)  # TODO just first thumb is used now.
            # if r['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.
    
    
                # 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.
    
    
                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']])
    
            # code for more thumbnails
    
            # tdict = {}
            # for i, t in enumerate(allthumbs):
            #     tdict['thumbnail_%i'] = t
    
    
    Vilém Duha's avatar
    Vilém Duha committed
            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,
                          'tooltip': tooltip,
    
                          }
            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)
    
    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 search_timer():
    
        # this makes a first search after opening blender. showing latest assets.
    
    Vilém Duha's avatar
    Vilém Duha committed
        # utils.p('timer search')
    
        # utils.p('start search timer')
    
        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.
    
                # if bpy.context.window_manager.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)
    
            # utils.p('end search timer')
    
    
    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:
    
            # utils.p('end search timer')
    
            props = utils.get_search_props()
            props.is_searching = False
    
    Vilém Duha's avatar
    Vilém Duha committed
            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:
    
            # utils.p('end search timer')
    
    
    Vilém Duha's avatar
    Vilém Duha committed
            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():
    
    Vilem Duha's avatar
    Vilem Duha committed
                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.
    
                wm = bpy.context.window_manager
    
    Vilem Duha's avatar
    Vilem Duha committed
                asset_type = thread[2]
    
    
                props = utils.get_search_props()
    
    Vilém Duha's avatar
    Vilém Duha committed
                search_name = f'bkit {asset_type} search'
    
                if not thread[0].params.get('get_next'):
                    # wm[search_name] = []
                    result_field = []
                else:
                    result_field = []
                    for r in wm[search_name]:
                        result_field.append(r.to_dict())
    
    Vilém Duha's avatar
    Vilém Duha committed
                global reports_queue
    
                while not reports_queue.empty():
                    props.report = str(reports_queue.get())
    
                    # utils.p('end search timer')
    
    
    Vilem Duha's avatar
    Vilem Duha committed
                    return .2
    
    Vilém Duha's avatar
    Vilém Duha committed
    
                rdata = thread[0].result
    
    Vilem Duha's avatar
    Vilem Duha committed
                ok, error = check_errors(rdata)
                if ok:
    
                    ui_props = bpy.context.scene.blenderkitUI
    
    
                    orig_len = len(result_field)
                    for ri, r in enumerate(rdata['results']):
    
                        if asset_data != None:
                            result_field.append(asset_data)
    
                            load_preview(asset_data,ri + orig_len)
    
                    # Get ratings from BlenderKit server
    
                    user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
                    api_key = user_preferences.api_key
                    headers = utils.get_headers(api_key)
    
                    if utils.profile_is_validator():
                        for r in rdata['results']:
                            if ratings_utils.get_rating_local(asset_data['id']) is None:
                                rating_thread = threading.Thread(target=ratings_utils.get_rating, args=([r['id'], headers]), daemon=True)
                                rating_thread.start()
    
                    wm[search_name] = result_field
                    wm['search results'] = result_field
    
                    #rdata=['results']=[]
                    wm[search_name + ' orig'] = rdata
                    wm['search results orig'] = rdata
    
                    # load_previews()
    
                    if len(result_field) < ui_props.scrolloffset or not (thread[0].params.get('get_next')):
                        # jump back
    
    Vilem Duha's avatar
    Vilem Duha committed
                        ui_props.scrolloffset = 0
                    props.search_error = False
    
                    props.report = 'Found %i results. ' % (wm['search results orig']['count'])
                    if len(wm['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')
    
                    #show asset bar automatically, but only on first page - others are loaded also when asset bar is hidden.
                    if not ui_props.assetbar_on and not thread[0].params.get('get_next'):
                        bpy.ops.object.run_assetbar_fix_context()
    
                    bk_logger.error(error)
    
    Vilem Duha's avatar
    Vilem Duha committed
                    props.report = error
                    props.search_error = True
    
    
                props.is_searching = False
    
    Vilem Duha's avatar
    Vilem Duha committed
                # print('finished search thread')
    
    Vilem Duha's avatar
    Vilem Duha committed
                mt('preview loading finished')
    
        # utils.p('end search timer')
    
    def load_preview(asset, index):
        scene = bpy.context.scene
        # FIRST START SEARCH
        props = scene.blenderkitUI
        directory = paths.get_temp_dir('%s_search' % props.asset_type.lower())
        s = bpy.context.scene
        results = bpy.context.window_manager.get('search results')
    
    
        tpath = os.path.join(directory, asset['thumbnail_small'])
        if not asset['thumbnail_small']:
            tpath = paths.get_addon_thumbnail_path('thumbnail_not_available.jpg')
    
        iname = utils.previmg_name(index)
    
        # if os.path.exists(tpath):  # sometimes we are unlucky...
        img = bpy.data.images.get(iname)
    
        if img is None:
            if not os.path.exists(tpath):
                return
            img = bpy.data.images.load(tpath)
            img.name = iname
        elif img.filepath != tpath:
            if not os.path.exists(tpath):
                return
            # 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 asset['assetType'] == 'hdr':
            # to display hdr thumbnails correctly, we use non-color, otherwise looks shifted
            image_utils.set_colorspace(img, 'Non-Color')
        else:
            image_utils.set_colorspace(img, 'sRGB')
    
    
    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 = bpy.context.window_manager.get('search results')
    
    Vilem Duha's avatar
    Vilem Duha committed
        #
        if results is not None:
            i = 0
            for r in results:
    
                load_preview(r,i)
    
    Vilem Duha's avatar
    Vilem Duha committed
                i += 1
    
    
    #  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 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
    
    
    Vilem Duha's avatar
    Vilem Duha committed
    def generate_tooltip(mdata):
    
        col_w = 40
        if type(mdata['parameters']) == list:
            mparams = utils.params_to_dict(mdata['parameters'])
        else:
            mparams = mdata['parameters']
        t = ''
    
        t = writeblock(t, mdata['displayName'], width=int(col_w * .6))
    
        # t = writeblockm(t, mdata, key='description', pretext='', width=col_w)
    
    Vilem Duha's avatar
    Vilem Duha committed
        return t
    
    
    def get_random_tip():
    
        tip = 'Tip: ' + random.choice(rtips)
        t = writeblock(t, tip)
        return t
    
    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 = 2000
    
            if len(adata['firstName'] + adata['lastName']) > 0:
    
                t = '%s %s\n' % (adata['firstName'], adata['lastName'])
    
                t += '\n'
                if adata.get('aboutMe') is not None:
                    t = writeblockm(t, adata, key='aboutMe', pretext='', width=col_w)
    
    Vilem Duha's avatar
    Vilem Duha committed
        return t
    
    
    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):
    
            # print('thumb downloader', self.url)
    
            # utils.p('start thumbdownloader thread')
    
            r = None
    
    Vilém Duha's avatar
    Vilém Duha committed
            try:
                r = requests.get(self.url, stream=False)
            except Exception as e:
                bk_logger.error('Thumbnail download failed')
                bk_logger.error(str(e))
    
            if r and r.status_code == 200:
    
    Vilem Duha's avatar
    Vilem Duha committed
                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)
    
            # utils.p('end thumbdownloader thread')
    
    
    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):
    
        '''
        Gets avatars from blenderkit server
        Parameters
        ----------
        adata - author data from elastic search result
    
        '''
    
    Vilém Duha's avatar
    Vilém Duha committed
        # utils.p('fetch gravatar')
    
    
        #fetch new avatars if available already
        if adata.get('avatar128') is not None:
            avatar_path = paths.get_temp_dir(subdir='bkit_g/') + adata['id']+ '.jpg'
            if os.path.exists(avatar_path):
                tasks_queue.add_task((write_gravatar, (adata['id'], avatar_path)))
                return;
    
            url= paths.get_bkit_url() + adata['avatar128']
            r = rerequests.get(url, stream=False)
            # print(r.body)
            if r.status_code == 200:
                # print(url)
                # print(r.headers['content-disposition'])
                with open(avatar_path, 'wb') as f:
                    f.write(r.content)
                tasks_queue.add_task((write_gravatar, (adata['id'], avatar_path)))
            elif r.status_code == '404':
                adata['avatar128'] = None
                utils.p('avatar for author not available.')
            return
    
        #older gravatar code
    
        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
    
    
    
    def query_to_url(query={}, 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', ]
        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
            if query.get('verification_status') == 'uploaded':
                # for validators, sort uploaded from oldest
                order.append('created')
            else:
                order.append('-last_upload')
        elif query.get('author_id') is not None and utils.profile_is_validator():
    
            order.append('-created')
        else:
            if query.get('category_subtree') is not None:
                order.append('-score,_score')
            else:
                order.append('_score')
    
        if requeststring.find('+order:')==-1:
            requeststring += '+order:' + ','.join(order)
    
        requeststring += '&page_size=' + str(params['page_size'])
    
        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)
        urlquery = url + requeststring
        return urlquery
    
    Vilém Duha's avatar
    Vilém Duha committed
    
    
    Vilém Duha's avatar
    Vilém Duha committed
    def parse_html_formated_error(text):
        report = text[text.find('<title>') + 7: text.find('</title>')]
    
        return report
    
    
    Vilem Duha's avatar
    Vilem Duha committed
    class Searcher(threading.Thread):
        query = None
    
    
        def __init__(self, query, params, tempdir='', headers=None, urlquery=''):
    
    Vilem Duha's avatar
    Vilem Duha committed
            super(Searcher, self).__init__()
            self.query = query
            self.params = params
            self._stop_event = threading.Event()
    
            self.result = {}
    
            self.tempdir = tempdir
            self.headers = headers
            self.urlquery = urlquery
    
    Vilem Duha's avatar
    Vilem Duha committed
    
        def stop(self):
            self._stop_event.set()
    
        def stopped(self):
            return self._stop_event.is_set()
    
        def run(self):
    
    Vilém Duha's avatar
    Vilém Duha committed
            global reports_queue
    
    
            maxthreads = 50
    
    Vilem Duha's avatar
    Vilem Duha committed
            query = self.query
            params = self.params
    
    Vilém Duha's avatar
    Vilém Duha committed
    
    
    Vilem Duha's avatar
    Vilem Duha committed
            t = time.time()
    
            # utils.p('start search thread')
    
    
    Vilem Duha's avatar
    Vilem Duha committed
            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'])
    
    Vilem Duha's avatar
    Vilem Duha committed
    
            rdata = {}
            rdata['results'] = []
    
            try:
    
                utils.p(self.urlquery)
                r = rerequests.get(self.urlquery, headers=self.headers)  # , params = rparameters)
    
    Vilem Duha's avatar
    Vilem Duha committed
            except requests.exceptions.RequestException as e:
    
                bk_logger.error(e)
    
    Vilém Duha's avatar
    Vilém Duha committed
                reports_queue.put(str(e))
    
                # utils.p('end search thread')
    
    
    Vilem Duha's avatar
    Vilem Duha committed
                return
    
    Vilém Duha's avatar
    Vilém Duha committed
    
    
    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:
    
                if hasattr(r,'text'):
                    error_description = parse_html_formated_error(r.text)
                    reports_queue.put(error_description)
                    tasks_queue.add_task((ui.add_report, (error_description, 10, colors.RED)))
    
    Vilém Duha's avatar
    Vilém Duha committed
                bk_logger.error(e)
                return
    
    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
    
                # utils.p('end search thread')
    
    
    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))
    
                # utils.p('end search thread')
    
    
    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', []):
    
                thumb_small_urls.append(d["thumbnailSmallUrl"])
                imgname = paths.extract_filename_from_url(d['thumbnailSmallUrl'])
                imgpath = os.path.join(self.tempdir, imgname)
                thumb_small_filepaths.append(imgpath)
    
                    larege_thumb_url = d['thumbnailLargeUrlNonsquared']
    
                else:
                    larege_thumb_url = d['thumbnailMiddleUrl']
    
                thumb_full_urls.append(larege_thumb_url)
                imgname = paths.extract_filename_from_url(larege_thumb_url)
                imgpath = os.path.join(self.tempdir, imgname)
                thumb_full_filepaths.append(imgpath)
    
                # 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(self.tempdir, imgname)
                #         thumb_small_filepaths.append(imgpath)
                #
                #         imgname = paths.extract_filename_from_url(f['fileThumbnailLarge'])
                #         imgpath = os.path.join(self.tempdir, imgname)
                #         thumb_full_filepaths.append(imgpath)
    
    Vilem Duha's avatar
    Vilem Duha committed
    
            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...
    
    Vilém Duha's avatar
    Vilém Duha committed
            self.result = rdata
    
            # with open(json_filepath, 'w', encoding = 'utf-8') as outfile:
            #     json.dump(rdata, outfile, ensure_ascii=False, indent=4)
    
    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():