1# ##### BEGIN GPL LICENSE BLOCK #####
2#
3#  This program is free software; you can redistribute it and/or
4#  modify it under the terms of the GNU General Public License
5#  as published by the Free Software Foundation; either version 2
6#  of the License, or (at your option) any later version.
7#
8#  This program is distributed in the hope that it will be useful,
9#  but WITHOUT ANY WARRANTY; without even the implied warranty of
10#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11#  GNU General Public License for more details.
12#
13#  You should have received a copy of the GNU General Public License
14#  along with this program; if not, write to the Free Software Foundation,
15#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16#
17# ##### END GPL LICENSE BLOCK #####
18
19if "bpy" in locals():
20    from importlib import reload
21
22    paths = reload(paths)
23    append_link = reload(append_link)
24    utils = reload(utils)
25    ui = reload(ui)
26    colors = reload(colors)
27    tasks_queue = reload(tasks_queue)
28    rerequests = reload(rerequests)
29else:
30    from blenderkit import paths, append_link, utils, ui, colors, tasks_queue, rerequests
31
32import threading
33import time
34import requests
35import shutil, sys, os
36import uuid
37import copy
38
39import bpy
40from bpy.props import (
41    IntProperty,
42    FloatProperty,
43    FloatVectorProperty,
44    StringProperty,
45    EnumProperty,
46    BoolProperty,
47    PointerProperty,
48)
49from bpy.app.handlers import persistent
50
51download_threads = []
52
53
54def check_missing():
55    '''checks for missing files, and possibly starts re-download of these into the scene'''
56    s = bpy.context.scene
57    # missing libs:
58    # TODO: put these into a panel and let the user decide if these should be downloaded.
59    missing = []
60    for l in bpy.data.libraries:
61        fp = l.filepath
62        if fp.startswith('//'):
63            fp = bpy.path.abspath(fp)
64        if not os.path.exists(fp) and l.get('asset_data') is not None:
65            missing.append(l)
66
67    # print('missing libraries', missing)
68
69    for l in missing:
70        asset_data = l['asset_data']
71        downloaded = check_existing(asset_data)
72        if downloaded:
73            try:
74                l.reload()
75            except:
76                download(l['asset_data'], redownload=True)
77        else:
78            download(l['asset_data'], redownload=True)
79
80
81def check_unused():
82    '''find assets that have been deleted from scene but their library is still present.'''
83    #this is obviously broken. Blender should take care of the extra data automaticlaly
84    return;
85    used_libs = []
86    for ob in bpy.data.objects:
87        if ob.instance_collection is not None and ob.instance_collection.library is not None:
88            # used_libs[ob.instance_collection.name] = True
89            if ob.instance_collection.library not in used_libs:
90                used_libs.append(ob.instance_collection.library)
91
92        for ps in ob.particle_systems:
93            set = ps.settings
94            if ps.settings.render_type == 'GROUP' \
95                    and ps.settings.instance_collection is not None \
96                    and ps.settings.instance_collection.library not in used_libs:
97                used_libs.append(ps.settings.instance_collection)
98
99    for l in bpy.data.libraries:
100        if l not in used_libs and l.getn('asset_data'):
101            print('attempt to remove this library: ', l.filepath)
102            # have to unlink all groups, since the file is a 'user' even if the groups aren't used at all...
103            for user_id in l.users_id:
104                if type(user_id) == bpy.types.Collection:
105                    bpy.data.collections.remove(user_id)
106            l.user_clear()
107
108
109@persistent
110def scene_save(context):
111    ''' does cleanup of blenderkit props and sends a message to the server about assets used.'''
112    # TODO this can be optimized by merging these 2 functions, since both iterate over all objects.
113    if not bpy.app.background:
114        check_unused()
115        report_usages()
116
117
118@persistent
119def scene_load(context):
120    '''restart broken downloads on scene load'''
121    t = time.time()
122    s = bpy.context.scene
123    global download_threads
124    download_threads = []
125
126    # commenting this out - old restore broken download on scene start. Might come back if downloads get recorded in scene
127    # reset_asset_ids = {}
128    # reset_obs = {}
129    # for ob in bpy.context.scene.collection.objects:
130    #     if ob.name[:12] == 'downloading ':
131    #         obn = ob.name
132    #
133    #         asset_data = ob['asset_data']
134    #
135    #         # obn.replace('#', '')
136    #         # if asset_data['id'] not in reset_asset_ids:
137    #
138    #         if reset_obs.get(asset_data['id']) is None:
139    #             reset_obs[asset_data['id']] = [obn]
140    #             reset_asset_ids[asset_data['id']] = asset_data
141    #         else:
142    #             reset_obs[asset_data['id']].append(obn)
143    # for asset_id in reset_asset_ids:
144    #     asset_data = reset_asset_ids[asset_id]
145    #     done = False
146    #     if check_existing(asset_data):
147    #         for obname in reset_obs[asset_id]:
148    #             downloader = s.collection.objects[obname]
149    #             done = try_finished_append(asset_data,
150    #                                        model_location=downloader.location,
151    #                                        model_rotation=downloader.rotation_euler)
152    #
153    #     if not done:
154    #         downloading = check_downloading(asset_data)
155    #         if not downloading:
156    #             print('redownloading %s' % asset_data['name'])
157    #             download(asset_data, downloaders=reset_obs[asset_id], delete=True)
158
159    # check for group users that have been deleted, remove the groups /files from the file...
160    # TODO scenes fixing part... download the assets not present on drive,
161    # and erase from scene linked files that aren't used in the scene.
162    # print('continue downlaods ', time.time() - t)
163    t = time.time()
164    check_missing()
165    # print('missing check', time.time() - t)
166
167
168def get_scene_id():
169    '''gets scene id and possibly also generates a new one'''
170    bpy.context.scene['uuid'] = bpy.context.scene.get('uuid', str(uuid.uuid4()))
171    return bpy.context.scene['uuid']
172
173
174def report_usages():
175    '''report the usage of assets to the server.'''
176    mt = time.time()
177    user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
178    api_key = user_preferences.api_key
179    sid = get_scene_id()
180    headers = utils.get_headers(api_key)
181    url = paths.get_api_url() + paths.BLENDERKIT_REPORT_URL
182
183    assets = {}
184    asset_obs = []
185    scene = bpy.context.scene
186    asset_usages = {}
187
188    for ob in scene.collection.objects:
189        if ob.get('asset_data') != None:
190            asset_obs.append(ob)
191
192    for ob in asset_obs:
193        asset_data = ob['asset_data']
194        abid = asset_data['assetBaseId']
195
196        if assets.get(abid) is None:
197            asset_usages[abid] = {'count': 1}
198            assets[abid] = asset_data
199        else:
200            asset_usages[abid]['count'] += 1
201
202    # brushes
203    for b in bpy.data.brushes:
204        if b.get('asset_data') != None:
205            abid = b['asset_data']['assetBaseId']
206            asset_usages[abid] = {'count': 1}
207            assets[abid] = b['asset_data']
208    # materials
209    for ob in scene.collection.objects:
210        for ms in ob.material_slots:
211            m = ms.material
212
213            if m is not None and m.get('asset_data') is not None:
214
215                abid = m['asset_data']['assetBaseId']
216                if assets.get(abid) is None:
217                    asset_usages[abid] = {'count': 1}
218                    assets[abid] = m['asset_data']
219                else:
220                    asset_usages[abid]['count'] += 1
221
222    assets_list = []
223    assets_reported = scene.get('assets reported', {})
224
225    new_assets_count = 0
226    for k in asset_usages.keys():
227        if k not in assets_reported.keys():
228            data = asset_usages[k]
229            list_item = {
230                'asset': k,
231                'usageCount': data['count'],
232                'proximitySet': data.get('proximity', [])
233            }
234            assets_list.append(list_item)
235            new_assets_count += 1
236        if k not in assets_reported.keys():
237            assets_reported[k] = True
238
239    scene['assets reported'] = assets_reported
240
241    if new_assets_count == 0:
242        utils.p('no new assets were added')
243        return;
244    usage_report = {
245        'scene': sid,
246        'reportType': 'save',
247        'assetusageSet': assets_list
248    }
249
250    au = scene.get('assets used', {})
251    ad = scene.get('assets deleted', {})
252
253    ak = assets.keys()
254    for k in au.keys():
255        if k not in ak:
256            ad[k] = au[k]
257        else:
258            if k in ad:
259                ad.pop(k)
260
261    # scene['assets used'] = {}
262    for k in ak:  # rewrite assets used.
263        scene['assets used'][k] = assets[k]
264
265    ###########check ratings herer too:
266    scene['assets rated'] = scene.get('assets rated', {})
267    for k in assets.keys():
268        scene['assets rated'][k] = scene['assets rated'].get(k, False)
269    thread = threading.Thread(target=utils.requests_post_thread, args=(url, usage_report, headers))
270    thread.start()
271    mt = time.time() - mt
272    print('report generation:                ', mt)
273
274
275def append_asset(asset_data, **kwargs):  # downloaders=[], location=None,
276    '''Link asset to the scene'''
277
278    file_names = paths.get_download_filenames(asset_data)
279    props = None
280    #####
281    # how to do particle  drop:
282    # link the group we are interested in( there are more groups in File!!!! , have to get the correct one!)
283    #
284    scene = bpy.context.scene
285
286    user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
287
288    if user_preferences.api_key == '':
289        user_preferences.asset_counter += 1
290
291    if asset_data['assetType'] == 'scene':
292        scene = append_link.append_scene(file_names[0], link=False, fake_user=False)
293        props = scene.blenderkit
294        parent = scene
295
296    if asset_data['assetType'] == 'model':
297        s = bpy.context.scene
298        downloaders = kwargs.get('downloaders')
299        s = bpy.context.scene
300        sprops = s.blenderkit_models
301        # TODO this is here because combinations of linking objects or appending groups are rather not-usefull
302        if sprops.append_method == 'LINK_COLLECTION':
303            sprops.append_link = 'LINK'
304            sprops.import_as = 'GROUP'
305        else:
306            sprops.append_link = 'APPEND'
307            sprops.import_as = 'INDIVIDUAL'
308
309        # copy for override
310        al = sprops.append_link
311        # set consistency for objects already in scene, otherwise this literally breaks blender :)
312        ain = asset_in_scene(asset_data)
313
314        # override based on history
315        if ain is not False:
316            if ain == 'LINKED':
317                al = 'LINK'
318            else:
319                al = 'APPEND'
320                if asset_data['assetType'] == 'model':
321                    source_parent = get_asset_in_scene(asset_data)
322                    parent, new_obs = duplicate_asset(source=source_parent, **kwargs)
323                    parent.location = kwargs['model_location']
324                    parent.rotation_euler = kwargs['model_rotation']
325                    # this is a case where asset is already in scene and should be duplicated instead.
326                    # there is a big chance that the duplication wouldn't work perfectly(hidden or unselectable objects)
327                    # so here we need to check and return if there was success
328                    # also, if it was successful, no other operations are needed , basically all asset data is already ready from the original asset
329                    if new_obs:
330                        bpy.ops.wm.undo_push_context(message='add %s to scene' % asset_data['name'])
331                        return
332
333        # first get conditions for append link
334        link = al == 'LINK'
335        # then append link
336        if downloaders:
337            for downloader in downloaders:
338                # this cares for adding particle systems directly to target mesh, but I had to block it now,
339                # because of the sluggishnes of it. Possibly re-enable when it's possible to do this faster?
340                if 0:  # 'particle_plants' in asset_data['tags']:
341                    append_link.append_particle_system(file_names[-1],
342                                                       target_object=kwargs['target_object'],
343                                                       rotation=downloader['rotation'],
344                                                       link=False,
345                                                       name=asset_data['name'])
346                    return
347
348                if link:
349                    parent, new_obs = append_link.link_collection(file_names[-1],
350                                                                 location=downloader['location'],
351                                                                 rotation=downloader['rotation'],
352                                                                 link=link,
353                                                                 name=asset_data['name'],
354                                                                 parent=kwargs.get('parent'))
355
356                else:
357
358                    parent, new_obs = append_link.append_objects(file_names[-1],
359                                                                location=downloader['location'],
360                                                                rotation=downloader['rotation'],
361                                                                link=link,
362                                                                name=asset_data['name'],
363                                                                parent=kwargs.get('parent'))
364                if parent.type == 'EMPTY' and link:
365                    bmin = asset_data['bbox_min']
366                    bmax = asset_data['bbox_max']
367                    size_min = min(1.0, (bmax[0] - bmin[0] + bmax[1] - bmin[1] + bmax[2] - bmin[2]) / 3)
368                    parent.empty_display_size = size_min
369
370        elif kwargs.get('model_location') is not None:
371            if link:
372                parent, new_obs = append_link.link_collection(file_names[-1],
373                                                             location=kwargs['model_location'],
374                                                             rotation=kwargs['model_rotation'],
375                                                             link=link,
376                                                             name=asset_data['name'],
377                                                             parent=kwargs.get('parent'))
378            else:
379                parent, new_obs = append_link.append_objects(file_names[-1],
380                                                            location=kwargs['model_location'],
381                                                            rotation=kwargs['model_rotation'],
382                                                            link=link,
383                                                            name=asset_data['name'],
384                                                            parent=kwargs.get('parent'))
385
386            # scale Empty for assets, so they don't clutter the scene.
387            if parent.type == 'EMPTY' and link:
388                bmin = asset_data['bbox_min']
389                bmax = asset_data['bbox_max']
390                size_min = min(1.0, (bmax[0] - bmin[0] + bmax[1] - bmin[1] + bmax[2] - bmin[2]) / 3)
391                parent.empty_display_size = size_min
392
393        if link:
394            group = parent.instance_collection
395
396            lib = group.library
397            lib['asset_data'] = asset_data
398
399    elif asset_data['assetType'] == 'brush':
400
401        # TODO if already in scene, should avoid reappending.
402        inscene = False
403        for b in bpy.data.brushes:
404
405            if b.blenderkit.id == asset_data['id']:
406                inscene = True
407                brush = b
408                break;
409        if not inscene:
410            brush = append_link.append_brush(file_names[-1], link=False, fake_user=False)
411
412            thumbnail_name = asset_data['thumbnail'].split(os.sep)[-1]
413            tempdir = paths.get_temp_dir('brush_search')
414            thumbpath = os.path.join(tempdir, thumbnail_name)
415            asset_thumbs_dir = paths.get_download_dirs('brush')[0]
416            asset_thumb_path = os.path.join(asset_thumbs_dir, thumbnail_name)
417            shutil.copy(thumbpath, asset_thumb_path)
418            brush.icon_filepath = asset_thumb_path
419
420        if bpy.context.view_layer.objects.active.mode == 'SCULPT':
421            bpy.context.tool_settings.sculpt.brush = brush
422        elif bpy.context.view_layer.objects.active.mode == 'TEXTURE_PAINT':  # could be just else, but for future possible more types...
423            bpy.context.tool_settings.image_paint.brush = brush
424        # TODO set brush by by asset data(user can be downloading while switching modes.)
425
426        # bpy.context.tool_settings.image_paint.brush = brush
427        props = brush.blenderkit
428        parent = brush
429
430    elif asset_data['assetType'] == 'material':
431        inscene = False
432        for m in bpy.data.materials:
433            if m.blenderkit.id == asset_data['id']:
434                inscene = True
435                material = m
436                break;
437        if not inscene:
438            material = append_link.append_material(file_names[-1], link=False, fake_user=False)
439        target_object = bpy.data.objects[kwargs['target_object']]
440
441        if len(target_object.material_slots) == 0:
442            target_object.data.materials.append(material)
443        else:
444            target_object.material_slots[kwargs['material_target_slot']].material = material
445
446        parent = material
447
448    scene['assets used'] = scene.get('assets used', {})
449    scene['assets used'][asset_data['assetBaseId']] = asset_data.copy()
450
451    scene['assets rated'] = scene.get('assets rated', {})
452
453    id = asset_data['assetBaseId']
454    scene['assets rated'][id] = scene['assets rated'].get(id, False)
455
456    parent['asset_data'] = asset_data  # TODO remove this??? should write to blenderkit Props?
457    bpy.ops.wm.undo_push_context(message='add %s to scene' % asset_data['name'])
458    # moving reporting to on save.
459    # report_use_success(asset_data['id'])
460
461
462# @bpy.app.handlers.persistent
463def timer_update():  # TODO might get moved to handle all blenderkit stuff, not to slow down.
464    '''check for running and finished downloads and react. write progressbars too.'''
465    global download_threads
466    if len(download_threads) == 0:
467        return 1.0
468    s = bpy.context.scene
469    for threaddata in download_threads:
470        t = threaddata[0]
471        asset_data = threaddata[1]
472        tcom = threaddata[2]
473
474        progress_bars = []
475        downloaders = []
476
477        if t.is_alive():  # set downloader size
478            sr = bpy.context.scene.get('search results')
479            if sr is not None:
480                for r in sr:
481                    if asset_data['id'] == r['id']:
482                        r['downloaded'] = tcom.progress
483
484        if not t.is_alive():
485            if tcom.error:
486                sprops = utils.get_search_props()
487                sprops.report = tcom.report
488                download_threads.remove(threaddata)
489                return
490            file_names = paths.get_download_filenames(asset_data)
491            wm = bpy.context.window_manager
492
493            at = asset_data['assetType']
494            if ((bpy.context.mode == 'OBJECT' and (at == 'model' \
495                                                   or at == 'material'))) \
496                    or ((at == 'brush') \
497                        and wm.get(
498                        'appendable') == True) or at == 'scene':  # don't do this stuff in editmode and other modes, just wait...
499                download_threads.remove(threaddata)
500
501                # duplicate file if the global and subdir are used in prefs
502                if len(file_names) == 2:  # todo this should try to check if both files exist and are ok.
503                    shutil.copyfile(file_names[0], file_names[1])
504
505                utils.p('appending asset')
506                # progress bars:
507
508                # we need to check if mouse isn't down, which means an operator can be running.
509                # Especially for sculpt mode, where appending a brush during a sculpt stroke causes crasehes
510                #
511
512                if tcom.passargs.get('redownload'):
513                    # handle lost libraries here:
514                    for l in bpy.data.libraries:
515                        if l.get('asset_data') is not None and l['asset_data']['id'] == asset_data['id']:
516                            l.filepath = file_names[-1]
517                            l.reload()
518                else:
519                    done = try_finished_append(asset_data, **tcom.passargs)
520                    if not done:
521                        at = asset_data['assetType']
522                        tcom.passargs['retry_counter'] = tcom.passargs.get('retry_counter', 0) + 1
523                        if at in ('model', 'material'):
524                            download(asset_data, **tcom.passargs)
525                        elif asset_data['assetType'] == 'material':
526                            download(asset_data, **tcom.passargs)
527                        elif asset_data['assetType'] == 'scene':
528                            download(asset_data, **tcom.passargs)
529                        elif asset_data['assetType'] == 'brush' or asset_data['assetType'] == 'texture':
530                            download(asset_data, **tcom.passargs)
531                    if bpy.context.scene['search results'] is not None and done:
532                        for sres in bpy.context.scene['search results']:
533                            if asset_data['id'] == sres['id']:
534                                sres['downloaded'] = 100
535
536                utils.p('finished download thread')
537    return .5
538
539
540def download_file(asset_data):
541    # this is a simple non-threaded way to download files for background resolution genenration tool
542    file_name = paths.get_download_filenames(asset_data)[0]  # prefer global dir if possible.
543
544    if check_existing(asset_data):
545        # this sends the thread for processing, where another check should occur, since the file might be corrupted.
546        utils.p('not downloading, already in db')
547        return file_name
548    preferences = bpy.context.preferences.addons['blenderkit'].preferences
549    api_key = preferences.api_key
550
551    with open(file_name, "wb") as f:
552        print("Downloading %s" % file_name)
553        headers = utils.get_headers(api_key)
554
555        response = requests.get(asset_data['url'], stream=True)
556        total_length = response.headers.get('Content-Length')
557
558        if total_length is None:  # no content length header
559            f.write(response.content)
560        else:
561            dl = 0
562            for data in response.iter_content(chunk_size=4096):
563                dl += len(data)
564                print(dl)
565                f.write(data)
566    return file_name
567
568
569class Downloader(threading.Thread):
570    def __init__(self, asset_data, tcom, scene_id, api_key):
571        super(Downloader, self).__init__()
572        self.asset_data = asset_data
573        self.tcom = tcom
574        self.scene_id = scene_id
575        self.api_key = api_key
576        self._stop_event = threading.Event()
577
578    def stop(self):
579        self._stop_event.set()
580
581    def stopped(self):
582        return self._stop_event.is_set()
583
584    # def main_download_thread(asset_data, tcom, scene_id, api_key):
585    def run(self):
586        '''try to download file from blenderkit'''
587        asset_data = self.asset_data
588        tcom = self.tcom
589        scene_id = self.scene_id
590        api_key = self.api_key
591
592        # TODO get real link here...
593        has_url = get_download_url(asset_data, scene_id, api_key, tcom=tcom)
594
595        if not has_url:
596            tasks_queue.add_task(
597                (ui.add_report, ('Failed to obtain download URL for %s.' % asset_data['name'], 5, colors.RED)))
598            return;
599        if tcom.error:
600            return
601        # only now we can check if the file already exists. This should have 2 levels, for materials and for brushes
602        # different than for the non free content. delete is here when called after failed append tries.
603        if check_existing(asset_data) and not tcom.passargs.get('delete'):
604            # this sends the thread for processing, where another check should occur, since the file might be corrupted.
605            tcom.downloaded = 100
606            utils.p('not downloading, trying to append again')
607            return;
608
609        file_name = paths.get_download_filenames(asset_data)[0]  # prefer global dir if possible.
610        # for k in asset_data:
611        #    print(asset_data[k])
612        if self.stopped():
613            utils.p('stopping download: ' + asset_data['name'])
614            return;
615
616        with open(file_name, "wb") as f:
617            print("Downloading %s" % file_name)
618            headers = utils.get_headers(api_key)
619
620            response = requests.get(asset_data['url'], stream=True)
621            total_length = response.headers.get('Content-Length')
622
623            if total_length is None:  # no content length header
624                f.write(response.content)
625            else:
626                tcom.file_size = int(total_length)
627                dl = 0
628                totdata = []
629                for data in response.iter_content(chunk_size=4096*32): #crashed here... why? investigate:
630                    dl += len(data)
631                    tcom.downloaded = dl
632                    tcom.progress = int(100 * tcom.downloaded / tcom.file_size)
633                    f.write(data)
634                    if self.stopped():
635                        utils.p('stopping download: ' + asset_data['name'])
636                        os.remove(file_name)
637                        return;
638
639
640class ThreadCom:  # object passed to threads to read background process stdout info
641    def __init__(self):
642        self.file_size = 1000000000000000  # property that gets written to.
643        self.downloaded = 0
644        self.lasttext = ''
645        self.error = False
646        self.report = ''
647        self.progress = 0.0
648        self.passargs = {}
649
650
651def download(asset_data, **kwargs):
652    '''start the download thread'''
653    user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
654    api_key = user_preferences.api_key
655    scene_id = get_scene_id()
656
657    tcom = ThreadCom()
658    tcom.passargs = kwargs
659
660    if kwargs.get('retry_counter', 0) > 3:
661        sprops = utils.get_search_props()
662        report = f"Maximum retries exceeded for {asset_data['name']}"
663        sprops.report = report
664        ui.add_report(report, 5, colors.RED)
665
666        utils.p(sprops.report)
667        return
668
669    # incoming data can be either directly dict from python, or blender id property
670    # (recovering failed downloads on reload)
671    if type(asset_data) == dict:
672        asset_data = copy.deepcopy(asset_data)
673    else:
674        asset_data = asset_data.to_dict()
675    readthread = Downloader(asset_data, tcom, scene_id, api_key)
676    readthread.start()
677
678    global download_threads
679    download_threads.append(
680        [readthread, asset_data, tcom])
681
682
683def check_downloading(asset_data, **kwargs):
684    ''' check if an asset is already downloading, if yes, just make a progress bar with downloader object.'''
685    global download_threads
686
687    downloading = False
688
689    for p in download_threads:
690        p_asset_data = p[1]
691        if p_asset_data['id'] == asset_data['id']:
692            at = asset_data['assetType']
693            if at in ('model', 'material'):
694                downloader = {'location': kwargs['model_location'],
695                              'rotation': kwargs['model_rotation']}
696                p[2].passargs['downloaders'].append(downloader)
697            downloading = True
698
699    return downloading
700
701
702def check_existing(asset_data):
703    ''' check if the object exists on the hard drive'''
704    fexists = False
705
706    file_names = paths.get_download_filenames(asset_data)
707
708    utils.p('check if file already exists')
709    if len(file_names) == 2:
710        # TODO this should check also for failed or running downloads.
711        # If download is running, assign just the running thread. if download isn't running but the file is wrong size,
712        #  delete file and restart download (or continue downoad? if possible.)
713        if os.path.isfile(file_names[0]) and not os.path.isfile(file_names[1]):
714            shutil.copy(file_names[0], file_names[1])
715        elif not os.path.isfile(file_names[0]) and os.path.isfile(
716                file_names[1]):  # only in case of changed settings or deleted/moved global dict.
717            shutil.copy(file_names[1], file_names[0])
718
719    if len(file_names) > 0 and os.path.isfile(file_names[0]):
720        fexists = True
721    return fexists
722
723
724def try_finished_append(asset_data, **kwargs):  # location=None, material_target=None):
725    ''' try to append asset, if not successfully delete source files.
726     This means probably wrong download, so download should restart'''
727    file_names = paths.get_download_filenames(asset_data)
728    done = False
729    utils.p('try to append already existing asset')
730    if len(file_names) > 0:
731        if os.path.isfile(file_names[-1]):
732            kwargs['name'] = asset_data['name']
733            try:
734                append_asset(asset_data, **kwargs)
735                done = True
736            except Exception as e:
737                print(e)
738                for f in file_names:
739                    try:
740                        os.remove(f)
741                    except Exception as e:
742                        # e = sys.exc_info()[0]
743                        print(e)
744                        pass;
745                done = False
746    return done
747
748
749def get_asset_in_scene(asset_data):
750    '''tries to find an appended copy of particular asset and duplicate it - so it doesn't have to be appended again.'''
751    scene = bpy.context.scene
752    for ob in bpy.context.scene.objects:
753        ad1 = ob.get('asset_data')
754        if not ad1:
755            continue
756        if ad1.get('assetBaseId') == asset_data['assetBaseId']:
757            return ob
758    return None
759
760
761def check_all_visible(obs):
762    '''checks all objects are visible, so they can be manipulated/copied.'''
763    for ob in obs:
764        if not ob.visible_get():
765            return False
766    return True
767
768
769def check_selectible(obs):
770    '''checks if all objects can be selected and selects them if possible.
771     this isn't only select_hide, but all possible combinations of collections e.t.c. so hard to check otherwise.'''
772    for ob in obs:
773        ob.select_set(True)
774        if not ob.select_get():
775            return False
776    return True
777
778
779def duplicate_asset(source, **kwargs):
780    '''Duplicate asset when it's already appended in the scene, so that blender's append doesn't create duplicated data.'''
781
782    # we need to save selection
783    sel = utils.selection_get()
784    bpy.ops.object.select_all(action='DESELECT')
785
786    # check visibility
787    obs = utils.get_hierarchy(source)
788    if not check_all_visible(obs):
789        return None
790    # check selectability and select in one run
791    if not check_selectible(obs):
792        return None
793
794    # duplicate the asset objects
795    bpy.ops.object.duplicate(linked=True)
796
797
798    nobs = bpy.context.selected_objects[:]
799    #get parent
800    for ob in nobs:
801        if ob.parent not in nobs:
802            parent = ob
803            break
804    # restore original selection
805    utils.selection_set(sel)
806    return parent , nobs
807
808
809def asset_in_scene(asset_data):
810    '''checks if the asset is already in scene. If yes, modifies asset data so the asset can be reached again.'''
811    scene = bpy.context.scene
812    au = scene.get('assets used', {})
813
814    id = asset_data['assetBaseId']
815    if id in au.keys():
816        ad = au[id]
817        if ad.get('file_name') != None:
818
819            asset_data['file_name'] = ad['file_name']
820            asset_data['url'] = ad['url']
821
822            # browse all collections since linked collections can have same name.
823            for c in bpy.data.collections:
824                if c.name == ad['name']:
825                    # there can also be more linked collections with same name, we need to check id.
826                    if c.library and c.library.get('asset_data') and c.library['asset_data']['assetBaseId'] == id:
827                        return 'LINKED'
828            return 'APPENDED'
829    return False
830
831
832def fprint(text):
833    print('###################################################################################')
834    print('\n\n\n')
835    print(text)
836    print('\n\n\n')
837    print('###################################################################################')
838
839
840def get_download_url(asset_data, scene_id, api_key, tcom=None):
841    ''''retrieves the download url. The server checks if user can download the item.'''
842    mt = time.time()
843
844    headers = utils.get_headers(api_key)
845
846    data = {
847        'scene_uuid': scene_id
848    }
849    r = None
850
851    try:
852        r = rerequests.get(asset_data['download_url'], params=data, headers=headers)
853    except Exception as e:
854        print(e)
855        if tcom is not None:
856            tcom.error = True
857    if r == None:
858        if tcom is not None:
859            tcom.report = 'Connection Error'
860            tcom.error = True
861        return 'Connection Error'
862
863
864    if r.status_code < 400:
865        data = r.json()
866        url = data['filePath']
867        asset_data['url'] = url
868        asset_data['file_name'] = paths.extract_filename_from_url(url)
869        return True
870
871    if r.status_code == 403:
872        r = 'You need Full plan to get this item.'
873        # r1 = 'All materials and brushes are available for free. Only users registered to Standard plan can use all models.'
874        # tasks_queue.add_task((ui.add_report, (r1, 5, colors.RED)))
875        if tcom is not None:
876            tcom.report = r
877            tcom.error = True
878
879    elif r.status_code >= 500:
880        utils.p(r.text)
881        if tcom is not None:
882            tcom.report = 'Server error'
883            tcom.error = True
884    return False
885
886
887def start_download(asset_data, **kwargs):
888    '''
889    check if file isn't downloading or doesn't exist, then start new download
890    '''
891    # first check if the asset is already in scene. We can use that asset without checking with server
892    quota_ok = asset_in_scene(asset_data) is not False
893
894    # otherwise, check on server
895
896    s = bpy.context.scene
897    done = False
898    # is the asseet being currently downloaded?
899    downloading = check_downloading(asset_data, **kwargs)
900    if not downloading:
901        # check if there are files already. This check happens 2x once here(for free assets),
902        # once in thread(for non-free)
903        fexists = check_existing(asset_data)
904
905        if fexists and quota_ok:
906            done = try_finished_append(asset_data, **kwargs)
907        # else:
908        #     props = utils.get_search_props()
909        #     props.report = str('asset ')
910        if not done:
911            at = asset_data['assetType']
912            if at in ('model', 'material'):
913                downloader = {'location': kwargs['model_location'],
914                              'rotation': kwargs['model_rotation']}
915                download(asset_data, downloaders=[downloader], **kwargs)
916
917            elif asset_data['assetType'] == 'scene':
918                download(asset_data, **kwargs)
919            elif asset_data['assetType'] == 'brush' or asset_data['assetType'] == 'texture':
920                download(asset_data)
921
922
923asset_types = (
924    ('MODEL', 'Model', 'set of objects'),
925    ('SCENE', 'Scene', 'scene'),
926    ('MATERIAL', 'Material', 'any .blend Material'),
927    ('TEXTURE', 'Texture', 'a texture, or texture set'),
928    ('BRUSH', 'Brush', 'brush, can be any type of blender brush'),
929    ('ADDON', 'Addon', 'addnon'),
930)
931
932
933class BlenderkitKillDownloadOperator(bpy.types.Operator):
934    """Kill a download"""
935    bl_idname = "scene.blenderkit_download_kill"
936    bl_label = "BlenderKit Kill Asset Download"
937    bl_options = {'REGISTER', 'INTERNAL'}
938
939    thread_index: IntProperty(name="Thread index", description='index of the thread to kill', default=-1)
940
941    def execute(self, context):
942        global download_threads
943        td = download_threads[self.thread_index]
944        download_threads.remove(td)
945        td[0].stop()
946        return {'FINISHED'}
947
948
949class BlenderkitDownloadOperator(bpy.types.Operator):
950    """Download and link asset to scene. Only link if asset already available locally."""
951    bl_idname = "scene.blenderkit_download"
952    bl_label = "BlenderKit Asset Download"
953    bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
954
955    asset_type: EnumProperty(
956        name="Type",
957        items=asset_types,
958        description="Type of download",
959        default="MODEL",
960    )
961    asset_index: IntProperty(name="Asset Index", description='asset index in search results', default=-1)
962
963    asset_base_id: StringProperty(
964        name="Asset base Id",
965        description="Asset base id, used instead of search result index.",
966        default="")
967
968    target_object: StringProperty(
969        name="Target Object",
970        description="Material or object target for replacement",
971        default="")
972
973    material_target_slot: IntProperty(name="Asset Index", description='asset index in search results', default=0)
974    model_location: FloatVectorProperty(name='Asset Location', default=(0, 0, 0))
975    model_rotation: FloatVectorProperty(name='Asset Rotation', default=(0, 0, 0))
976
977    replace: BoolProperty(name='Replace', description='replace selection with the asset', default=False)
978
979    cast_parent: StringProperty(
980        name="Particles Target Object",
981        description="",
982        default="")
983
984    # @classmethod
985    # def poll(cls, context):
986    #     return bpy.context.window_manager.BlenderKitModelThumbnails is not ''
987
988    def execute(self, context):
989        s = bpy.context.scene
990
991        if self.asset_index > -1:
992            # either get the data from search results
993            sr = s['search results']
994            asset_data = sr[
995                self.asset_index].to_dict()  # TODO CHECK ALL OCCURRENCES OF PASSING BLENDER ID PROPS TO THREADS!
996            asset_base_id = asset_data['assetBaseId']
997        else:
998            # or from the scene.
999            asset_base_id = self.asset_base_id
1000
1001        au = s.get('assets used')
1002        if au == None:
1003            s['assets used'] = {}
1004        if asset_base_id in s.get('assets used'):
1005            # already used assets have already download link and especially file link.
1006            asset_data = s['assets used'][asset_base_id].to_dict()
1007
1008        atype = asset_data['assetType']
1009        if bpy.context.mode != 'OBJECT' and (
1010                atype == 'model' or atype == 'material') and bpy.context.view_layer.objects.active is not None:
1011            bpy.ops.object.mode_set(mode='OBJECT')
1012
1013        if self.replace:  # cleanup first, assign later.
1014            obs = utils.get_selected_replace_adepts()
1015            # print(obs)
1016            for ob in obs:
1017                print('replace attempt ', ob.name)
1018                if self.asset_base_id != '':
1019                    # this is for a case when replace is called from a panel, this makes the first of the objects not replacable.
1020                    if ob.get('asset_data') is not None and ob['asset_data']['assetBaseId'] == self.asset_base_id:
1021                        print('skipping this oneli')
1022                        continue;
1023
1024                kwargs = {
1025                    'cast_parent': self.cast_parent,
1026                    'target_object': ob.name,
1027                    'material_target_slot': ob.active_material_index,
1028                    'model_location': tuple(ob.matrix_world.translation),
1029                    'model_rotation': tuple(ob.matrix_world.to_euler()),
1030                    'replace': False,
1031                    'parent': ob.parent
1032                }
1033                utils.delete_hierarchy(ob)
1034                start_download(asset_data, **kwargs)
1035        else:
1036            kwargs = {
1037                'cast_parent': self.cast_parent,
1038                'target_object': self.target_object,
1039                'material_target_slot': self.material_target_slot,
1040                'model_location': tuple(self.model_location),
1041                'model_rotation': tuple(self.model_rotation),
1042                'replace': False
1043            }
1044
1045            start_download(asset_data, **kwargs)
1046        return {'FINISHED'}
1047
1048
1049def register_download():
1050    bpy.utils.register_class(BlenderkitDownloadOperator)
1051    bpy.utils.register_class(BlenderkitKillDownloadOperator)
1052    bpy.app.handlers.load_post.append(scene_load)
1053    bpy.app.handlers.save_pre.append(scene_save)
1054    user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
1055    if user_preferences.use_timers:
1056        bpy.app.timers.register(timer_update)
1057
1058
1059def unregister_download():
1060    bpy.utils.unregister_class(BlenderkitDownloadOperator)
1061    bpy.utils.unregister_class(BlenderkitKillDownloadOperator)
1062    bpy.app.handlers.load_post.remove(scene_load)
1063    bpy.app.handlers.save_pre.remove(scene_save)
1064    if bpy.app.timers.is_registered(timer_update):
1065        bpy.app.timers.unregister(timer_update)
1066