hydrus/hydrus/client/caches/ClientCaches.py

1087 lines
37 KiB
Python
Raw Normal View History

2020-05-20 21:36:02 +00:00
import collections
import json
import os
import threading
import time
2020-04-22 21:00:35 +00:00
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusExceptions
2022-12-07 22:41:53 +00:00
from hydrus.core import HydrusFileHandling
2020-04-22 21:00:35 +00:00
from hydrus.core import HydrusThreading
from hydrus.core import HydrusData
from hydrus.core import HydrusGlobals as HG
2023-04-19 20:38:13 +00:00
from hydrus.core import HydrusTime
2023-10-04 20:51:17 +00:00
from hydrus.core.images import HydrusBlurhash
from hydrus.core.images import HydrusImageHandling
2015-03-18 21:46:29 +00:00
2020-07-29 20:52:44 +00:00
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientFiles
from hydrus.client import ClientImageHandling
from hydrus.client import ClientParsing
from hydrus.client import ClientRendering
2023-02-15 21:26:44 +00:00
from hydrus.client import ClientThreading
2023-06-28 20:29:14 +00:00
from hydrus.client.caches import ClientCachesBase
2020-07-29 20:52:44 +00:00
2019-12-05 05:29:32 +00:00
class LocalBooruCache( object ):
2018-12-05 22:35:30 +00:00
def __init__( self, controller ):
self._controller = controller
self._lock = threading.Lock()
2019-12-05 05:29:32 +00:00
self._RefreshShares()
self._controller.sub( self, 'RefreshShares', 'refresh_local_booru_shares' )
self._controller.sub( self, 'RefreshShares', 'restart_client_server_service' )
2018-12-05 22:35:30 +00:00
2019-12-05 05:29:32 +00:00
def _CheckDataUsage( self ):
2018-12-05 22:35:30 +00:00
2019-12-05 05:29:32 +00:00
if not self._local_booru_service.BandwidthOK():
raise HydrusExceptions.InsufficientCredentialsException( 'This booru has used all its monthly data. Please try again next month.' )
2018-12-05 22:35:30 +00:00
2019-12-05 05:29:32 +00:00
def _CheckFileAuthorised( self, share_key, hash ):
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
self._CheckShareAuthorised( share_key )
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
info = self._GetInfo( share_key )
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
if hash not in info[ 'hashes_set' ]:
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
raise HydrusExceptions.NotFoundException( 'That file was not found in that share.' )
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
def _CheckShareAuthorised( self, share_key ):
self._CheckDataUsage()
info = self._GetInfo( share_key )
timeout = info[ 'timeout' ]
2023-04-19 20:38:13 +00:00
if timeout is not None and HydrusTime.TimeHasPassed( timeout ):
2019-06-05 19:42:39 +00:00
2020-05-06 21:31:41 +00:00
raise HydrusExceptions.NotFoundException( 'This share has expired.' )
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
def _GetInfo( self, share_key ):
try: info = self._keys_to_infos[ share_key ]
except: raise HydrusExceptions.NotFoundException( 'Did not find that share on this booru.' )
if info is None:
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
info = self._controller.Read( 'local_booru_share', share_key )
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
hashes = info[ 'hashes' ]
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
info[ 'hashes_set' ] = set( hashes )
media_results = self._controller.Read( 'media_results', hashes )
info[ 'media_results' ] = media_results
hashes_to_media_results = { media_result.GetHash() : media_result for media_result in media_results }
info[ 'hashes_to_media_results' ] = hashes_to_media_results
self._keys_to_infos[ share_key ] = info
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
return info
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
def _RefreshShares( self ):
self._local_booru_service = self._controller.services_manager.GetService( CC.LOCAL_BOORU_SERVICE_KEY )
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
self._keys_to_infos = {}
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
share_keys = self._controller.Read( 'local_booru_share_keys' )
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
for share_key in share_keys:
self._keys_to_infos[ share_key ] = None
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
def CheckShareAuthorised( self, share_key ):
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
with self._lock: self._CheckShareAuthorised( share_key )
2019-06-05 19:42:39 +00:00
2019-12-05 05:29:32 +00:00
def CheckFileAuthorised( self, share_key, hash ):
2018-12-05 22:35:30 +00:00
2019-12-05 05:29:32 +00:00
with self._lock: self._CheckFileAuthorised( share_key, hash )
2018-12-05 22:35:30 +00:00
2019-12-05 05:29:32 +00:00
def GetGalleryInfo( self, share_key ):
2015-03-18 21:46:29 +00:00
with self._lock:
self._CheckShareAuthorised( share_key )
info = self._GetInfo( share_key )
name = info[ 'name' ]
text = info[ 'text' ]
timeout = info[ 'timeout' ]
media_results = info[ 'media_results' ]
return ( name, text, timeout, media_results )
def GetMediaResult( self, share_key, hash ):
with self._lock:
info = self._GetInfo( share_key )
media_result = info[ 'hashes_to_media_results' ][ hash ]
return media_result
def GetPageInfo( self, share_key, hash ):
with self._lock:
self._CheckFileAuthorised( share_key, hash )
info = self._GetInfo( share_key )
name = info[ 'name' ]
text = info[ 'text' ]
timeout = info[ 'timeout' ]
media_result = info[ 'hashes_to_media_results' ][ hash ]
return ( name, text, timeout, media_result )
2019-01-30 22:14:54 +00:00
def RefreshShares( self, *args, **kwargs ):
2015-03-18 21:46:29 +00:00
with self._lock:
self._RefreshShares()
2018-04-25 22:07:52 +00:00
class ParsingCache( object ):
def __init__( self ):
2023-04-19 20:38:13 +00:00
self._next_clean_cache_time = HydrusTime.GetNow()
2018-08-08 20:29:54 +00:00
2018-04-25 22:07:52 +00:00
self._html_to_soups = {}
self._json_to_jsons = {}
self._lock = threading.Lock()
def _CleanCache( self ):
2023-04-19 20:38:13 +00:00
if HydrusTime.TimeHasPassed( self._next_clean_cache_time ):
2018-04-25 22:07:52 +00:00
2018-08-08 20:29:54 +00:00
for cache in ( self._html_to_soups, self._json_to_jsons ):
dead_datas = set()
2019-09-05 00:05:32 +00:00
for ( data, ( last_accessed, parsed_object ) ) in cache.items():
2018-08-08 20:29:54 +00:00
2023-04-19 20:38:13 +00:00
if HydrusTime.TimeHasPassed( last_accessed + 10 ):
2019-12-05 05:29:32 +00:00
dead_datas.add( data )
2019-03-20 21:22:10 +00:00
2015-11-18 22:44:07 +00:00
2019-12-05 05:29:32 +00:00
for dead_data in dead_datas:
2016-04-06 19:52:45 +00:00
2019-12-05 05:29:32 +00:00
del cache[ dead_data ]
2016-04-06 19:52:45 +00:00
2015-08-05 18:42:35 +00:00
2023-04-19 20:38:13 +00:00
self._next_clean_cache_time = HydrusTime.GetNow() + 5
2019-03-20 21:22:10 +00:00
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
def CleanCache( self ):
2019-03-20 21:22:10 +00:00
with self._lock:
2016-04-06 19:52:45 +00:00
2019-12-05 05:29:32 +00:00
self._CleanCache()
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
def GetJSON( self, json_text ):
2015-08-05 18:42:35 +00:00
2019-03-20 21:22:10 +00:00
with self._lock:
2015-08-05 18:42:35 +00:00
2023-04-19 20:38:13 +00:00
now = HydrusTime.GetNow()
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
if json_text not in self._json_to_jsons:
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
json_object = json.loads( json_text )
2019-03-20 21:22:10 +00:00
2019-12-05 05:29:32 +00:00
self._json_to_jsons[ json_text ] = ( now, json_object )
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
( last_accessed, json_object ) = self._json_to_jsons[ json_text ]
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
if last_accessed != now:
self._json_to_jsons[ json_text ] = ( now, json_object )
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
if len( self._json_to_jsons ) > 10:
self._CleanCache()
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
return json_object
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
def GetSoup( self, html ):
2015-11-18 22:44:07 +00:00
2015-08-05 18:42:35 +00:00
with self._lock:
2023-04-19 20:38:13 +00:00
now = HydrusTime.GetNow()
2019-03-20 21:22:10 +00:00
2019-12-05 05:29:32 +00:00
if html not in self._html_to_soups:
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
soup = ClientParsing.GetSoup( html )
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
self._html_to_soups[ html ] = ( now, soup )
2019-03-20 21:22:10 +00:00
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
( last_accessed, soup ) = self._html_to_soups[ html ]
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
if last_accessed != now:
2015-11-11 21:20:41 +00:00
2019-12-05 05:29:32 +00:00
self._html_to_soups[ html ] = ( now, soup )
2019-03-20 21:22:10 +00:00
2019-12-05 05:29:32 +00:00
if len( self._html_to_soups ) > 10:
2019-03-20 21:22:10 +00:00
2019-12-05 05:29:32 +00:00
self._CleanCache()
2015-11-11 21:20:41 +00:00
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
return soup
2015-08-05 18:42:35 +00:00
2021-05-05 20:12:11 +00:00
class ImageRendererCache( object ):
2019-12-05 05:29:32 +00:00
def __init__( self, controller ):
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
self._controller = controller
2019-03-20 21:22:10 +00:00
2022-04-20 20:18:56 +00:00
cache_size = self._controller.new_options.GetInteger( 'image_cache_size' )
2019-12-05 05:29:32 +00:00
cache_timeout = self._controller.new_options.GetInteger( 'image_cache_timeout' )
2023-06-28 20:29:14 +00:00
self._data_cache = ClientCachesBase.DataCache( self._controller, 'image cache', cache_size, timeout = cache_timeout )
2015-08-05 18:42:35 +00:00
2021-06-09 20:28:09 +00:00
self._controller.sub( self, 'NotifyNewOptions', 'notify_new_options' )
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
def Clear( self ):
2018-03-28 21:55:58 +00:00
2019-12-05 05:29:32 +00:00
self._data_cache.Clear()
def GetImageRenderer( self, media ):
hash = media.GetHash()
key = hash
result = self._data_cache.GetIfHasData( key )
if result is None:
2018-03-28 21:55:58 +00:00
2019-12-05 05:29:32 +00:00
image_renderer = ClientRendering.ImageRenderer( media )
2018-03-28 21:55:58 +00:00
2021-05-05 20:12:11 +00:00
# we are no longer going to let big lads flush the whole cache. they can render on demand
2021-06-09 20:28:09 +00:00
image_cache_storage_limit_percentage = self._controller.new_options.GetInteger( 'image_cache_storage_limit_percentage' )
if image_renderer.GetEstimatedMemoryFootprint() < self._data_cache.GetSizeLimit() * ( image_cache_storage_limit_percentage / 100 ):
2021-05-05 20:12:11 +00:00
self._data_cache.AddData( key, image_renderer )
2019-12-05 05:29:32 +00:00
else:
2018-11-07 23:09:40 +00:00
2019-12-05 05:29:32 +00:00
image_renderer = result
2018-03-28 21:55:58 +00:00
2019-12-05 05:29:32 +00:00
return image_renderer
2018-03-28 21:55:58 +00:00
2019-12-05 05:29:32 +00:00
def HasImageRenderer( self, hash ):
2015-08-05 18:42:35 +00:00
2019-12-05 05:29:32 +00:00
key = hash
return self._data_cache.HasData( key )
2015-08-05 18:42:35 +00:00
2021-06-09 20:28:09 +00:00
def NotifyNewOptions( self ):
2022-04-20 20:18:56 +00:00
cache_size = self._controller.new_options.GetInteger( 'image_cache_size' )
2021-06-09 20:28:09 +00:00
cache_timeout = self._controller.new_options.GetInteger( 'image_cache_timeout' )
self._data_cache.SetCacheSizeAndTimeout( cache_size, cache_timeout )
2021-05-05 20:12:11 +00:00
def PrefetchImageRenderer( self, media ):
( width, height ) = media.GetResolution()
# essentially, we are not going to prefetch giganto images any more. they can render on demand and not mess our queue
2021-06-09 20:28:09 +00:00
image_cache_prefetch_limit_percentage = self._controller.new_options.GetInteger( 'image_cache_prefetch_limit_percentage' )
if width * height * 3 < self._data_cache.GetSizeLimit() * ( image_cache_prefetch_limit_percentage / 100 ):
2021-05-05 20:12:11 +00:00
self.GetImageRenderer( media )
class ImageTileCache( object ):
def __init__( self, controller ):
self._controller = controller
cache_size = self._controller.new_options.GetInteger( 'image_tile_cache_size' )
cache_timeout = self._controller.new_options.GetInteger( 'image_tile_cache_timeout' )
2023-06-28 20:29:14 +00:00
self._data_cache = ClientCachesBase.DataCache( self._controller, 'image tile cache', cache_size, timeout = cache_timeout )
2021-05-05 20:12:11 +00:00
2021-06-09 20:28:09 +00:00
self._controller.sub( self, 'NotifyNewOptions', 'notify_new_options' )
2022-11-16 21:34:30 +00:00
self._controller.sub( self, 'Clear', 'clear_image_tile_cache' )
2021-06-09 20:28:09 +00:00
2021-05-05 20:12:11 +00:00
def Clear( self ):
self._data_cache.Clear()
2021-05-12 20:49:20 +00:00
def GetTile( self, image_renderer: ClientRendering.ImageRenderer, media, clip_rect, target_resolution ):
2021-05-05 20:12:11 +00:00
hash = media.GetHash()
2021-05-12 20:49:20 +00:00
key = (
hash,
clip_rect.left(),
clip_rect.top(),
clip_rect.right(),
clip_rect.bottom(),
target_resolution.width(),
target_resolution.height()
)
2021-05-05 20:12:11 +00:00
result = self._data_cache.GetIfHasData( key )
if result is None:
qt_pixmap = image_renderer.GetQtPixmap( clip_rect = clip_rect, target_resolution = target_resolution )
tile = ClientRendering.ImageTile( hash, clip_rect, qt_pixmap )
self._data_cache.AddData( key, tile )
else:
tile = result
return tile
2021-06-09 20:28:09 +00:00
def NotifyNewOptions( self ):
cache_size = self._controller.new_options.GetInteger( 'image_tile_cache_size' )
cache_timeout = self._controller.new_options.GetInteger( 'image_tile_cache_timeout' )
self._data_cache.SetCacheSizeAndTimeout( cache_size, cache_timeout )
2019-03-20 21:22:10 +00:00
class ThumbnailCache( object ):
2015-08-05 18:42:35 +00:00
2015-11-25 22:00:57 +00:00
def __init__( self, controller ):
self._controller = controller
2015-08-05 18:42:35 +00:00
2022-04-20 20:18:56 +00:00
cache_size = self._controller.new_options.GetInteger( 'thumbnail_cache_size' )
2019-03-20 21:22:10 +00:00
cache_timeout = self._controller.new_options.GetInteger( 'thumbnail_cache_timeout' )
2016-09-14 18:03:59 +00:00
2023-06-28 20:29:14 +00:00
self._data_cache = ClientCachesBase.DataCache( self._controller, 'thumbnail cache', cache_size, timeout = cache_timeout )
2015-08-05 18:42:35 +00:00
2019-04-03 22:45:57 +00:00
self._magic_mime_thumbnail_ease_score_lookup = {}
self._InitialiseMagicMimeScores()
2015-08-05 18:42:35 +00:00
self._lock = threading.Lock()
2019-03-20 21:22:10 +00:00
self._thumbnail_error_occurred = False
2015-08-05 18:42:35 +00:00
2019-03-20 21:22:10 +00:00
self._waterfall_queue_quick = set()
2019-04-10 22:50:53 +00:00
self._waterfall_queue = []
2019-11-28 01:11:46 +00:00
self._waterfall_queue_empty_event = threading.Event()
2019-04-10 22:50:53 +00:00
self._delayed_regeneration_queue_quick = set()
self._delayed_regeneration_queue = []
2015-11-11 21:20:41 +00:00
2023-10-04 20:51:17 +00:00
self._allow_blurhash_fallback = self._controller.new_options.GetBoolean( 'allow_blurhash_fallback' )
2019-03-20 21:22:10 +00:00
self._waterfall_event = threading.Event()
self._special_thumbs = {}
self.Clear()
2019-12-05 05:29:32 +00:00
self._controller.CallToThreadLongRunning( self.MainLoop )
2019-03-20 21:22:10 +00:00
2019-10-02 23:38:59 +00:00
self._controller.sub( self, 'Clear', 'reset_thumbnail_cache' )
2019-03-20 21:22:10 +00:00
self._controller.sub( self, 'ClearThumbnails', 'clear_thumbnails' )
2021-06-09 20:28:09 +00:00
self._controller.sub( self, 'NotifyNewOptions', 'notify_new_options' )
2015-11-11 21:20:41 +00:00
2023-09-27 21:12:55 +00:00
def _GetBestRecoveryThumbnailHydrusBitmap( self, display_media ):
2023-10-04 20:51:17 +00:00
if self._allow_blurhash_fallback:
2023-09-27 21:12:55 +00:00
2023-10-04 20:51:17 +00:00
blurhash = display_media.GetFileInfoManager().blurhash
if blurhash is not None:
2023-09-27 21:12:55 +00:00
2023-10-04 20:51:17 +00:00
try:
( media_width, media_height ) = display_media.GetResolution()
bounding_dimensions = self._controller.options[ 'thumbnail_dimensions' ]
thumbnail_scale_type = self._controller.new_options.GetInteger( 'thumbnail_scale_type' )
thumbnail_dpr_percent = HG.client_controller.new_options.GetInteger( 'thumbnail_dpr_percent' )
( expected_width, expected_height ) = HydrusImageHandling.GetThumbnailResolution( ( media_width, media_height ), bounding_dimensions, thumbnail_scale_type, thumbnail_dpr_percent )
2023-10-04 20:51:17 +00:00
numpy_image = HydrusBlurhash.GetNumpyFromBlurhash( blurhash, expected_width, expected_height )
hydrus_bitmap = ClientRendering.GenerateHydrusBitmapFromNumPyImage( numpy_image )
return hydrus_bitmap
except:
pass
2023-09-27 21:12:55 +00:00
return self._special_thumbs[ 'hydrus' ]
2019-03-27 22:01:02 +00:00
def _GetThumbnailHydrusBitmap( self, display_media ):
2017-04-05 21:16:40 +00:00
2023-09-27 21:12:55 +00:00
if HG.blurhash_mode:
return self._GetBestRecoveryThumbnailHydrusBitmap( display_media )
2019-03-20 21:22:10 +00:00
hash = display_media.GetHash()
2015-08-05 18:42:35 +00:00
2019-03-20 21:22:10 +00:00
locations_manager = display_media.GetLocationsManager()
try:
2016-09-14 18:03:59 +00:00
2023-09-27 21:12:55 +00:00
thumbnail_path = self._controller.client_files_manager.GetThumbnailPath( display_media )
2016-09-14 18:03:59 +00:00
2019-03-20 21:22:10 +00:00
except HydrusExceptions.FileMissingException as e:
if locations_manager.IsLocal():
2017-04-05 21:16:40 +00:00
2019-03-27 22:01:02 +00:00
summary = 'Unable to get thumbnail for file {}.'.format( hash.hex() )
2017-04-05 21:16:40 +00:00
2023-02-15 21:26:44 +00:00
self._HandleThumbnailException( hash, e, summary )
2017-04-05 21:16:40 +00:00
2016-09-14 18:03:59 +00:00
2023-09-27 21:12:55 +00:00
return self._GetBestRecoveryThumbnailHydrusBitmap( display_media )
2016-09-14 18:03:59 +00:00
2019-03-20 21:22:10 +00:00
2023-02-15 21:26:44 +00:00
thumbnail_mime = HC.IMAGE_JPEG
2019-03-20 21:22:10 +00:00
try:
2016-09-14 18:03:59 +00:00
2023-09-27 21:12:55 +00:00
thumbnail_mime = HydrusFileHandling.GetThumbnailMime( thumbnail_path )
2022-12-07 22:41:53 +00:00
2023-09-27 21:12:55 +00:00
numpy_image = ClientImageHandling.GenerateNumPyImage( thumbnail_path, thumbnail_mime )
2016-09-14 18:03:59 +00:00
2019-03-20 21:22:10 +00:00
except Exception as e:
try:
2016-09-14 18:03:59 +00:00
2019-03-20 21:22:10 +00:00
# file is malformed, let's force a regen
2019-05-22 22:35:06 +00:00
self._controller.files_maintenance_manager.RunJobImmediately( [ display_media ], ClientFiles.REGENERATE_FILE_DATA_JOB_FORCE_THUMBNAIL, pub_job_key = False )
2019-03-20 21:22:10 +00:00
except Exception as e:
2020-11-11 22:20:16 +00:00
summary = 'The thumbnail for file {} was not loadable. An attempt to regenerate it failed.'.format( hash.hex() )
2019-03-20 21:22:10 +00:00
2023-02-15 21:26:44 +00:00
self._HandleThumbnailException( hash, e, summary )
2019-03-20 21:22:10 +00:00
2023-09-27 21:12:55 +00:00
return self._GetBestRecoveryThumbnailHydrusBitmap( display_media )
2016-09-14 18:03:59 +00:00
2019-03-20 21:22:10 +00:00
try:
2023-09-27 21:12:55 +00:00
numpy_image = ClientImageHandling.GenerateNumPyImage( thumbnail_path, thumbnail_mime )
2019-03-20 21:22:10 +00:00
except Exception as e:
2020-11-11 22:20:16 +00:00
summary = 'The thumbnail for file {} was not loadable. It was regenerated, but that file would not render either. Your image libraries or hard drive connection are unreliable. Please inform the hydrus developer what has happened.'.format( hash.hex() )
2019-03-20 21:22:10 +00:00
2023-02-15 21:26:44 +00:00
self._HandleThumbnailException( hash, e, summary )
2019-03-20 21:22:10 +00:00
2023-09-27 21:12:55 +00:00
return self._GetBestRecoveryThumbnailHydrusBitmap( display_media )
2019-03-20 21:22:10 +00:00
2016-09-14 18:03:59 +00:00
2019-05-08 21:06:42 +00:00
( current_width, current_height ) = HydrusImageHandling.GetResolutionNumPy( numpy_image )
2019-03-20 21:22:10 +00:00
2019-05-22 22:35:06 +00:00
( media_width, media_height ) = display_media.GetResolution()
2022-02-02 22:14:01 +00:00
bounding_dimensions = self._controller.options[ 'thumbnail_dimensions' ]
thumbnail_scale_type = self._controller.new_options.GetInteger( 'thumbnail_scale_type' )
2022-12-21 22:00:27 +00:00
thumbnail_dpr_percent = HG.client_controller.new_options.GetInteger( 'thumbnail_dpr_percent' )
2022-02-02 22:14:01 +00:00
( expected_width, expected_height ) = HydrusImageHandling.GetThumbnailResolution( ( media_width, media_height ), bounding_dimensions, thumbnail_scale_type, thumbnail_dpr_percent )
2019-03-20 21:22:10 +00:00
2019-04-24 22:18:50 +00:00
exactly_as_expected = current_width == expected_width and current_height == expected_height
rotation_exception = current_width == expected_height and current_height == expected_width
correct_size = exactly_as_expected or rotation_exception
2019-03-20 21:22:10 +00:00
2019-03-27 22:01:02 +00:00
if not correct_size:
2015-08-05 18:42:35 +00:00
2022-02-02 22:14:01 +00:00
numpy_image = HydrusImageHandling.ResizeNumPyImage( numpy_image, ( expected_width, expected_height ) )
2015-08-05 18:42:35 +00:00
2022-02-02 22:14:01 +00:00
if locations_manager.IsLocal():
# we have the master file, so we should regen the thumb from source
2019-04-03 22:45:57 +00:00
if HG.file_report_mode:
2019-03-27 22:01:02 +00:00
2022-12-21 22:00:27 +00:00
HydrusData.ShowText( 'Thumbnail {} wrong size ({}x{} instead of {}x{}), scheduling regeneration from source.'.format( hash.hex(), current_width, current_height, expected_width, expected_height ) )
2019-03-27 22:01:02 +00:00
2019-03-20 21:22:10 +00:00
2022-02-02 22:14:01 +00:00
delayed_item = display_media.GetMediaResult()
2019-03-20 21:22:10 +00:00
2022-02-02 22:14:01 +00:00
with self._lock:
2019-03-27 22:01:02 +00:00
2022-02-02 22:14:01 +00:00
if delayed_item not in self._delayed_regeneration_queue_quick:
2019-04-10 22:50:53 +00:00
2022-02-02 22:14:01 +00:00
self._delayed_regeneration_queue_quick.add( delayed_item )
2019-04-10 22:50:53 +00:00
2022-02-02 22:14:01 +00:00
self._delayed_regeneration_queue.append( delayed_item )
2019-04-10 22:50:53 +00:00
else:
2022-02-02 22:14:01 +00:00
# we do not have the master file, so we have to scale up from what we have
2019-04-10 22:50:53 +00:00
2022-02-02 22:14:01 +00:00
if HG.file_report_mode:
2019-04-10 22:50:53 +00:00
2022-12-21 22:00:27 +00:00
HydrusData.ShowText( 'Thumbnail {} wrong size ({}x{} instead of {}x{}), only scaling due to no local source.'.format( hash.hex(), current_width, current_height, expected_width, expected_height ) )
2019-03-27 22:01:02 +00:00
2019-03-20 21:22:10 +00:00
2019-03-27 22:01:02 +00:00
hydrus_bitmap = ClientRendering.GenerateHydrusBitmapFromNumPyImage( numpy_image )
2019-03-20 21:22:10 +00:00
return hydrus_bitmap
2023-02-15 21:26:44 +00:00
def _HandleThumbnailException( self, hash, e, summary ):
2019-03-20 21:22:10 +00:00
if self._thumbnail_error_occurred:
HydrusData.Print( summary )
else:
self._thumbnail_error_occurred = True
message = 'A thumbnail error has occurred. The problem thumbnail will appear with the default \'hydrus\' symbol. You may need to take hard drive recovery actions, and if the error is not obviously fixable, you can contact hydrus dev for additional help. Specific information for this first error follows. Subsequent thumbnail errors in this session will be silently printed to the log.'
message += os.linesep * 2
message += str( e )
message += os.linesep * 2
message += summary
2023-02-15 21:26:44 +00:00
job_key = ClientThreading.JobKey()
job_key.SetStatusText( message )
job_key.SetFiles( { hash }, 'broken thumbnail' )
HG.client_controller.pub( 'message', job_key )
2019-03-20 21:22:10 +00:00
2019-04-03 22:45:57 +00:00
def _InitialiseMagicMimeScores( self ):
# let's render our thumbs in order of ease of regeneration, so we rush what we can to screen as fast as possible and leave big vids until the end
for mime in HC.ALLOWED_MIMES:
self._magic_mime_thumbnail_ease_score_lookup[ mime ] = 5
# default filetype thumbs are easiest
self._magic_mime_thumbnail_ease_score_lookup[ None ] = 0
self._magic_mime_thumbnail_ease_score_lookup[ HC.APPLICATION_UNKNOWN ] = 0
for mime in HC.APPLICATIONS:
self._magic_mime_thumbnail_ease_score_lookup[ mime ] = 0
for mime in HC.AUDIO:
self._magic_mime_thumbnail_ease_score_lookup[ mime ] = 0
# images a little trickier
for mime in HC.IMAGES:
self._magic_mime_thumbnail_ease_score_lookup[ mime ] = 1
2023-08-16 20:46:51 +00:00
for mime in HC.ANIMATIONS:
self._magic_mime_thumbnail_ease_score_lookup[ mime ] = 2
# could get more specific here because some applications will probably be even worse than videos
for mime in HC.APPLICATIONS_WITH_THUMBNAILS:
2019-04-03 22:45:57 +00:00
self._magic_mime_thumbnail_ease_score_lookup[ mime ] = 3
2023-08-16 20:46:51 +00:00
# ffmpeg hellzone
for mime in HC.VIDEO:
2020-01-22 21:04:43 +00:00
self._magic_mime_thumbnail_ease_score_lookup[ mime ] = 3
2019-04-03 22:45:57 +00:00
2019-04-10 22:50:53 +00:00
def _RecalcQueues( self ):
2019-03-20 21:22:10 +00:00
# here we sort by the hash since this is both breddy random and more likely to access faster on a well defragged hard drive!
2019-04-10 22:50:53 +00:00
# and now with the magic mime order
2019-03-20 21:22:10 +00:00
2019-04-10 22:50:53 +00:00
def sort_waterfall( item ):
2015-08-05 18:42:35 +00:00
2019-03-20 21:22:10 +00:00
( page_key, media ) = item
2015-08-05 18:42:35 +00:00
2019-04-03 22:45:57 +00:00
display_media = media.GetDisplayMedia()
2020-01-16 02:08:23 +00:00
if display_media is None:
magic_score = self._magic_mime_thumbnail_ease_score_lookup[ None ]
hash = ''
else:
magic_score = self._magic_mime_thumbnail_ease_score_lookup[ display_media.GetMime() ]
hash = display_media.GetHash()
2019-04-03 22:45:57 +00:00
return ( magic_score, hash )
2015-08-05 18:42:35 +00:00
2019-04-10 22:50:53 +00:00
self._waterfall_queue = list( self._waterfall_queue_quick )
2019-03-20 21:22:10 +00:00
2019-04-03 22:45:57 +00:00
# we pop off the end, so reverse
2019-04-10 22:50:53 +00:00
self._waterfall_queue.sort( key = sort_waterfall, reverse = True )
2019-11-28 01:11:46 +00:00
if len( self._waterfall_queue ) == 0:
self._waterfall_queue_empty_event.set()
else:
self._waterfall_queue_empty_event.clear()
2019-04-10 22:50:53 +00:00
def sort_regen( item ):
2019-04-24 22:18:50 +00:00
media_result = item
hash = media_result.GetHash()
mime = media_result.GetMime()
2019-04-10 22:50:53 +00:00
magic_score = self._magic_mime_thumbnail_ease_score_lookup[ mime ]
return ( magic_score, hash )
self._delayed_regeneration_queue = list( self._delayed_regeneration_queue_quick )
# we pop off the end, so reverse
self._delayed_regeneration_queue.sort( key = sort_regen, reverse = True )
2019-03-20 21:22:10 +00:00
2015-08-05 18:42:35 +00:00
2022-01-19 21:28:59 +00:00
def _ShouldBeAbleToProvideThumb( self, media ):
locations_manager = media.GetLocationsManager()
2023-09-27 21:12:55 +00:00
we_have_file = locations_manager.IsLocal()
we_should_have_thumb = not locations_manager.GetCurrent().isdisjoint( HG.client_controller.services_manager.GetServiceKeys( ( HC.FILE_REPOSITORY, ) ) )
we_have_blurhash = media.GetFileInfoManager().blurhash is not None
return we_have_file or we_should_have_thumb or we_have_blurhash
2022-01-19 21:28:59 +00:00
2020-04-01 21:51:42 +00:00
def CancelWaterfall( self, page_key: bytes, medias: list ):
2015-08-05 18:42:35 +00:00
2019-03-20 21:22:10 +00:00
with self._lock:
2017-04-05 21:16:40 +00:00
2019-03-20 21:22:10 +00:00
self._waterfall_queue_quick.difference_update( ( ( page_key, media ) for media in medias ) )
2017-04-05 21:16:40 +00:00
2020-04-01 21:51:42 +00:00
cancelled_display_medias = { media.GetDisplayMedia() for media in medias }
cancelled_display_medias.discard( None )
cancelled_media_results = { media.GetMediaResult() for media in cancelled_display_medias }
2019-05-29 21:34:43 +00:00
outstanding_delayed_hashes = { media_result.GetHash() for media_result in cancelled_media_results if media_result in self._delayed_regeneration_queue_quick }
if len( outstanding_delayed_hashes ) > 0:
self._controller.files_maintenance_manager.ScheduleJob( outstanding_delayed_hashes, ClientFiles.REGENERATE_FILE_DATA_JOB_FORCE_THUMBNAIL )
self._delayed_regeneration_queue_quick.difference_update( cancelled_media_results )
2019-04-10 22:50:53 +00:00
self._RecalcQueues()
2019-03-20 21:22:10 +00:00
def Clear( self ):
2017-04-05 21:16:40 +00:00
2015-08-05 18:42:35 +00:00
with self._lock:
2019-03-20 21:22:10 +00:00
self._data_cache.Clear()
2016-09-14 18:03:59 +00:00
2019-03-20 21:22:10 +00:00
self._special_thumbs = {}
2015-08-05 18:42:35 +00:00
names = [ 'hydrus', 'pdf', 'psd', 'clip', 'sai', 'krita', 'xcf', 'svg', 'audio', 'video', 'zip', 'epub', 'djvu' ]
2019-03-20 21:22:10 +00:00
2019-04-24 22:18:50 +00:00
bounding_dimensions = self._controller.options[ 'thumbnail_dimensions' ]
2022-02-02 22:14:01 +00:00
thumbnail_scale_type = self._controller.new_options.GetInteger( 'thumbnail_scale_type' )
2022-12-21 22:00:27 +00:00
thumbnail_dpr_percent = HG.client_controller.new_options.GetInteger( 'thumbnail_dpr_percent' )
2019-03-20 21:22:10 +00:00
2022-03-23 20:57:10 +00:00
# it would be ideal to replace this with mimes_to_default_thumbnail_paths at a convenient point
2019-04-24 22:18:50 +00:00
for name in names:
2015-08-05 18:42:35 +00:00
2022-03-23 20:57:10 +00:00
path = os.path.join( HC.STATIC_DIR, '{}.png'.format( name ) )
2019-04-10 22:50:53 +00:00
2019-05-08 21:06:42 +00:00
numpy_image = ClientImageHandling.GenerateNumPyImage( path, HC.IMAGE_PNG )
numpy_image_resolution = HydrusImageHandling.GetResolutionNumPy( numpy_image )
target_resolution = HydrusImageHandling.GetThumbnailResolution( numpy_image_resolution, bounding_dimensions, thumbnail_scale_type, thumbnail_dpr_percent )
2019-04-24 22:18:50 +00:00
2019-05-08 21:06:42 +00:00
numpy_image = HydrusImageHandling.ResizeNumPyImage( numpy_image, target_resolution )
2015-08-05 18:42:35 +00:00
2019-04-24 22:18:50 +00:00
hydrus_bitmap = ClientRendering.GenerateHydrusBitmapFromNumPyImage( numpy_image )
2019-03-20 21:22:10 +00:00
2019-04-24 22:18:50 +00:00
self._special_thumbs[ name ] = hydrus_bitmap
2015-08-05 18:42:35 +00:00
2019-10-02 23:38:59 +00:00
self._controller.pub( 'notify_complete_thumbnail_reset' )
2019-04-10 22:50:53 +00:00
self._waterfall_queue_quick = set()
self._delayed_regeneration_queue_quick = set()
self._RecalcQueues()
2015-08-05 18:42:35 +00:00
2019-03-20 21:22:10 +00:00
def ClearThumbnails( self, hashes ):
2017-04-05 21:16:40 +00:00
2015-11-11 21:20:41 +00:00
with self._lock:
2019-03-20 21:22:10 +00:00
for hash in hashes:
2015-11-11 21:20:41 +00:00
2019-03-20 21:22:10 +00:00
self._data_cache.DeleteData( hash )
2015-11-11 21:20:41 +00:00
2019-11-28 01:11:46 +00:00
def WaitUntilFree( self ):
2017-04-05 21:16:40 +00:00
2019-11-28 01:11:46 +00:00
while True:
2022-01-19 21:28:59 +00:00
if HG.started_shutdown:
2019-11-28 01:11:46 +00:00
raise HydrusExceptions.ShutdownException( 'Application shutting down!' )
queue_is_empty = self._waterfall_queue_empty_event.wait( 1 )
2016-09-14 18:03:59 +00:00
2019-11-28 01:11:46 +00:00
if queue_is_empty:
return
2016-09-14 18:03:59 +00:00
2019-03-20 21:22:10 +00:00
def GetThumbnail( self, media ):
2015-08-05 18:42:35 +00:00
2020-04-01 21:51:42 +00:00
display_media = media.GetDisplayMedia()
if display_media is None:
2015-11-11 21:20:41 +00:00
2019-03-20 21:22:10 +00:00
# sometimes media can get switched around during a collect event, and if this happens during waterfall, we have a problem here
# just return for now, we'll see how it goes
2017-04-05 21:16:40 +00:00
2019-03-20 21:22:10 +00:00
return self._special_thumbs[ 'hydrus' ]
2017-04-05 21:16:40 +00:00
2022-01-19 21:28:59 +00:00
can_provide = self._ShouldBeAbleToProvideThumb( display_media )
2019-03-20 21:22:10 +00:00
2022-01-19 21:28:59 +00:00
if can_provide:
2016-09-14 18:03:59 +00:00
2019-03-20 21:22:10 +00:00
mime = display_media.GetMime()
2015-08-05 18:42:35 +00:00
2019-03-20 21:22:10 +00:00
if mime in HC.MIMES_WITH_THUMBNAILS:
2015-08-05 18:42:35 +00:00
2019-03-20 21:22:10 +00:00
hash = display_media.GetHash()
result = self._data_cache.GetIfHasData( hash )
if result is None:
2016-09-14 18:03:59 +00:00
2019-03-20 21:22:10 +00:00
try:
2019-03-27 22:01:02 +00:00
hydrus_bitmap = self._GetThumbnailHydrusBitmap( display_media )
2019-03-20 21:22:10 +00:00
except:
hydrus_bitmap = self._special_thumbs[ 'hydrus' ]
self._data_cache.AddData( hash, hydrus_bitmap )
else:
hydrus_bitmap = result
2016-09-14 18:03:59 +00:00
2015-08-05 18:42:35 +00:00
2019-03-20 21:22:10 +00:00
return hydrus_bitmap
2015-08-05 18:42:35 +00:00
2019-03-20 21:22:10 +00:00
elif mime in HC.AUDIO: return self._special_thumbs[ 'audio' ]
elif mime in HC.VIDEO: return self._special_thumbs[ 'video' ]
elif mime == HC.APPLICATION_PDF: return self._special_thumbs[ 'pdf' ]
elif mime == HC.APPLICATION_EPUB: return self._special_thumbs[ 'epub' ]
elif mime == HC.APPLICATION_DJVU: return self._special_thumbs[ 'djvu' ]
2019-03-20 21:22:10 +00:00
elif mime == HC.APPLICATION_PSD: return self._special_thumbs[ 'psd' ]
2023-06-29 17:30:37 +00:00
elif mime == HC.APPLICATION_SAI2: return self._special_thumbs[ 'sai' ]
2023-07-06 06:29:13 +00:00
elif mime == HC.APPLICATION_KRITA: return self._special_thumbs[ 'krita' ]
2023-07-09 12:27:47 +00:00
elif mime == HC.APPLICATION_XCF: return self._special_thumbs[ 'xcf' ]
2023-07-03 17:13:35 +00:00
elif mime == HC.IMAGE_SVG: return self._special_thumbs[ 'svg' ]
2019-03-20 21:22:10 +00:00
elif mime in HC.ARCHIVES: return self._special_thumbs[ 'zip' ]
else: return self._special_thumbs[ 'hydrus' ]
2015-08-05 18:42:35 +00:00
2019-03-20 21:22:10 +00:00
else:
return self._special_thumbs[ 'hydrus' ]
2015-08-05 18:42:35 +00:00
2015-10-07 21:56:22 +00:00
2019-03-20 21:22:10 +00:00
def HasThumbnailCached( self, media ):
2018-03-28 21:55:58 +00:00
2019-03-20 21:22:10 +00:00
display_media = media.GetDisplayMedia()
2020-04-01 21:51:42 +00:00
if display_media is None:
return True
2019-03-20 21:22:10 +00:00
mime = display_media.GetMime()
if mime in HC.MIMES_WITH_THUMBNAILS:
2018-03-28 21:55:58 +00:00
2022-01-19 21:28:59 +00:00
if self._ShouldBeAbleToProvideThumb( display_media ):
hash = display_media.GetHash()
return self._data_cache.HasData( hash )
else:
# yes because we provide the hydrus icon instantly
return True
2018-03-28 21:55:58 +00:00
2019-03-20 21:22:10 +00:00
else:
2018-03-28 21:55:58 +00:00
2019-03-20 21:22:10 +00:00
return True
2018-03-28 21:55:58 +00:00
2021-06-09 20:28:09 +00:00
def NotifyNewOptions( self ):
2022-04-20 20:18:56 +00:00
cache_size = self._controller.new_options.GetInteger( 'thumbnail_cache_size' )
2021-06-09 20:28:09 +00:00
cache_timeout = self._controller.new_options.GetInteger( 'thumbnail_cache_timeout' )
self._data_cache.SetCacheSizeAndTimeout( cache_size, cache_timeout )
2023-10-04 20:51:17 +00:00
allow_blurhash_fallback = self._controller.new_options.GetBoolean( 'allow_blurhash_fallback' )
if allow_blurhash_fallback != self._allow_blurhash_fallback:
self._allow_blurhash_fallback = allow_blurhash_fallback
self.Clear()
2021-06-09 20:28:09 +00:00
2019-03-20 21:22:10 +00:00
def Waterfall( self, page_key, medias ):
2018-03-28 21:55:58 +00:00
2019-03-20 21:22:10 +00:00
with self._lock:
2018-03-28 21:55:58 +00:00
2019-03-20 21:22:10 +00:00
self._waterfall_queue_quick.update( ( ( page_key, media ) for media in medias ) )
2018-03-28 21:55:58 +00:00
2019-04-10 22:50:53 +00:00
self._RecalcQueues()
2018-03-28 21:55:58 +00:00
2019-03-20 21:22:10 +00:00
self._waterfall_event.set()
2019-12-05 05:29:32 +00:00
def MainLoop( self ):
2019-03-20 21:22:10 +00:00
while not HydrusThreading.IsThreadShuttingDown():
2018-03-28 21:55:58 +00:00
2019-04-10 22:50:53 +00:00
time.sleep( 0.00001 )
2019-03-20 21:22:10 +00:00
with self._lock:
2018-03-28 21:55:58 +00:00
2019-04-10 22:50:53 +00:00
do_wait = len( self._waterfall_queue ) == 0 and len( self._delayed_regeneration_queue ) == 0
2018-03-28 21:55:58 +00:00
2019-03-20 21:22:10 +00:00
if do_wait:
2018-03-28 21:55:58 +00:00
2019-03-20 21:22:10 +00:00
self._waterfall_event.wait( 1 )
2018-03-28 21:55:58 +00:00
2019-03-20 21:22:10 +00:00
self._waterfall_event.clear()
2018-03-28 21:55:58 +00:00
2023-04-19 20:38:13 +00:00
start_time = HydrusTime.GetNowPrecise()
2019-03-20 21:22:10 +00:00
stop_time = start_time + 0.005 # a bit of a typical frame
2018-03-28 21:55:58 +00:00
2019-03-20 21:22:10 +00:00
page_keys_to_rendered_medias = collections.defaultdict( list )
2018-03-28 21:55:58 +00:00
2020-01-22 21:04:43 +00:00
num_done = 0
max_at_once = 16
2023-04-19 20:38:13 +00:00
while not HydrusTime.TimeHasPassedPrecise( stop_time ) and num_done <= max_at_once:
2018-11-07 23:09:40 +00:00
2019-03-20 21:22:10 +00:00
with self._lock:
2019-04-10 22:50:53 +00:00
if len( self._waterfall_queue ) == 0:
2019-03-20 21:22:10 +00:00
break
2019-04-10 22:50:53 +00:00
result = self._waterfall_queue.pop()
2019-03-20 21:22:10 +00:00
2019-11-28 01:11:46 +00:00
if len( self._waterfall_queue ) == 0:
self._waterfall_queue_empty_event.set()
2019-03-20 21:22:10 +00:00
self._waterfall_queue_quick.discard( result )
2018-11-07 23:09:40 +00:00
2019-03-20 21:22:10 +00:00
( page_key, media ) = result
2018-03-28 21:55:58 +00:00
2020-01-16 02:08:23 +00:00
if media.GetDisplayMedia() is not None:
self.GetThumbnail( media )
page_keys_to_rendered_medias[ page_key ].append( media )
2018-03-28 21:55:58 +00:00
2020-01-22 21:04:43 +00:00
num_done += 1
2019-03-20 21:22:10 +00:00
2019-04-10 22:50:53 +00:00
if len( page_keys_to_rendered_medias ) > 0:
2018-11-07 23:09:40 +00:00
2019-04-10 22:50:53 +00:00
for ( page_key, rendered_medias ) in page_keys_to_rendered_medias.items():
self._controller.pub( 'waterfall_thumbnails', page_key, rendered_medias )
time.sleep( 0.00001 )
2019-03-20 21:22:10 +00:00
2019-04-10 22:50:53 +00:00
# now we will do regen if appropriate
with self._lock:
# got more important work or no work to do
if len( self._waterfall_queue ) > 0 or len( self._delayed_regeneration_queue ) == 0 or HG.client_controller.CurrentlyPubSubbing():
continue
2019-04-24 22:18:50 +00:00
media_result = self._delayed_regeneration_queue.pop()
2019-04-10 22:50:53 +00:00
2019-04-24 22:18:50 +00:00
self._delayed_regeneration_queue_quick.discard( media_result )
2019-04-10 22:50:53 +00:00
if HG.file_report_mode:
2019-04-24 22:18:50 +00:00
hash = media_result.GetHash()
2019-04-10 22:50:53 +00:00
HydrusData.ShowText( 'Thumbnail {} now regenerating from source.'.format( hash.hex() ) )
try:
2019-05-22 22:35:06 +00:00
self._controller.files_maintenance_manager.RunJobImmediately( [ media_result ], ClientFiles.REGENERATE_FILE_DATA_JOB_FORCE_THUMBNAIL, pub_job_key = False )
2019-04-10 22:50:53 +00:00
except HydrusExceptions.FileMissingException:
pass
except Exception as e:
2019-04-24 22:18:50 +00:00
hash = media_result.GetHash()
2019-04-10 22:50:53 +00:00
summary = 'The thumbnail for file {} was incorrect, but a later attempt to regenerate it or load the new file back failed.'.format( hash.hex() )
2023-02-15 21:26:44 +00:00
self._HandleThumbnailException( hash, e, summary )
2019-04-10 22:50:53 +00:00
2018-03-28 21:55:58 +00:00