import collections import threading import typing from qtpy import QtGui as QG from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusData from hydrus.core import HydrusGlobals as HG from hydrus.core import HydrusTime from hydrus.client import ClientConstants as CC from hydrus.client import ClientGlobals as CG from hydrus.client import ClientData from hydrus.client.media import ClientMedia from hydrus.client.metadata import ClientContentUpdates # now let's fill out grandparents def BuildServiceKeysToChildrenToParents( service_keys_to_simple_children_to_parents ): # TODO: this is not used any more. was it all moved elsewhere? delete if so # important thing here, and reason why it is recursive, is because we want to preserve the parent-grandparent interleaving in list order def AddParentsAndGrandparents( simple_children_to_parents, this_childs_parents, parents ): for parent in parents: if parent not in this_childs_parents: this_childs_parents.append( parent ) # this parent has its own parents, so the child should get those as well if parent in simple_children_to_parents: grandparents = simple_children_to_parents[ parent ] AddParentsAndGrandparents( simple_children_to_parents, this_childs_parents, grandparents ) service_keys_to_children_to_parents = collections.defaultdict( HydrusData.default_dict_list ) for ( service_key, simple_children_to_parents ) in service_keys_to_simple_children_to_parents.items(): children_to_parents = service_keys_to_children_to_parents[ service_key ] for ( child, parents ) in list( simple_children_to_parents.items() ): this_childs_parents = children_to_parents[ child ] AddParentsAndGrandparents( simple_children_to_parents, this_childs_parents, parents ) return service_keys_to_children_to_parents def BuildServiceKeysToSimpleChildrenToParents( service_keys_to_pairs_flat ): # TODO: this is not used any more. was it all moved elsewhere? delete if so service_keys_to_simple_children_to_parents = collections.defaultdict( HydrusData.default_dict_set ) for ( service_key, pairs ) in service_keys_to_pairs_flat.items(): service_keys_to_simple_children_to_parents[ service_key ] = BuildSimpleChildrenToParents( pairs ) return service_keys_to_simple_children_to_parents # take pairs, make dict of child -> parents while excluding loops # no grandparents here def BuildSimpleChildrenToParents( pairs ): # TODO: move this and Loop guy somewhere better simple_children_to_parents = HydrusData.default_dict_set() for ( child, parent ) in pairs: if child == parent: continue if parent in simple_children_to_parents and LoopInSimpleChildrenToParents( simple_children_to_parents, child, parent ): continue simple_children_to_parents[ child ].add( parent ) return simple_children_to_parents def LoopInSimpleChildrenToParents( simple_children_to_parents, child, parent ): # TODO: move this somewhere better potential_loop_paths = { parent } while True: new_potential_loop_paths = set() for potential_loop_path in potential_loop_paths: if potential_loop_path in simple_children_to_parents: new_potential_loop_paths.update( simple_children_to_parents[ potential_loop_path ] ) potential_loop_paths = new_potential_loop_paths if child in potential_loop_paths: return True elif len( potential_loop_paths ) == 0: return False class BitmapManager( object ): MAX_MEMORY_ALLOWANCE = 512 * 1024 * 1024 def __init__( self, controller ): self._controller = controller self._media_background_pixmap_path = None self._media_background_pixmap = None def _GetQtImageFormat( self, depth ): if depth == 24: return QG.QImage.Format_RGB888 elif depth == 32: return QG.QImage.Format_RGBA8888 def GetQtImage( self, width, height, depth = 24 ): if width < 0: width = 20 if height < 0: height = 20 qt_image_format = self._GetQtImageFormat( depth ) return QG.QImage( width, height, qt_image_format ) def GetQtPixmap( self, width, height ): if width < 0: width = 20 if height < 0: height = 20 return QG.QPixmap( width, height ) def GetQtImageFromBuffer( self, width, height, depth, data ): if isinstance( data, memoryview ) and not data.c_contiguous: data = data.copy() qt_image_format = self._GetQtImageFormat( depth ) bytes_per_line = ( depth // 8 ) * width # no copy here qt_image = QG.QImage( data, width, height, bytes_per_line, qt_image_format ) # cheeky solution here # the QImage init does not take python ownership of the data, so if it gets garbage collected, we crash # so, add a beardy python ref to it, no problem :^) # other anwser here is to do a .copy, but this can be a _little_ expensive and eats memory qt_image.python_data_reference = data return qt_image def GetQtPixmapFromBuffer( self, width, height, depth, data ): if isinstance( data, memoryview ) and not data.c_contiguous: data = data.copy() qt_image_format = self._GetQtImageFormat( depth ) bytes_per_line = ( depth // 8 ) * width # no copy, no new data allocated qt_image = QG.QImage( data, width, height, bytes_per_line, qt_image_format ) # _should_ be a safe copy of the hot data pixmap = QG.QPixmap.fromImage( qt_image ) return pixmap def GetMediaBackgroundPixmap( self ): pixmap_path = self._controller.new_options.GetNoneableString( 'media_background_bmp_path' ) if pixmap_path != self._media_background_pixmap_path: self._media_background_pixmap_path = pixmap_path try: self._media_background_pixmap = QG.QPixmap( self._media_background_pixmap_path ) except Exception as e: self._media_background_pixmap = None HydrusData.ShowText( 'Loading a bmp caused an error!' ) HydrusData.ShowException( e ) return None return self._media_background_pixmap class FileViewingStatsManager( object ): def __init__( self, controller ): self._controller = controller self._lock = threading.Lock() self._pending_updates = {} self._last_update = HydrusTime.GetNow() self._my_flush_job = self._controller.CallRepeating( 5, 60, self.REPEATINGFlush ) def _GenerateViewsRow( self, media: ClientMedia.Media, canvas_type: int, view_timestamp_ms: int, viewtime_delta: int ): new_options = CG.client_controller.new_options viewtime_min = None viewtime_max = None result_views_delta = 0 result_viewtime_delta = 0 do_it = True if canvas_type == CC.CANVAS_PREVIEW: viewtime_min = new_options.GetNoneableInteger( 'file_viewing_statistics_preview_min_time' ) viewtime_max = new_options.GetNoneableInteger( 'file_viewing_statistics_preview_max_time' ) elif canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES: viewtime_min = new_options.GetNoneableInteger( 'file_viewing_statistics_media_min_time' ) viewtime_max = new_options.GetNoneableInteger( 'file_viewing_statistics_media_max_time' ) if canvas_type == CC.CANVAS_MEDIA_VIEWER_DUPLICATES and not new_options.GetBoolean( 'file_viewing_statistics_active_on_dupe_filter' ): do_it = False elif canvas_type == CC.CANVAS_MEDIA_VIEWER_ARCHIVE_DELETE and not new_options.GetBoolean( 'file_viewing_statistics_active_on_archive_delete_filter' ): do_it = False canvas_type = CC.CANVAS_MEDIA_VIEWER if media.HasDuration() and viewtime_max is not None: # if user is watching a long vid, save that whole time mate viewtime_max = max( viewtime_max, ( media.GetMediaResult().GetDuration() ) * 5 ) if do_it: # if a cap on max viewtime, cap it if viewtime_max is not None: viewtime_delta = min( viewtime_delta, viewtime_max ) # if a min on viewtime, then maybe don't do anything if viewtime_min is None or viewtime_delta >= viewtime_min: result_views_delta = 1 result_viewtime_delta = viewtime_delta return ( canvas_type, ( view_timestamp_ms, result_views_delta, result_viewtime_delta ) ) def _RowMakesChanges( self, row ): ( view_timestamp_ms, views_delta, viewtime_delta ) = row return views_delta != 0 or viewtime_delta != 0 def _PubSubRow( self, hash, canvas_type, row ): ( view_timestamp_ms, views_delta, viewtime_delta ) = row pubsub_row = ( hash, canvas_type, view_timestamp_ms, views_delta, viewtime_delta ) content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILE_VIEWING_STATS, HC.CONTENT_UPDATE_ADD, pubsub_row ) content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, content_update ) CG.client_controller.pub( 'content_updates_data', content_update_package ) CG.client_controller.pub( 'content_updates_gui', content_update_package ) def Flush( self ): with self._lock: if len( self._pending_updates ) > 0: content_updates = [] for ( ( hash, canvas_type ), ( view_timestamp_ms, views_delta, viewtime_delta ) ) in self._pending_updates.items(): row = ( hash, canvas_type, view_timestamp_ms, views_delta, viewtime_delta ) content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILE_VIEWING_STATS, HC.CONTENT_UPDATE_ADD, row ) content_updates.append( content_update ) content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdates( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, content_updates ) # non-synchronous, non-publishing self._controller.Write( 'content_updates', content_update_package, publish_content_updates = False ) self._pending_updates = {} def FinishViewing( self, media: ClientMedia.MediaSingleton, canvas_type, view_timestamp_ms, viewtime_delta ): if not CG.client_controller.new_options.GetBoolean( 'file_viewing_statistics_active' ): return hash = media.GetHash() with self._lock: ( canvas_type, row ) = self._GenerateViewsRow( media, canvas_type, view_timestamp_ms, viewtime_delta ) if not self._RowMakesChanges( row ): return key = ( hash, canvas_type ) if key not in self._pending_updates: self._pending_updates[ key ] = row else: ( view_timestamp_ms, views_delta, viewtime_delta ) = row ( existing_view_timestamp_ms, existing_views_delta, existing_viewtime_delta ) = self._pending_updates[ key ] self._pending_updates[ key ] = ( max( view_timestamp_ms, existing_view_timestamp_ms ), existing_views_delta + views_delta, existing_viewtime_delta + viewtime_delta ) self._PubSubRow( hash, canvas_type, row ) def REPEATINGFlush( self ): self.Flush() class UndoManager( object ): def __init__( self, controller ): self._controller = controller self._commands = [] self._inverted_commands = [] self._current_index = 0 self._lock = threading.Lock() self._controller.sub( self, 'Undo', 'undo' ) self._controller.sub( self, 'Redo', 'redo' ) def _FilterContentUpdatePackage( self, content_update_package: ClientContentUpdates.ContentUpdatePackage ): filtered_content_update_package = ClientContentUpdates.ContentUpdatePackage() for ( service_key, content_updates ) in content_update_package.IterateContentUpdates(): filtered_content_updates = [] for content_update in content_updates: ( data_type, action, row ) = content_update.ToTuple() if data_type == HC.CONTENT_TYPE_FILES: if action in ( HC.CONTENT_UPDATE_ADD, HC.CONTENT_UPDATE_DELETE, HC.CONTENT_UPDATE_UNDELETE, HC.CONTENT_UPDATE_RESCIND_PETITION, HC.CONTENT_UPDATE_CLEAR_DELETE_RECORD, HC.CONTENT_UPDATE_DELETE_FROM_SOURCE_AFTER_MIGRATE ): continue elif data_type == HC.CONTENT_TYPE_MAPPINGS: if action in ( HC.CONTENT_UPDATE_RESCIND_PETITION, HC.CONTENT_UPDATE_ADVANCED ): continue else: continue filtered_content_update = ClientContentUpdates.ContentUpdate( data_type, action, row ) filtered_content_updates.append( filtered_content_update ) if len( filtered_content_updates ) > 0: filtered_content_update_package.AddContentUpdates( service_key, filtered_content_updates ) return filtered_content_update_package def _InvertContentUpdatePackage( self, content_update_package: ClientContentUpdates.ContentUpdatePackage ): inverted_content_update_package = ClientContentUpdates.ContentUpdatePackage() for ( service_key, content_updates ) in content_update_package.IterateContentUpdates(): inverted_content_updates = [] for content_update in content_updates: ( data_type, action, row ) = content_update.ToTuple() inverted_row = row if data_type == HC.CONTENT_TYPE_FILES: if action == HC.CONTENT_UPDATE_ARCHIVE: inverted_action = HC.CONTENT_UPDATE_INBOX elif action == HC.CONTENT_UPDATE_INBOX: inverted_action = HC.CONTENT_UPDATE_ARCHIVE elif action == HC.CONTENT_UPDATE_PEND: inverted_action = HC.CONTENT_UPDATE_RESCIND_PEND elif action == HC.CONTENT_UPDATE_RESCIND_PEND: inverted_action = HC.CONTENT_UPDATE_PEND elif action == HC.CONTENT_UPDATE_PETITION: inverted_action = HC.CONTENT_UPDATE_RESCIND_PETITION else: continue elif data_type == HC.CONTENT_TYPE_MAPPINGS: if action == HC.CONTENT_UPDATE_ADD: inverted_action = HC.CONTENT_UPDATE_DELETE elif action == HC.CONTENT_UPDATE_DELETE: inverted_action = HC.CONTENT_UPDATE_ADD elif action == HC.CONTENT_UPDATE_PEND: inverted_action = HC.CONTENT_UPDATE_RESCIND_PEND elif action == HC.CONTENT_UPDATE_RESCIND_PEND: inverted_action = HC.CONTENT_UPDATE_PEND elif action == HC.CONTENT_UPDATE_PETITION: inverted_action = HC.CONTENT_UPDATE_RESCIND_PETITION else: continue else: continue inverted_content_update = ClientContentUpdates.ContentUpdate( data_type, inverted_action, inverted_row ) inverted_content_updates.append( inverted_content_update ) inverted_content_update_package.AddContentUpdates( service_key, inverted_content_updates ) return inverted_content_update_package def AddCommand( self, action, *args, **kwargs ): with self._lock: inverted_action = action inverted_args = args inverted_kwargs = kwargs if action == 'content_updates': ( content_update_package, ) = args content_update_package = self._FilterContentUpdatePackage( content_update_package ) if not content_update_package.HasContent(): return inverted_content_update_package = self._InvertContentUpdatePackage( content_update_package ) if not inverted_content_update_package.HasContent(): return inverted_args = ( inverted_content_update_package, ) else: return self._commands = self._commands[ : self._current_index ] self._inverted_commands = self._inverted_commands[ : self._current_index ] self._commands.append( ( action, args, kwargs ) ) self._inverted_commands.append( ( inverted_action, inverted_args, inverted_kwargs ) ) self._current_index += 1 self._controller.pub( 'notify_new_undo' ) def GetUndoRedoStrings( self ): with self._lock: ( undo_string, redo_string ) = ( None, None ) if self._current_index > 0: undo_index = self._current_index - 1 ( action, args, kwargs ) = self._commands[ undo_index ] if action == 'content_updates': ( content_update_package, ) = args undo_string = 'undo ' + content_update_package.ToString() if len( self._commands ) > 0 and self._current_index < len( self._commands ): redo_index = self._current_index ( action, args, kwargs ) = self._commands[ redo_index ] if action == 'content_updates': ( content_update_package, ) = args redo_string = 'redo ' + content_update_package.ToString() return ( undo_string, redo_string ) def Undo( self ): action = None with self._lock: if self._current_index > 0: self._current_index -= 1 ( action, args, kwargs ) = self._inverted_commands[ self._current_index ] if action is not None: self._controller.WriteSynchronous( action, *args, **kwargs ) self._controller.pub( 'notify_new_undo' ) def Redo( self ): action = None with self._lock: if len( self._commands ) > 0 and self._current_index < len( self._commands ): ( action, args, kwargs ) = self._commands[ self._current_index ] self._current_index += 1 if action is not None: self._controller.WriteSynchronous( action, *args, **kwargs ) self._controller.pub( 'notify_new_undo' )