diff --git a/hydrus/client/db/ClientDBFilesSearch.py b/hydrus/client/db/ClientDBFilesSearch.py index 60b132658..c26b6cbb1 100644 --- a/hydrus/client/db/ClientDBFilesSearch.py +++ b/hydrus/client/db/ClientDBFilesSearch.py @@ -2430,7 +2430,106 @@ def _DoNotePreds( self, system_predicates: ClientSearchFileSearchContext.FileSys notes_hash_ids = self.modules_notes_map.GetHashIdsFromNoteName( note_name, temp_table_name, job_status = job_status ) query_hash_ids.difference_update( notes_hash_ids ) + + + + if 'note_matches' in simple_preds: + + inclusive_contents = simple_preds[ 'note_matches' ] + + for content in inclusive_contents: + + with self._MakeTemporaryIntegerTable( query_hash_ids, 'hash_id' ) as temp_table_name: + + self._AnalyzeTempTable( temp_table_name ) + + notes_hash_ids = self.modules_notes_map.GetHashIdsFromNoteContent( content, temp_table_name, job_status = job_status ) + + query_hash_ids = intersection_update_qhi( query_hash_ids, notes_hash_ids ) + + + + if 'not_note_matches' in simple_preds: + + exclusive_contents = simple_preds[ 'not_note_matches' ] + + for content in exclusive_contents: + + with self._MakeTemporaryIntegerTable( query_hash_ids, 'hash_id' ) as temp_table_name: + self._AnalyzeTempTable( temp_table_name ) + + notes_hash_ids = self.modules_notes_map.GetHashIdsFromNoteContent( content, temp_table_name, job_status = job_status ) + + query_hash_ids.difference_update( notes_hash_ids ) + + + + if 'note_contains' in simple_preds: + + inclusive_contents = simple_preds[ 'note_contains' ] + + for content in inclusive_contents: + + content = f"%{content}%" + + with self._MakeTemporaryIntegerTable( query_hash_ids, 'hash_id' ) as temp_table_name: + + self._AnalyzeTempTable( temp_table_name ) + + notes_hash_ids = self.modules_notes_map.GetHashIdsFromNoteContent( content, temp_table_name, job_status = job_status ) + + query_hash_ids = intersection_update_qhi( query_hash_ids, notes_hash_ids ) + + + + if 'not_note_contains' in simple_preds: + + exclusive_contents = simple_preds[ 'not_note_contains' ] + + for content in exclusive_contents: + + content = f"%{content}%" + + with self._MakeTemporaryIntegerTable( query_hash_ids, 'hash_id' ) as temp_table_name: + + self._AnalyzeTempTable( temp_table_name ) + + notes_hash_ids = self.modules_notes_map.GetHashIdsFromNoteContent( content, temp_table_name, job_status = job_status ) + + query_hash_ids.difference_update( notes_hash_ids ) + + + + if 'note_contains_words' in simple_preds: + + inclusive_contents = simple_preds[ 'note_contains_words' ] + + for content in inclusive_contents: + + with self._MakeTemporaryIntegerTable( query_hash_ids, 'hash_id' ) as temp_table_name: + + self._AnalyzeTempTable( temp_table_name ) + + notes_hash_ids = self.modules_notes_map.GetHashIdsFromNoteWords( content, temp_table_name, job_status = job_status ) + + query_hash_ids = intersection_update_qhi( query_hash_ids, notes_hash_ids ) + + + + if 'not_note_contains_words' in simple_preds: + + exclusive_contents = simple_preds[ 'not_note_contains_words' ] + + for content in exclusive_contents: + + with self._MakeTemporaryIntegerTable( query_hash_ids, 'hash_id' ) as temp_table_name: + + self._AnalyzeTempTable( temp_table_name ) + + notes_hash_ids = self.modules_notes_map.GetHashIdsFromNoteWords( content, temp_table_name, job_status = job_status ) + + query_hash_ids.difference_update( notes_hash_ids ) diff --git a/hydrus/client/db/ClientDBNotesMap.py b/hydrus/client/db/ClientDBNotesMap.py index a9e027a68..8debc1073 100644 --- a/hydrus/client/db/ClientDBNotesMap.py +++ b/hydrus/client/db/ClientDBNotesMap.py @@ -59,7 +59,36 @@ def GetHashIdsFromNoteName( self, name: str, hash_ids_table_name: str, job_statu return self._STS( self._ExecuteCancellable( 'SELECT hash_id FROM file_notes CROSS JOIN {} USING ( hash_id ) WHERE name_id = ?;'.format( hash_ids_table_name ), ( label_id, ), cancelled_hook ) ) - def GetHashIdsFromNumNotes( self, number_tests: list[ ClientNumberTest.NumberTest ], hash_ids: collections.abc.Collection[ int ], hash_ids_table_name: str, job_status: ClientThreading.JobStatus | None = None ): + + def GetHashIdsFromNoteContent( self, content_pattern: str, hash_ids_table_name: str, job_status: typing.Optional[ ClientThreading.JobStatus ] = None ): + + cancelled_hook = None + + if job_status is not None: + + cancelled_hook = job_status.IsCancelled + + return self._STS( self._ExecuteCancellable( + fr"SELECT hash_id FROM {hash_ids_table_name} CROSS JOIN file_notes USING ( hash_id ) CROSS JOIN notes using ( note_id ) WHERE note LIKE ? ESCAPE '\';", ( content_pattern, ), cancelled_hook) ) + + + def GetHashIdsFromNoteWords( self, words: str, hash_ids_table_name: str, job_status: typing.Optional[ ClientThreading.JobStatus ] = None ): + + cancelled_hook = None + + if job_status is not None: + + cancelled_hook = job_status.IsCancelled + + words = [f"%{word}%" for word in words.split(" ")] + + query = " AND ".join([ fr"note LIKE ? ESCAPE '\'" for _ in words ]) + + return self._STS( self._ExecuteCancellable( + fr"SELECT hash_id FROM {hash_ids_table_name} CROSS JOIN file_notes USING ( hash_id ) CROSS JOIN notes using ( note_id ) WHERE {query};", words, cancelled_hook) ) + + + def GetHashIdsFromNumNotes( self, number_tests: list[ ClientNumberTest.NumberTest ], hash_ids: collections.abc.Collection[ int ], hash_ids_table_name: str, job_status: typing.Optional[ ClientThreading.JobStatus ] = None ): result_hash_ids = set( hash_ids ) diff --git a/hydrus/client/gui/search/ClientGUIPredicatesSingle.py b/hydrus/client/gui/search/ClientGUIPredicatesSingle.py index 6cd20bc73..eeae1aaad 100644 --- a/hydrus/client/gui/search/ClientGUIPredicatesSingle.py +++ b/hydrus/client/gui/search/ClientGUIPredicatesSingle.py @@ -1351,6 +1351,77 @@ def GetPredicates( self ): return predicates + +class PanelPredicateSystemNoteContent( PanelPredicateSystemSingle ): + + def __init__( self, parent, predicate ): + + super().__init__( parent ) + + self._operator = ClientGUICommon.BetterChoice( self ) + + self._operator.addItem( 'has note that contains', (0, True) ) + self._operator.addItem( 'does not have note that contains', (0, False) ) + self._operator.addItem( 'has note that contains the words', (1, True) ) + self._operator.addItem( 'does not have note that contains the words', (1, False) ) + self._operator.addItem( 'has note that matches', (2, True) ) + self._operator.addItem( 'does not have note that matches', (2, False) ) + + self._name = QW.QLineEdit( self ) + + # + + predicate = self._GetPredicateToInitialisePanelWith( predicate ) + + ( operator, name ) = predicate.GetValue() + + self._operator.SetValue( operator ) + self._name.setText( name ) + + # + + hbox = QP.HBoxLayout() + + QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:note content'), CC.FLAGS_CENTER_PERPENDICULAR ) + QP.AddToLayout( hbox, self._operator, CC.FLAGS_CENTER_PERPENDICULAR ) + QP.AddToLayout( hbox, self._name, CC.FLAGS_CENTER_PERPENDICULAR ) + + hbox.addStretch( 0 ) + + self.setLayout( hbox ) + + self.setFocusProxy( self._operator ) + + + def GetDefaultPredicate( self ): + + operator = True + name = '' + + return ClientSearchPredicate.Predicate( ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_CONTENT, ( operator, name ) ) + + + def GetPredicates( self ): + + name = self._name.text() + + if name == '': + + name = 'notes' + + index, operator = self._operator.GetValue() + + types = [ + ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_CONTENT, + ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_WORDS, + ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_MATCHES_CONTENT + ] + + predicates = ( ClientSearchPredicate.Predicate( types[index], ( operator, name ) ), ) + + return predicates + + class PanelPredicateSystemHeight( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): diff --git a/hydrus/client/gui/search/ClientGUISearch.py b/hydrus/client/gui/search/ClientGUISearch.py index cfd658e3a..941afc7b8 100644 --- a/hydrus/client/gui/search/ClientGUISearch.py +++ b/hydrus/client/gui/search/ClientGUISearch.py @@ -352,6 +352,14 @@ def __init__( self, parent, predicates: collections.abc.Collection[ ClientSearch self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemHasNoteName( self, predicate ) ) + elif predicate_type in ( + ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_MATCHES_CONTENT, + ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_CONTENT, + ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_WORDS, + ): + + self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemNoteContent( self, predicate ) ) + elif predicate_type == ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NUM_WORDS: self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemNumWords( self, predicate ) ) @@ -770,13 +778,20 @@ def __init__( self, parent, predicate: ClientSearchPredicate.Predicate ): elif predicate_type == ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTES: - recent_predicate_types = [ ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME ] + recent_predicate_types = [ + ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NUM_NOTES, + ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME, + ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_MATCHES_CONTENT, + ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_CONTENT, + ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_WORDS, + ] static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearchPredicate.Predicate( ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ClientNumberTest.NumberTest.STATICCreateFromCharacters( '>', 0 ) ), ), show_remove_button = False ) ) static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearchPredicate.Predicate( ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ClientNumberTest.NumberTest.STATICCreateFromCharacters( '=', 0 ) ), ), show_remove_button = False ) ) editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemNumNotes, predicate ) ) editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemHasNoteName, predicate ) ) + editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemNoteContent, predicate ) ) elif predicate_type == ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NUM_WORDS: diff --git a/hydrus/client/search/ClientSearchFileSearchContext.py b/hydrus/client/search/ClientSearchFileSearchContext.py index f8ae69c7b..1db7d1a56 100644 --- a/hydrus/client/search/ClientSearchFileSearchContext.py +++ b/hydrus/client/search/ClientSearchFileSearchContext.py @@ -378,6 +378,68 @@ def __init__( self, predicates: collections.abc.Collection[ ClientSearchPredicat self._common_info[ label ].add( name ) + if predicate_type == ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_MATCHES_CONTENT: + + ( operator, name ) = value + + if operator: + + label = 'note_matches' + + else: + + label = 'not_note_matches' + + + if label not in self._common_info: + + self._common_info[ label ] = set() + + + self._common_info[ label ].add( name ) + + + if predicate_type == ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_CONTENT: + + ( operator, name ) = value + + if operator: + + label = 'note_contains' + + else: + + label = 'not_note_contains' + + + if label not in self._common_info: + + self._common_info[ label ] = set() + + + self._common_info[ label ].add( name ) + + + if predicate_type == ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_WORDS: + + ( operator, name ) = value + + if operator: + + label = 'note_contains_words' + + else: + + label = 'not_note_contains_words' + + + if label not in self._common_info: + + self._common_info[ label ] = set() + + + self._common_info[ label ].add( name ) + if predicate_type == ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_LIMIT: limit = value diff --git a/hydrus/client/search/ClientSearchParseSystemPredicates.py b/hydrus/client/search/ClientSearchParseSystemPredicates.py index 9ec099055..ce2c4c5a6 100644 --- a/hydrus/client/search/ClientSearchParseSystemPredicates.py +++ b/hydrus/client/search/ClientSearchParseSystemPredicates.py @@ -331,6 +331,12 @@ def viewtime_pred_generator( o, v, u ): SystemPredicateParser.Predicate.TAG_ADVANCED_INCLUSIVE : lambda o, v, u: ClientSearchPredicate.Predicate( ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_TAG_ADVANCED, ( o[0], o[1], o[2], v ) ), SystemPredicateParser.Predicate.TAG_ADVANCED_EXCLUSIVE : lambda o, v, u: ClientSearchPredicate.Predicate( ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_TAG_ADVANCED, ( o[0], o[1], o[2], v ), inclusive = False ), SystemPredicateParser.Predicate.RATING_ADVANCED : lambda o, v, u: ClientSearchPredicate.Predicate( ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_RATING_ADVANCED, v ), + SystemPredicateParser.Predicate.MATCHES_NOTE_CONTENT : lambda o, v, u: ClientSearchPredicate.Predicate(ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_MATCHES_CONTENT, ( True, strip_quotes( v ) ) ), + SystemPredicateParser.Predicate.NO_MATCHES_NOTE_CONTENT : lambda o, v, u: ClientSearchPredicate.Predicate(ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_MATCHES_CONTENT, ( False, strip_quotes( v ) ) ), + SystemPredicateParser.Predicate.CONTAINS_NOTE_WORDS : lambda o, v, u: ClientSearchPredicate.Predicate(ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_WORDS, ( True, strip_quotes( v ) ) ), + SystemPredicateParser.Predicate.NO_CONTAINS_NOTE_WORDS : lambda o, v, u: ClientSearchPredicate.Predicate(ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_WORDS, ( False, strip_quotes( v ) ) ), + SystemPredicateParser.Predicate.CONTAINS_NOTE_CONTENT : lambda o, v, u: ClientSearchPredicate.Predicate(ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_CONTENT, ( True, strip_quotes( v ) ) ), + SystemPredicateParser.Predicate.NO_CONTAINS_NOTE_CONTENT : lambda o, v, u: ClientSearchPredicate.Predicate(ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_CONTENT, ( False, strip_quotes( v ) ) ), } def ParseSystemPredicateStringsToPredicates( system_predicate_strings: collections.abc.Collection[ str ], discard_failures = False ) -> list[ ClientSearchPredicate.Predicate ]: diff --git a/hydrus/client/search/ClientSearchPredicate.py b/hydrus/client/search/ClientSearchPredicate.py index ce43c2462..48504a843 100644 --- a/hydrus/client/search/ClientSearchPredicate.py +++ b/hydrus/client/search/ClientSearchPredicate.py @@ -76,6 +76,9 @@ PREDICATE_TYPE_SYSTEM_URLS = 53 PREDICATE_TYPE_SYSTEM_TAG_ADVANCED = 54 PREDICATE_TYPE_SYSTEM_RATING_ADVANCED_LEGACY = 55 # not using +PREDICATE_TYPE_SYSTEM_NOTE_MATCHES_CONTENT = 56 +PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_CONTENT = 57 +PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_WORDS = 58 PREDICATE_TYPE_SYSTEM_RATING_ADVANCED = 66 SYSTEM_PREDICATE_TYPES = { @@ -129,6 +132,9 @@ PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS, PREDICATE_TYPE_SYSTEM_TIME, PREDICATE_TYPE_SYSTEM_HAS_FORCED_FILETYPE, + PREDICATE_TYPE_SYSTEM_NOTE_MATCHES_CONTENT, + PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_CONTENT, + PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_WORDS } def ConvertSpecificFiletypesToSummary( specific_mimes: collections.abc.Collection[ int ], only_searchable = True ) -> collections.abc.Collection[ int ]: @@ -364,7 +370,10 @@ def STATICCreateStaticCount( current_count, pending_count ) -> "PredicateCount": PREDICATE_TYPE_OR_CONTAINER, PREDICATE_TYPE_NAMESPACE, PREDICATE_TYPE_WILDCARD, - PREDICATE_TYPE_TAG + PREDICATE_TYPE_TAG, + PREDICATE_TYPE_SYSTEM_NOTE_MATCHES_CONTENT, + PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_CONTENT, + PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_WORDS, } # this has useful order @@ -2076,6 +2085,59 @@ def _ToString( self, with_count: bool = True, render_for_user: bool = False, or_ else: base = 'does not have note with name "{}"'.format( name ) + + + elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NOTE_MATCHES_CONTENT: + + base = 'note matches' + + if self._value is not None: + + ( operator, name ) = self._value + + if operator: + + base = 'has note that matches "{}"'.format( name ) + + else: + + base = 'does not have note that matches "{}"'.format( name ) + + + + elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_CONTENT: + + base = 'note contains' + + if self._value is not None: + + ( operator, name ) = self._value + + if operator: + + base = 'has note that contains "{}"'.format( name ) + + else: + + base = 'does not have note that contains "{}"'.format( name ) + + + + elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NOTE_CONTAINS_WORDS: + + base = 'note contains words' + + if self._value is not None: + + ( operator, name ) = self._value + + if operator: + + base = 'has note that contains the words "{}"'.format( name ) + + else: + + base = 'does not have note that contains the words "{}"'.format( name ) diff --git a/hydrus/external/SystemPredicateParser.py b/hydrus/external/SystemPredicateParser.py index d278263da..2eecbb489 100644 --- a/hydrus/external/SystemPredicateParser.py +++ b/hydrus/external/SystemPredicateParser.py @@ -167,6 +167,12 @@ class Predicate( Enum ): NUM_NOTES = auto() HAS_NOTE_NAME = auto() NO_NOTE_NAME = auto() + MATCHES_NOTE_CONTENT = auto() + NO_MATCHES_NOTE_CONTENT = auto() + CONTAINS_NOTE_CONTENT = auto() + NO_CONTAINS_NOTE_CONTENT = auto() + CONTAINS_NOTE_WORDS = auto() + NO_CONTAINS_NOTE_WORDS = auto() RATING_SPECIFIC_NUMERICAL = auto() RATING_SPECIFIC_LIKE_DISLIKE = auto() RATING_SPECIFIC_INCDEC = auto() @@ -325,6 +331,12 @@ class Units( Enum ): r'has tag': (Predicate.TAG_ADVANCED_INCLUSIVE, Operators.TAG_ADVANCED_GUBBINS, Value.TAG_ADVANCED_TAG, None ), r'does not have tag': (Predicate.TAG_ADVANCED_EXCLUSIVE, Operators.TAG_ADVANCED_GUBBINS, Value.TAG_ADVANCED_TAG, None ), r'(all|any|only).+rated': (Predicate.RATING_ADVANCED, None, Value.RATING_ADVANCED, None ), + '(has (a )?)?note ((that )?matches)|((with )?content)': (Predicate.MATCHES_NOTE_CONTENT, None, Value.ANY_STRING, None), + '((has )?no|does not have( a)?|doesn\'t have( a)?) note ((that )?matches)|((with )?content)': (Predicate.NO_MATCHES_NOTE_CONTENT, None, Value.ANY_STRING, None), + '(has (a )?)?note (that )?contains (the )?words?': (Predicate.CONTAINS_NOTE_WORDS, None, Value.ANY_STRING, None), + '((has )?no|does not have( a)?|doesn\'t have( a)?) note (that )?contains (the )?words?': (Predicate.NO_CONTAINS_NOTE_WORDS, None, Value.ANY_STRING, None), + '(has (a )?)?note (that )?contains': (Predicate.CONTAINS_NOTE_CONTENT, None, Value.ANY_STRING, None), + '((has )?no|does not have( a)?|doesn\'t have( a)?) note (that )?contains': (Predicate.NO_CONTAINS_NOTE_CONTENT, None, Value.ANY_STRING, None), } def string_looks_like_date( string ):