import collections import datetime import re import threading import time import typing from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusData from hydrus.core import HydrusExceptions from hydrus.core import HydrusGlobals as HG from hydrus.core import HydrusSerialisable from hydrus.core import HydrusTags from hydrus.core import HydrusText from hydrus.client import ClientConstants as CC from hydrus.client import ClientData from hydrus.client import ClientLocation from hydrus.client import ClientTime from hydrus.client.metadata import ClientTags from hydrus.client.metadata import ClientTagsHandling PREDICATE_TYPE_TAG = 0 PREDICATE_TYPE_NAMESPACE = 1 PREDICATE_TYPE_PARENT = 2 PREDICATE_TYPE_WILDCARD = 3 PREDICATE_TYPE_SYSTEM_EVERYTHING = 4 PREDICATE_TYPE_SYSTEM_INBOX = 5 PREDICATE_TYPE_SYSTEM_ARCHIVE = 6 PREDICATE_TYPE_SYSTEM_UNTAGGED = 7 PREDICATE_TYPE_SYSTEM_NUM_TAGS = 8 PREDICATE_TYPE_SYSTEM_LIMIT = 9 PREDICATE_TYPE_SYSTEM_SIZE = 10 PREDICATE_TYPE_SYSTEM_AGE = 11 PREDICATE_TYPE_SYSTEM_HASH = 12 PREDICATE_TYPE_SYSTEM_WIDTH = 13 PREDICATE_TYPE_SYSTEM_HEIGHT = 14 PREDICATE_TYPE_SYSTEM_RATIO = 15 PREDICATE_TYPE_SYSTEM_DURATION = 16 PREDICATE_TYPE_SYSTEM_MIME = 17 PREDICATE_TYPE_SYSTEM_RATING = 18 PREDICATE_TYPE_SYSTEM_SIMILAR_TO = 19 PREDICATE_TYPE_SYSTEM_LOCAL = 20 PREDICATE_TYPE_SYSTEM_NOT_LOCAL = 21 PREDICATE_TYPE_SYSTEM_NUM_WORDS = 22 PREDICATE_TYPE_SYSTEM_FILE_SERVICE = 23 PREDICATE_TYPE_SYSTEM_NUM_PIXELS = 24 PREDICATE_TYPE_SYSTEM_DIMENSIONS = 25 PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_COUNT = 26 PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER = 27 PREDICATE_TYPE_SYSTEM_KNOWN_URLS = 28 PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS = 29 PREDICATE_TYPE_OR_CONTAINER = 30 PREDICATE_TYPE_LABEL = 31 PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_KING = 32 PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS = 33 PREDICATE_TYPE_SYSTEM_HAS_AUDIO = 34 PREDICATE_TYPE_SYSTEM_MODIFIED_TIME = 35 PREDICATE_TYPE_SYSTEM_FRAMERATE = 36 PREDICATE_TYPE_SYSTEM_NUM_FRAMES = 37 PREDICATE_TYPE_SYSTEM_NUM_NOTES = 38 PREDICATE_TYPE_SYSTEM_NOTES = 39 PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME = 40 PREDICATE_TYPE_SYSTEM_HAS_ICC_PROFILE = 41 PREDICATE_TYPE_SYSTEM_TIME = 42 PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME = 43 PREDICATE_TYPE_SYSTEM_HAS_HUMAN_READABLE_EMBEDDED_METADATA = 44 PREDICATE_TYPE_SYSTEM_EMBEDDED_METADATA = 45 PREDICATE_TYPE_SYSTEM_HAS_EXIF = 46 SYSTEM_PREDICATE_TYPES = { PREDICATE_TYPE_SYSTEM_EVERYTHING, PREDICATE_TYPE_SYSTEM_INBOX, PREDICATE_TYPE_SYSTEM_ARCHIVE, PREDICATE_TYPE_SYSTEM_UNTAGGED, PREDICATE_TYPE_SYSTEM_NUM_TAGS, PREDICATE_TYPE_SYSTEM_LIMIT, PREDICATE_TYPE_SYSTEM_SIZE, PREDICATE_TYPE_SYSTEM_AGE, PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, PREDICATE_TYPE_SYSTEM_MODIFIED_TIME, PREDICATE_TYPE_SYSTEM_HASH, PREDICATE_TYPE_SYSTEM_WIDTH, PREDICATE_TYPE_SYSTEM_HEIGHT, PREDICATE_TYPE_SYSTEM_RATIO, PREDICATE_TYPE_SYSTEM_DURATION, PREDICATE_TYPE_SYSTEM_FRAMERATE, PREDICATE_TYPE_SYSTEM_NUM_FRAMES, PREDICATE_TYPE_SYSTEM_HAS_AUDIO, PREDICATE_TYPE_SYSTEM_EMBEDDED_METADATA, PREDICATE_TYPE_SYSTEM_HAS_EXIF, PREDICATE_TYPE_SYSTEM_HAS_HUMAN_READABLE_EMBEDDED_METADATA, PREDICATE_TYPE_SYSTEM_HAS_ICC_PROFILE, PREDICATE_TYPE_SYSTEM_MIME, PREDICATE_TYPE_SYSTEM_RATING, PREDICATE_TYPE_SYSTEM_SIMILAR_TO, PREDICATE_TYPE_SYSTEM_LOCAL, PREDICATE_TYPE_SYSTEM_NOT_LOCAL, PREDICATE_TYPE_SYSTEM_NUM_WORDS, PREDICATE_TYPE_SYSTEM_NUM_NOTES, PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME, PREDICATE_TYPE_SYSTEM_FILE_SERVICE, PREDICATE_TYPE_SYSTEM_NUM_PIXELS, PREDICATE_TYPE_SYSTEM_DIMENSIONS, PREDICATE_TYPE_SYSTEM_NOTES, PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER, PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS, PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_COUNT, PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_KING, PREDICATE_TYPE_SYSTEM_KNOWN_URLS, PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS, PREDICATE_TYPE_SYSTEM_TIME } IGNORED_TAG_SEARCH_CHARACTERS = '[](){}/\\"\'-_' IGNORED_TAG_SEARCH_CHARACTERS_UNICODE_TRANSLATE = { ord( char ) : ' ' for char in IGNORED_TAG_SEARCH_CHARACTERS } def ConvertSpecificFiletypesToSummary( specific_mimes: typing.Collection[ int ], only_searchable = True ) -> typing.Collection[ int ]: specific_mimes_to_process = set( specific_mimes ) summary_mimes = set() for ( general_mime, mime_group ) in HC.general_mimetypes_to_mime_groups.items(): if only_searchable: mime_group = set( mime_group ) mime_group.intersection_update( HC.SEARCHABLE_MIMES ) if specific_mimes_to_process.issuperset( mime_group ): summary_mimes.add( general_mime ) specific_mimes_to_process.difference_update( mime_group ) summary_mimes.update( specific_mimes_to_process ) return summary_mimes def ConvertSubtagToSearchable( subtag ): if subtag == '': return '' subtag = CollapseWildcardCharacters( subtag ) subtag = subtag.translate( IGNORED_TAG_SEARCH_CHARACTERS_UNICODE_TRANSLATE ) subtag = HydrusText.re_one_or_more_whitespace.sub( ' ', subtag ) subtag = subtag.strip() return subtag def ConvertSummaryFiletypesToSpecific( summary_mimes: typing.Collection[ int ], only_searchable = True ) -> typing.Collection[ int ]: specific_mimes = set() for mime in summary_mimes: if mime in HC.GENERAL_FILETYPES: specific_mimes.update( HC.general_mimetypes_to_mime_groups[ mime ] ) else: specific_mimes.add( mime ) if only_searchable: specific_mimes.intersection_update( HC.SEARCHABLE_MIMES ) return specific_mimes def ConvertSummaryFiletypesToString( summary_mimes: typing.Collection[ int ] ) -> str: if set( summary_mimes ) == HC.GENERAL_FILETYPES: mime_text = 'anything' else: summary_mimes = sorted( summary_mimes, key = lambda m: HC.mime_mimetype_string_lookup[ m ] ) mime_text = ', '.join( [ HC.mime_string_lookup[ mime ] for mime in summary_mimes ] ) return mime_text def ConvertTagToSearchable( tag ): ( namespace, subtag ) = HydrusTags.SplitTag( tag ) searchable_subtag = ConvertSubtagToSearchable( subtag ) return HydrusTags.CombineTag( namespace, searchable_subtag ) def CollapseWildcardCharacters( text ): while '**' in text: text = text.replace( '**', '*' ) return text def IsComplexWildcard( search_text ): num_stars = search_text.count( '*' ) if num_stars > 1: return True if num_stars == 1 and not search_text.endswith( '*' ): return True return False def SortPredicates( predicates ): key = lambda p: p.GetCount().GetMinCount() predicates.sort( key = key, reverse = True ) return predicates NUMBER_TEST_OPERATOR_LESS_THAN = 0 NUMBER_TEST_OPERATOR_GREATER_THAN = 1 NUMBER_TEST_OPERATOR_EQUAL = 2 NUMBER_TEST_OPERATOR_APPROXIMATE = 3 NUMBER_TEST_OPERATOR_NOT_EQUAL = 4 number_test_operator_to_str_lookup = { NUMBER_TEST_OPERATOR_LESS_THAN : '<', NUMBER_TEST_OPERATOR_GREATER_THAN : '>', NUMBER_TEST_OPERATOR_EQUAL : '=', NUMBER_TEST_OPERATOR_APPROXIMATE : CC.UNICODE_ALMOST_EQUAL_TO, NUMBER_TEST_OPERATOR_NOT_EQUAL : CC.UNICODE_NOT_EQUAL_TO } number_test_str_to_operator_lookup = { value : key for ( key, value ) in number_test_operator_to_str_lookup.items() } class NumberTest( HydrusSerialisable.SerialisableBase ): SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_NUMBER_TEST SERIALISABLE_NAME = 'Number Test' SERIALISABLE_VERSION = 1 def __init__( self, operator = NUMBER_TEST_OPERATOR_EQUAL, value = 1 ): HydrusSerialisable.SerialisableBase.__init__( self ) self.operator = operator self.value = value def __eq__( self, other ): if isinstance( other, NumberTest ): return self.__hash__() == other.__hash__() return NotImplemented def __hash__( self ): return ( self.operator, self.value ).__hash__() def __repr__( self ): return '{} {}'.format( number_test_operator_to_str_lookup[ self.operator ], self.value ) def _GetSerialisableInfo( self ): return ( self.operator, self.value ) def _InitialiseFromSerialisableInfo( self, serialisable_info ): ( self.operator, self.value ) = serialisable_info def GetLambda( self ): if self.operator == NUMBER_TEST_OPERATOR_LESS_THAN: return lambda x: x < self.value elif self.operator == NUMBER_TEST_OPERATOR_GREATER_THAN: return lambda x: x > self.value elif self.operator == NUMBER_TEST_OPERATOR_EQUAL: return lambda x: x == self.value elif self.operator == NUMBER_TEST_OPERATOR_APPROXIMATE: lower = self.value * 0.85 upper = self.value * 1.15 return lambda x: lower < x < upper def IsAnythingButZero( self ): return self.operator == NUMBER_TEST_OPERATOR_GREATER_THAN and self.value == 0 def IsZero( self ): actually_zero = self.operator == NUMBER_TEST_OPERATOR_EQUAL and self.value == 0 less_than_one = self.operator == NUMBER_TEST_OPERATOR_LESS_THAN and self.value == 1 return actually_zero or less_than_one def WantsZero( self ): return self.GetLambda()( 0 ) @staticmethod def STATICCreateFromCharacters( operator_str: str, value: int ) -> "NumberTest": operator = number_test_str_to_operator_lookup[ operator_str ] return NumberTest( operator, value ) HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_NUMBER_TEST ] = NumberTest class FileSystemPredicates( object ): def __init__( self, system_predicates: typing.Collection[ "Predicate" ], apply_implicit_limit = True ): self._has_system_everything = False self._inbox = False self._archive = False self._local = False self._not_local = False self._common_info = {} self._timestamp_ranges = collections.defaultdict( dict ) self._limit = None self._similar_to = None self._required_file_service_statuses = collections.defaultdict( set ) self._excluded_file_service_statuses = collections.defaultdict( set ) self._ratings_predicates = [] self._num_tags_predicates = [] self._duplicate_count_predicates = [] self._king_filter = None self._file_viewing_stats_predicates = [] new_options = HG.client_controller.new_options for predicate in system_predicates: predicate_type = predicate.GetType() value = predicate.GetValue() if predicate_type == PREDICATE_TYPE_SYSTEM_EVERYTHING: self._has_system_everything = True if predicate_type == PREDICATE_TYPE_SYSTEM_INBOX: self._inbox = True if predicate_type == PREDICATE_TYPE_SYSTEM_ARCHIVE: self._archive = True if predicate_type == PREDICATE_TYPE_SYSTEM_LOCAL: self._local = True if predicate_type == PREDICATE_TYPE_SYSTEM_NOT_LOCAL: self._not_local = True if predicate_type == PREDICATE_TYPE_SYSTEM_KNOWN_URLS: ( operator, rule_type, rule, description ) = value if 'known_url_rules' not in self._common_info: self._common_info[ 'known_url_rules' ] = [] self._common_info[ 'known_url_rules' ].append( ( operator, rule_type, rule ) ) if predicate_type == PREDICATE_TYPE_SYSTEM_HAS_AUDIO: has_audio = value self._common_info[ 'has_audio' ] = has_audio if predicate_type == PREDICATE_TYPE_SYSTEM_HAS_EXIF: has_exif = value self._common_info[ 'has_exif' ] = has_exif if predicate_type == PREDICATE_TYPE_SYSTEM_HAS_HUMAN_READABLE_EMBEDDED_METADATA: has_human_readable_embedded_metadata = value self._common_info[ 'has_human_readable_embedded_metadata' ] = has_human_readable_embedded_metadata if predicate_type == PREDICATE_TYPE_SYSTEM_HAS_ICC_PROFILE: has_icc_profile = value self._common_info[ 'has_icc_profile' ] = has_icc_profile if predicate_type == PREDICATE_TYPE_SYSTEM_HASH: ( hashes, hash_type ) = value self._common_info[ 'hash' ] = ( hashes, hash_type, predicate.IsInclusive() ) if predicate_type in ( PREDICATE_TYPE_SYSTEM_AGE, PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, PREDICATE_TYPE_SYSTEM_MODIFIED_TIME ): ( operator, age_type, age_value ) = value if age_type == 'delta': ( years, months, days, hours ) = age_value age = ( years * 365 * 86400 ) + ( ( ( ( ( months * 30 ) + days ) * 24 ) + hours ) * 3600 ) now = HydrusData.GetNow() # this is backwards (less than means min timestamp) because we are talking about age, not timestamp # the before/since semantic logic is: # '<' 7 days age means 'since that date' # '>' 7 days ago means 'before that date' if operator == '<': time_pivot = now - age self._timestamp_ranges[ predicate_type ][ '>' ] = time_pivot elif operator == '>': time_pivot = now - age self._timestamp_ranges[ predicate_type ][ '<' ] = time_pivot elif operator == CC.UNICODE_ALMOST_EQUAL_TO: earliest = now - int( age * 1.15 ) latest = now - int( age * 0.85 ) self._timestamp_ranges[ predicate_type ][ '>' ] = earliest self._timestamp_ranges[ predicate_type ][ '<' ] = latest elif age_type == 'date': ( year, month, day ) = age_value dt = ClientTime.GetDateTime( year, month, day ) time_pivot = ClientTime.CalendarToTimestamp( dt ) next_day_timestamp = ClientTime.CalendarToTimestamp( ClientTime.CalendarDelta( dt, day_delta = 1 ) ) # the before/since semantic logic is: # '<' 2022-05-05 means 'before that date' # '>' 2022-05-05 means 'since that date' if operator == '<': self._timestamp_ranges[ predicate_type ][ '<' ] = time_pivot elif operator == '>': self._timestamp_ranges[ predicate_type ][ '>' ] = next_day_timestamp elif operator == '=': self._timestamp_ranges[ predicate_type ][ '>' ] = time_pivot self._timestamp_ranges[ predicate_type ][ '<' ] = next_day_timestamp elif operator == CC.UNICODE_ALMOST_EQUAL_TO: previous_month_timestamp = ClientTime.CalendarToTimestamp( ClientTime.CalendarDelta( dt, month_delta = -1 ) ) next_month_timestamp = ClientTime.CalendarToTimestamp( ClientTime.CalendarDelta( dt, month_delta = 1 ) ) self._timestamp_ranges[ predicate_type ][ '>' ] = previous_month_timestamp self._timestamp_ranges[ predicate_type ][ '<' ] = next_month_timestamp if predicate_type == PREDICATE_TYPE_SYSTEM_MIME: summary_mimes = value if isinstance( summary_mimes, int ): summary_mimes = ( summary_mimes, ) self._common_info[ 'mimes' ] = ConvertSummaryFiletypesToSpecific( summary_mimes ) if predicate_type == PREDICATE_TYPE_SYSTEM_DURATION: ( operator, duration ) = value if operator == '<': self._common_info[ 'max_duration' ] = duration elif operator == '>': self._common_info[ 'min_duration' ] = duration elif operator == '=': self._common_info[ 'duration' ] = duration elif operator == CC.UNICODE_NOT_EQUAL_TO: self._common_info[ 'not_duration' ] = duration elif operator == CC.UNICODE_ALMOST_EQUAL_TO: if duration == 0: self._common_info[ 'duration' ] = 0 else: self._common_info[ 'min_duration' ] = int( duration * 0.85 ) self._common_info[ 'max_duration' ] = int( duration * 1.15 ) if predicate_type == PREDICATE_TYPE_SYSTEM_FRAMERATE: ( operator, framerate ) = value if operator == '<': self._common_info[ 'max_framerate' ] = framerate elif operator == '>': self._common_info[ 'min_framerate' ] = framerate elif operator == '=': self._common_info[ 'framerate' ] = framerate elif operator == CC.UNICODE_NOT_EQUAL_TO: self._common_info[ 'not_framerate' ] = framerate if predicate_type == PREDICATE_TYPE_SYSTEM_NUM_FRAMES: ( operator, num_frames ) = value if operator == '<': self._common_info[ 'max_num_frames' ] = num_frames elif operator == '>': self._common_info[ 'min_num_frames' ] = num_frames elif operator == '=': self._common_info[ 'num_frames' ] = num_frames elif operator == CC.UNICODE_NOT_EQUAL_TO: self._common_info[ 'not_num_frames' ] = num_frames elif operator == CC.UNICODE_ALMOST_EQUAL_TO: if num_frames == 0: self._common_info[ 'num_frames' ] = 0 else: self._common_info[ 'min_num_frames' ] = int( num_frames * 0.85 ) self._common_info[ 'max_num_frames' ] = int( num_frames * 1.15 ) if predicate_type == PREDICATE_TYPE_SYSTEM_RATING: ( operator, value, service_key ) = value self._ratings_predicates.append( ( operator, value, service_key ) ) if predicate_type == PREDICATE_TYPE_SYSTEM_RATIO: ( operator, ratio_width, ratio_height ) = value if operator == '=': self._common_info[ 'ratio' ] = ( ratio_width, ratio_height ) elif operator == 'wider than': self._common_info[ 'min_ratio' ] = ( ratio_width, ratio_height ) elif operator == 'taller than': self._common_info[ 'max_ratio' ] = ( ratio_width, ratio_height ) elif operator == CC.UNICODE_NOT_EQUAL_TO: self._common_info[ 'not_ratio' ] = ( ratio_width, ratio_height ) elif operator == CC.UNICODE_ALMOST_EQUAL_TO: self._common_info[ 'min_ratio' ] = ( ratio_width * 0.85, ratio_height ) self._common_info[ 'max_ratio' ] = ( ratio_width * 1.15, ratio_height ) if predicate_type == PREDICATE_TYPE_SYSTEM_SIZE: ( operator, size, unit ) = value size = size * unit if operator == '<': self._common_info[ 'max_size' ] = size elif operator == '>': self._common_info[ 'min_size' ] = size elif operator == '=': self._common_info[ 'size' ] = size elif operator == CC.UNICODE_NOT_EQUAL_TO: self._common_info[ 'not_size' ] = size elif operator == CC.UNICODE_ALMOST_EQUAL_TO: self._common_info[ 'min_size' ] = int( size * 0.85 ) self._common_info[ 'max_size' ] = int( size * 1.15 ) if predicate_type == PREDICATE_TYPE_SYSTEM_NUM_TAGS: self._num_tags_predicates.append( predicate.Duplicate() ) if predicate_type == PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER: ( namespace, operator, num ) = value if operator == '<': self._common_info[ 'max_tag_as_number' ] = ( namespace, num ) elif operator == '>': self._common_info[ 'min_tag_as_number' ] = ( namespace, num ) elif operator == CC.UNICODE_ALMOST_EQUAL_TO: self._common_info[ 'min_tag_as_number' ] = ( namespace, int( num * 0.85 ) ) self._common_info[ 'max_tag_as_number' ] = ( namespace, int( num * 1.15 ) ) if predicate_type == PREDICATE_TYPE_SYSTEM_WIDTH: ( operator, width ) = value if operator == '<': self._common_info[ 'max_width' ] = width elif operator == '>': self._common_info[ 'min_width' ] = width elif operator == '=': self._common_info[ 'width' ] = width elif operator == '\u2260': self._common_info[ 'not_width' ] = width elif operator == CC.UNICODE_ALMOST_EQUAL_TO: if width == 0: self._common_info[ 'width' ] = 0 else: self._common_info[ 'min_width' ] = int( width * 0.85 ) self._common_info[ 'max_width' ] = int( width * 1.15 ) if predicate_type == PREDICATE_TYPE_SYSTEM_NUM_PIXELS: ( operator, num_pixels, unit ) = value num_pixels = num_pixels * unit if operator == '<': self._common_info[ 'max_num_pixels' ] = num_pixels elif operator == '>': self._common_info[ 'min_num_pixels' ] = num_pixels elif operator == '=': self._common_info[ 'num_pixels' ] = num_pixels elif operator == CC.UNICODE_NOT_EQUAL_TO: self._common_info[ 'not_num_pixels' ] = num_pixels elif operator == CC.UNICODE_ALMOST_EQUAL_TO: self._common_info[ 'min_num_pixels' ] = int( num_pixels * 0.85 ) self._common_info[ 'max_num_pixels' ] = int( num_pixels * 1.15 ) if predicate_type == PREDICATE_TYPE_SYSTEM_HEIGHT: ( operator, height ) = value if operator == '<': self._common_info[ 'max_height' ] = height elif operator == '>': self._common_info[ 'min_height' ] = height elif operator == '=': self._common_info[ 'height' ] = height elif operator == '\u2260': self._common_info[ 'not_height' ] = height elif operator == CC.UNICODE_ALMOST_EQUAL_TO: if height == 0: self._common_info[ 'height' ] = 0 else: self._common_info[ 'min_height' ] = int( height * 0.85 ) self._common_info[ 'max_height' ] = int( height * 1.15 ) if predicate_type == PREDICATE_TYPE_SYSTEM_NUM_NOTES: ( operator, num_notes ) = value if operator == '<': self._common_info[ 'max_num_notes' ] = num_notes elif operator == '>': self._common_info[ 'min_num_notes' ] = num_notes elif operator == '=': self._common_info[ 'num_notes' ] = num_notes if predicate_type == PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME: ( operator, name ) = value if operator: label = 'has_note_names' else: label = 'not_has_note_names' if label not in self._common_info: self._common_info[ label ] = set() self._common_info[ label ].add( name ) if predicate_type == PREDICATE_TYPE_SYSTEM_NUM_WORDS: ( operator, num_words ) = value if operator == '<': self._common_info[ 'max_num_words' ] = num_words elif operator == '>': self._common_info[ 'min_num_words' ] = num_words elif operator == '=': self._common_info[ 'num_words' ] = num_words elif operator == CC.UNICODE_NOT_EQUAL_TO: self._common_info[ 'not_num_words' ] = num_words elif operator == CC.UNICODE_ALMOST_EQUAL_TO: if num_words == 0: self._common_info[ 'num_words' ] = 0 else: self._common_info[ 'min_num_words' ] = int( num_words * 0.85 ) self._common_info[ 'max_num_words' ] = int( num_words * 1.15 ) if predicate_type == PREDICATE_TYPE_SYSTEM_LIMIT: limit = value if self._limit is None: self._limit = limit else: self._limit = min( limit, self._limit ) if predicate_type == PREDICATE_TYPE_SYSTEM_FILE_SERVICE: ( operator, status, service_key ) = value if operator: self._required_file_service_statuses[ service_key ].add( status ) else: self._excluded_file_service_statuses[ service_key ].add( status ) if predicate_type == PREDICATE_TYPE_SYSTEM_SIMILAR_TO: ( hashes, max_hamming ) = value self._similar_to = ( hashes, max_hamming ) if predicate_type == PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_COUNT: ( operator, num_relationships, dupe_type ) = value self._duplicate_count_predicates.append( ( operator, num_relationships, dupe_type ) ) if predicate_type == PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_KING: king = value self._king_filter = king if predicate_type == PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS: ( view_type, viewing_locations, operator, viewing_value ) = value self._file_viewing_stats_predicates.append( ( view_type, viewing_locations, operator, viewing_value ) ) def GetDuplicateRelationshipCountPredicates( self ): return self._duplicate_count_predicates def GetFileServiceStatuses( self ): return ( self._required_file_service_statuses, self._excluded_file_service_statuses ) def GetFileViewingStatsPredicates( self ): return self._file_viewing_stats_predicates def GetKingFilter( self ): return self._king_filter def GetLimit( self, apply_implicit_limit = True ): if self._limit is None and apply_implicit_limit: forced_search_limit = HG.client_controller.new_options.GetNoneableInteger( 'forced_search_limit' ) return forced_search_limit return self._limit def GetNumTagsNumberTests( self ) -> typing.Dict[ str, typing.List[ NumberTest ] ]: namespaces_to_tests = collections.defaultdict( list ) for predicate in self._num_tags_predicates: ( namespace, operator, value ) = predicate.GetValue() test = NumberTest.STATICCreateFromCharacters( operator, value ) namespaces_to_tests[ namespace ].append( test ) return namespaces_to_tests def GetRatingsPredicates( self ): return self._ratings_predicates def GetSimilarTo( self ): return self._similar_to def GetSimpleInfo( self ): return self._common_info def GetTimestampRanges( self ): return self._timestamp_ranges def HasSimilarTo( self ): return self._similar_to is not None def HasSystemEverything( self ): return self._has_system_everything def HasSystemLimit( self ): return self._limit is not None def MustBeArchive( self ): return self._archive def MustBeInbox( self ): return self._inbox def MustBeLocal( self ): return self._local def MustNotBeLocal( self ): return self._not_local SEARCH_TYPE_AND = 0 SEARCH_TYPE_OR = 1 class FileSearchContext( HydrusSerialisable.SerialisableBase ): SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_FILE_SEARCH_CONTEXT SERIALISABLE_NAME = 'File Search Context' SERIALISABLE_VERSION = 5 def __init__( self, location_context = None, tag_context = None, search_type = SEARCH_TYPE_AND, predicates = None ): if location_context is None: location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY ) if tag_context is None: tag_context = TagContext() if predicates is None: predicates = [] self._location_context = location_context self._tag_context = tag_context self._search_type = search_type self._predicates = predicates self._search_complete = False self._InitialiseTemporaryVariables() def _GetSerialisableInfo( self ): serialisable_predicates = [ predicate.GetSerialisableTuple() for predicate in self._predicates ] serialisable_location_context = self._location_context.GetSerialisableTuple() return ( serialisable_location_context, self._tag_context.GetSerialisableTuple(), self._search_type, serialisable_predicates, self._search_complete ) def _InitialiseFromSerialisableInfo( self, serialisable_info ): ( serialisable_location_context, serialisable_tag_context, self._search_type, serialisable_predicates, self._search_complete ) = serialisable_info self._location_context = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_location_context ) self._tag_context = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_tag_context ) self._predicates = [ HydrusSerialisable.CreateFromSerialisableTuple( pred_tuple ) for pred_tuple in serialisable_predicates ] self._InitialiseTemporaryVariables() def _InitialiseTemporaryVariables( self ): system_predicates = [ predicate for predicate in self._predicates if predicate.GetType() in SYSTEM_PREDICATE_TYPES ] self._system_predicates = FileSystemPredicates( system_predicates ) tag_predicates = [ predicate for predicate in self._predicates if predicate.GetType() == PREDICATE_TYPE_TAG ] self._tags_to_include = [] self._tags_to_exclude = [] for predicate in tag_predicates: tag = predicate.GetValue() if predicate.GetInclusive(): self._tags_to_include.append( tag ) else: self._tags_to_exclude.append( tag ) namespace_predicates = [ predicate for predicate in self._predicates if predicate.GetType() == PREDICATE_TYPE_NAMESPACE ] self._namespaces_to_include = [] self._namespaces_to_exclude = [] for predicate in namespace_predicates: namespace = predicate.GetValue() if predicate.GetInclusive(): self._namespaces_to_include.append( namespace ) else: self._namespaces_to_exclude.append( namespace ) wildcard_predicates = [ predicate for predicate in self._predicates if predicate.GetType() == PREDICATE_TYPE_WILDCARD ] self._wildcards_to_include = [] self._wildcards_to_exclude = [] for predicate in wildcard_predicates: # this is an important convert. preds store nice looking text, but convert for the actual search wildcard = ConvertTagToSearchable( predicate.GetValue() ) if predicate.GetInclusive(): self._wildcards_to_include.append( wildcard ) else: self._wildcards_to_exclude.append( wildcard ) self._or_predicates = [ predicate for predicate in self._predicates if predicate.GetType() == PREDICATE_TYPE_OR_CONTAINER ] def _UpdateSerialisableInfo( self, version, old_serialisable_info ): if version == 1: ( file_service_key_hex, tag_service_key_hex, include_current_tags, include_pending_tags, serialisable_predicates, search_complete ) = old_serialisable_info search_type = SEARCH_TYPE_AND new_serialisable_info = ( file_service_key_hex, tag_service_key_hex, search_type, include_current_tags, include_pending_tags, serialisable_predicates, search_complete ) return ( 2, new_serialisable_info ) if version == 2: ( file_service_key_hex, tag_service_key_hex, search_type, include_current_tags, include_pending_tags, serialisable_predicates, search_complete ) = old_serialisable_info # screwed up the serialisation code for the previous update, so these were getting swapped search_type = SEARCH_TYPE_AND include_current_tags = True new_serialisable_info = ( file_service_key_hex, tag_service_key_hex, search_type, include_current_tags, include_pending_tags, serialisable_predicates, search_complete ) return ( 3, new_serialisable_info ) if version == 3: ( file_service_key_hex, tag_service_key_hex, search_type, include_current_tags, include_pending_tags, serialisable_predicates, search_complete ) = old_serialisable_info tag_service_key = bytes.fromhex( tag_service_key_hex ) tag_context = TagContext( service_key = tag_service_key, include_current_tags = include_current_tags, include_pending_tags = include_pending_tags ) serialisable_tag_context = tag_context.GetSerialisableTuple() new_serialisable_info = ( file_service_key_hex, serialisable_tag_context, search_type, serialisable_predicates, search_complete ) return ( 4, new_serialisable_info ) if version == 4: ( file_service_key_hex, serialisable_tag_context, search_type, serialisable_predicates, search_complete ) = old_serialisable_info file_service_key = bytes.fromhex( file_service_key_hex ) location_context = ClientLocation.LocationContext.STATICCreateSimple( file_service_key ) serialisable_location_context = location_context.GetSerialisableTuple() new_serialisable_info = ( serialisable_location_context, serialisable_tag_context, search_type, serialisable_predicates, search_complete ) return ( 5, new_serialisable_info ) def FixMissingServices( self, filter_method ): self._location_context.FixMissingServices( filter_method ) self._tag_context.FixMissingServices( filter_method ) def GetLocationContext( self ) -> ClientLocation.LocationContext: return self._location_context def GetNamespacesToExclude( self ): return self._namespaces_to_exclude def GetNamespacesToInclude( self ): return self._namespaces_to_include def GetORPredicates( self ): return self._or_predicates def GetPredicates( self ): return self._predicates def GetSystemPredicates( self ) -> FileSystemPredicates: return self._system_predicates def GetTagContext( self ) -> "TagContext": return self._tag_context def GetTagsToExclude( self ): return self._tags_to_exclude def GetTagsToInclude( self ): return self._tags_to_include def GetWildcardsToExclude( self ): return self._wildcards_to_exclude def GetWildcardsToInclude( self ): return self._wildcards_to_include def HasNoPredicates( self ): return len( self._predicates ) == 0 def IsComplete( self ): return self._search_complete def IsJustSystemEverything( self ): return len( self._predicates ) == 1 and self._system_predicates.HasSystemEverything() def SetComplete( self ): self._search_complete = True def SetLocationContext( self, location_context: ClientLocation.LocationContext ): self._location_context = location_context def SetIncludeCurrentTags( self, value ): self._tag_context.include_current_tags = value def SetIncludePendingTags( self, value ): self._tag_context.include_pending_tags = value def SetPredicates( self, predicates ): self._predicates = predicates self._InitialiseTemporaryVariables() def SetTagServiceKey( self, tag_service_key ): self._tag_context.service_key = tag_service_key self._tag_context.display_service_key = tag_service_key HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_FILE_SEARCH_CONTEXT ] = FileSearchContext class TagContext( HydrusSerialisable.SerialisableBase ): SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_tag_context SERIALISABLE_NAME = 'Tag Search Context' SERIALISABLE_VERSION = 2 def __init__( self, service_key = CC.COMBINED_TAG_SERVICE_KEY, include_current_tags = True, include_pending_tags = True, display_service_key = None ): self.service_key = service_key self.include_current_tags = include_current_tags self.include_pending_tags = include_pending_tags if display_service_key is None: self.display_service_key = self.service_key else: self.display_service_key = display_service_key def __eq__( self, other ): if isinstance( other, TagContext ): return self.__hash__() == other.__hash__() return NotImplemented def __hash__( self ): return ( self.service_key, self.include_current_tags, self.include_pending_tags, self.display_service_key ).__hash__() def _GetSerialisableInfo( self ): return ( self.service_key.hex(), self.include_current_tags, self.include_pending_tags, self.display_service_key.hex() ) def _InitialiseFromSerialisableInfo( self, serialisable_info ): ( encoded_service_key, self.include_current_tags, self.include_pending_tags, encoded_display_service_key ) = serialisable_info self.service_key = bytes.fromhex( encoded_service_key ) self.display_service_key = bytes.fromhex( encoded_display_service_key ) def _UpdateSerialisableInfo( self, version, old_serialisable_info ): if version == 1: ( encoded_service_key, self.include_current_tags, self.include_pending_tags ) = old_serialisable_info encoded_display_service_key = encoded_service_key new_serialisable_info = ( encoded_service_key, self.include_current_tags, self.include_pending_tags, encoded_display_service_key ) return ( 2, new_serialisable_info ) def FixMissingServices( self, filter_method ): if len( filter_method( [ self.service_key ] ) ) == 0: self.service_key = CC.COMBINED_TAG_SERVICE_KEY def IsAllKnownTags( self ): return self.service_key == CC.COMBINED_TAG_SERVICE_KEY def ToString( self, name_method ): return name_method( self.service_key ) HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_tag_context ] = TagContext class FavouriteSearchManager( HydrusSerialisable.SerialisableBase ): SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_FAVOURITE_SEARCH_MANAGER SERIALISABLE_NAME = 'Favourite Search Manager' SERIALISABLE_VERSION = 1 def __init__( self ): HydrusSerialisable.SerialisableBase.__init__( self ) self._favourite_search_rows = [] self._lock = threading.Lock() self._dirty = False def _GetSerialisableInfo( self ): serialisable_favourite_search_info = [] for row in self._favourite_search_rows: ( folder, name, file_search_context, synchronised, media_sort, media_collect ) = row serialisable_file_search_context = file_search_context.GetSerialisableTuple() if media_sort is None: serialisable_media_sort = None else: serialisable_media_sort = media_sort.GetSerialisableTuple() if media_collect is None: serialisable_media_collect = None else: serialisable_media_collect = media_collect.GetSerialisableTuple() serialisable_row = ( folder, name, serialisable_file_search_context, synchronised, serialisable_media_sort, serialisable_media_collect ) serialisable_favourite_search_info.append( serialisable_row ) return serialisable_favourite_search_info def _InitialiseFromSerialisableInfo( self, serialisable_info ): self._favourite_search_rows = [] for serialisable_row in serialisable_info: ( folder, name, serialisable_file_search_context, synchronised, serialisable_media_sort, serialisable_media_collect ) = serialisable_row file_search_context = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_file_search_context ) if serialisable_media_sort is None: media_sort = None else: media_sort = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_media_sort ) if serialisable_media_collect is None: media_collect = None else: media_collect = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_media_collect ) row = ( folder, name, file_search_context, synchronised, media_sort, media_collect ) self._favourite_search_rows.append( row ) def GetFavouriteSearch( self, desired_folder_name, desired_name ): with self._lock: for ( folder, name, file_search_context, synchronised, media_sort, media_collect ) in self._favourite_search_rows: if folder == desired_folder_name and name == desired_name: return ( file_search_context, synchronised, media_sort, media_collect ) raise HydrusExceptions.DataMissing( 'Could not find a favourite search named "{}"!'.format( desired_name ) ) def GetFavouriteSearchRows( self ): return list( self._favourite_search_rows ) def GetFoldersToNames( self ): with self._lock: folders_to_names = collections.defaultdict( list ) for ( folder, name, file_search_context, synchronised, media_sort, media_collect ) in self._favourite_search_rows: folders_to_names[ folder ].append( name ) return folders_to_names def IsDirty( self ): with self._lock: return self._dirty def SetClean( self ): with self._lock: self._dirty = False def SetDirty( self ): with self._lock: self._dirty = True def SetFavouriteSearchRows( self, favourite_search_rows ): with self._lock: self._favourite_search_rows = list( favourite_search_rows ) self._dirty = True HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_FAVOURITE_SEARCH_MANAGER ] = FavouriteSearchManager class PredicateCount( object ): def __init__( self, min_current_count: int, min_pending_count: int, max_current_count: typing.Optional[ int ], max_pending_count: typing.Optional[ int ] ): self.min_current_count = min_current_count self.min_pending_count = min_pending_count self.max_current_count = max_current_count if max_current_count is not None else min_current_count self.max_pending_count = max_pending_count if max_pending_count is not None else min_pending_count def __eq__( self, other ): if isinstance( other, PredicateCount ): return self.__hash__() == other.__hash__() return NotImplemented def __hash__( self ): return ( self.min_current_count, self.min_pending_count, self.max_current_count, self.max_pending_count ).__hash__() def __repr__( self ): return 'Predicate Count: {}-{} +{}-{}'.format( self.min_current_count, self.max_current_count, self.min_pending_count, self.max_pending_count ) def AddCounts( self, count: "PredicateCount" ): ( self.min_current_count, self.max_current_count ) = ClientData.MergeCounts( self.min_current_count, self.max_current_count, count.min_current_count, count.max_current_count ) ( self.min_pending_count, self.max_pending_count) = ClientData.MergeCounts( self.min_pending_count, self.max_pending_count, count.min_pending_count, count.max_pending_count ) def Duplicate( self ): return PredicateCount( self.min_current_count, self.min_pending_count, self.max_current_count, self.max_pending_count ) def GetMinCount( self, current_or_pending = None ): if current_or_pending is None: return self.min_current_count + self.min_pending_count elif current_or_pending == HC.CONTENT_STATUS_CURRENT: return self.min_current_count elif current_or_pending == HC.CONTENT_STATUS_PENDING: return self.min_pending_count def GetSuffixString( self ) -> str: suffix_components = [] if self.min_current_count > 0 or self.max_current_count > 0: number_text = HydrusData.ToHumanInt( self.min_current_count ) if self.max_current_count > self.min_current_count: number_text = '{}-{}'.format( number_text, HydrusData.ToHumanInt( self.max_current_count ) ) suffix_components.append( '({})'.format( number_text ) ) if self.min_pending_count > 0 or self.max_pending_count > 0: number_text = HydrusData.ToHumanInt( self.min_pending_count ) if self.max_pending_count > self.min_pending_count: number_text = '{}-{}'.format( number_text, HydrusData.ToHumanInt( self.max_pending_count ) ) suffix_components.append( '(+{})'.format( number_text ) ) return ' '.join( suffix_components ) def HasNonZeroCount( self ): return self.min_current_count > 0 or self.min_pending_count > 0 or self.max_current_count > 0 or self.max_pending_count > 0 def HasZeroCount( self ): return not self.HasNonZeroCount() @staticmethod def STATICCreateCurrentCount( current_count ) -> "PredicateCount": return PredicateCount( current_count, 0, None, None ) @staticmethod def STATICCreateNullCount() -> "PredicateCount": return PredicateCount( 0, 0, None, None ) @staticmethod def STATICCreateStaticCount( current_count, pending_count ) -> "PredicateCount": return PredicateCount( current_count, pending_count, None, None ) EDIT_PRED_TYPES = { PREDICATE_TYPE_SYSTEM_AGE, PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, PREDICATE_TYPE_SYSTEM_MODIFIED_TIME, PREDICATE_TYPE_SYSTEM_HEIGHT, PREDICATE_TYPE_SYSTEM_WIDTH, PREDICATE_TYPE_SYSTEM_RATIO, PREDICATE_TYPE_SYSTEM_NUM_PIXELS, PREDICATE_TYPE_SYSTEM_DURATION, PREDICATE_TYPE_SYSTEM_FRAMERATE, PREDICATE_TYPE_SYSTEM_NUM_FRAMES, PREDICATE_TYPE_SYSTEM_FILE_SERVICE, PREDICATE_TYPE_SYSTEM_KNOWN_URLS, PREDICATE_TYPE_SYSTEM_HASH, PREDICATE_TYPE_SYSTEM_LIMIT, PREDICATE_TYPE_SYSTEM_MIME, PREDICATE_TYPE_SYSTEM_RATING, PREDICATE_TYPE_SYSTEM_NUM_TAGS, PREDICATE_TYPE_SYSTEM_NUM_NOTES, PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME, PREDICATE_TYPE_SYSTEM_NUM_WORDS, PREDICATE_TYPE_SYSTEM_SIMILAR_TO, PREDICATE_TYPE_SYSTEM_SIZE, PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER, PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_COUNT, PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS, PREDICATE_TYPE_OR_CONTAINER, PREDICATE_TYPE_NAMESPACE, PREDICATE_TYPE_WILDCARD, PREDICATE_TYPE_TAG } class Predicate( HydrusSerialisable.SerialisableBase ): SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_PREDICATE SERIALISABLE_NAME = 'File Search Predicate' SERIALISABLE_VERSION = 5 def __init__( self, predicate_type: int = None, value: object = None, inclusive: bool = True, count = None ): if predicate_type == PREDICATE_TYPE_SYSTEM_MIME and value is not None: value = tuple( ConvertSpecificFiletypesToSummary( value ) ) if predicate_type == PREDICATE_TYPE_OR_CONTAINER: value = list( value ) value.sort( key = lambda p: HydrusTags.ConvertTagToSortable( p.ToString() ) ) if isinstance( value, ( list, set ) ): value = tuple( value ) if count is None: count = PredicateCount.STATICCreateNullCount() self._predicate_type = predicate_type self._value = value self._inclusive = inclusive self._count = count self._count_text_suffix = '' self._ideal_sibling = None self._siblings = None self._parents = None self._parent_predicates = set() if self._predicate_type == PREDICATE_TYPE_PARENT: self._parent_key = HydrusData.GenerateKey() else: self._parent_key = None self._RecalculateMatchableSearchTexts() # self._RecalcPythonHash() def __eq__( self, other ): if isinstance( other, Predicate ): if self._predicate_type == PREDICATE_TYPE_PARENT: return False return self.__hash__() == other.__hash__() return NotImplemented def __hash__( self ): return self._python_hash def __repr__( self ): return 'Predicate: ' + str( ( self._predicate_type, self._value, self._inclusive, self._count.GetMinCount() ) ) def _RecalcPythonHash( self ): if self._predicate_type == PREDICATE_TYPE_PARENT: self._python_hash = self._parent_key.__hash__() else: self._python_hash = ( self._predicate_type, self._value, self._inclusive ).__hash__() def _GetSerialisableInfo( self ): if self._predicate_type in ( PREDICATE_TYPE_SYSTEM_RATING, PREDICATE_TYPE_SYSTEM_FILE_SERVICE ): ( operator, value, service_key ) = self._value serialisable_value = ( operator, value, service_key.hex() ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_SIMILAR_TO: ( hashes, max_hamming ) = self._value serialisable_value = ( [ hash.hex() for hash in hashes ], max_hamming ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_KNOWN_URLS: ( operator, rule_type, rule, description ) = self._value if rule_type in ( 'url_match', 'url_class' ): serialisable_rule = rule.GetSerialisableTuple() else: serialisable_rule = rule serialisable_value = ( operator, rule_type, serialisable_rule, description ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_HASH: ( hashes, hash_type ) = self._value serialisable_value = ( [ hash.hex() for hash in hashes ], hash_type ) elif self._predicate_type == PREDICATE_TYPE_OR_CONTAINER: or_predicates = self._value serialisable_value = HydrusSerialisable.SerialisableList( or_predicates ).GetSerialisableTuple() else: serialisable_value = self._value return ( self._predicate_type, serialisable_value, self._inclusive ) def _InitialiseFromSerialisableInfo( self, serialisable_info ): ( self._predicate_type, serialisable_value, self._inclusive ) = serialisable_info if self._predicate_type in ( PREDICATE_TYPE_SYSTEM_RATING, PREDICATE_TYPE_SYSTEM_FILE_SERVICE ): ( operator, value, service_key ) = serialisable_value self._value = ( operator, value, bytes.fromhex( service_key ) ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_SIMILAR_TO: ( serialisable_hashes, max_hamming ) = serialisable_value self._value = ( tuple( [ bytes.fromhex( serialisable_hash ) for serialisable_hash in serialisable_hashes ] ) , max_hamming ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_KNOWN_URLS: ( operator, rule_type, serialisable_rule, description ) = serialisable_value if rule_type in ( 'url_match', 'url_class' ): rule = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_rule ) else: rule = serialisable_rule self._value = ( operator, rule_type, rule, description ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_HASH: ( serialisable_hashes, hash_type ) = serialisable_value self._value = ( tuple( [ bytes.fromhex( serialisable_hash ) for serialisable_hash in serialisable_hashes ] ), hash_type ) elif self._predicate_type in ( PREDICATE_TYPE_SYSTEM_AGE, PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, PREDICATE_TYPE_SYSTEM_MODIFIED_TIME ): ( operator, age_type, age_value ) = serialisable_value self._value = ( operator, age_type, tuple( age_value ) ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS: ( view_type, viewing_locations, operator, viewing_value ) = serialisable_value self._value = ( view_type, tuple( viewing_locations ), operator, viewing_value ) elif self._predicate_type == PREDICATE_TYPE_OR_CONTAINER: serialisable_or_predicates = serialisable_value self._value = tuple( HydrusSerialisable.CreateFromSerialisableTuple( serialisable_or_predicates ) ) else: self._value = serialisable_value if isinstance( self._value, list ): self._value = tuple( self._value ) self._RecalcPythonHash() def _RecalculateMatchableSearchTexts( self ): if self._predicate_type == PREDICATE_TYPE_TAG: self._matchable_search_texts = { self._value } if self._siblings is not None: self._matchable_search_texts.update( self._siblings ) else: self._matchable_search_texts = set() def _UpdateSerialisableInfo( self, version, old_serialisable_info ): if version == 1: ( predicate_type, serialisable_value, inclusive ) = old_serialisable_info if predicate_type == PREDICATE_TYPE_SYSTEM_AGE: ( operator, years, months, days, hours ) = serialisable_value serialisable_value = ( operator, 'delta', ( years, months, days, hours ) ) new_serialisable_info = ( predicate_type, serialisable_value, inclusive ) return ( 2, new_serialisable_info ) if version == 2: ( predicate_type, serialisable_value, inclusive ) = old_serialisable_info if predicate_type in ( PREDICATE_TYPE_SYSTEM_HASH, PREDICATE_TYPE_SYSTEM_SIMILAR_TO ): # other value is either hash type or max hamming distance ( serialisable_hash, other_value ) = serialisable_value serialisable_hashes = ( serialisable_hash, ) serialisable_value = ( serialisable_hashes, other_value ) new_serialisable_info = ( predicate_type, serialisable_value, inclusive ) return ( 3, new_serialisable_info ) if version == 3: ( predicate_type, serialisable_value, inclusive ) = old_serialisable_info if predicate_type == PREDICATE_TYPE_SYSTEM_NUM_TAGS: ( operator, num ) = serialisable_value namespace = '*' serialisable_value = ( namespace, operator, num ) new_serialisable_info = ( predicate_type, serialisable_value, inclusive ) return ( 4, new_serialisable_info ) if version == 4: ( predicate_type, serialisable_value, inclusive ) = old_serialisable_info if predicate_type == PREDICATE_TYPE_SYSTEM_MIME: specific_mimes = serialisable_value summary_mimes = ConvertSpecificFiletypesToSummary( specific_mimes ) serialisable_value = tuple( summary_mimes ) new_serialisable_info = ( predicate_type, serialisable_value, inclusive ) return ( 5, new_serialisable_info ) def GetCopy( self ): return Predicate( self._predicate_type, self._value, self._inclusive, count = self._count.Duplicate() ) def GetCount( self ): return self._count def GetCountlessCopy( self ): return Predicate( self._predicate_type, self._value, self._inclusive ) def GetNamespace( self ): if self._predicate_type in SYSTEM_PREDICATE_TYPES: return 'system' elif self._predicate_type == PREDICATE_TYPE_NAMESPACE: namespace = self._value return namespace elif self._predicate_type in ( PREDICATE_TYPE_PARENT, PREDICATE_TYPE_TAG, PREDICATE_TYPE_WILDCARD ): tag_analogue = self._value ( namespace, subtag ) = HydrusTags.SplitTag( tag_analogue ) if namespace == '*': return '' else: return namespace else: return '' def GetIdealPredicate( self ): if self._ideal_sibling is None: return None else: return Predicate( PREDICATE_TYPE_TAG, self._ideal_sibling, self._inclusive ) def GetIdealSibling( self ): return self._ideal_sibling def GetInclusive( self ): # patch from an upgrade mess-up ~v144 if not hasattr( self, '_inclusive' ): if self._predicate_type not in SYSTEM_PREDICATE_TYPES: ( operator, value ) = self._value self._value = value self._inclusive = operator == '+' else: self._inclusive = True self._RecalcPythonHash() return self._inclusive def GetInfo( self ): return ( self._predicate_type, self._value, self._inclusive ) def GetInverseCopy( self ): if self._predicate_type == PREDICATE_TYPE_SYSTEM_ARCHIVE: return Predicate( PREDICATE_TYPE_SYSTEM_INBOX ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_INBOX: return Predicate( PREDICATE_TYPE_SYSTEM_ARCHIVE ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_LOCAL: return Predicate( PREDICATE_TYPE_SYSTEM_NOT_LOCAL ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NOT_LOCAL: return Predicate( PREDICATE_TYPE_SYSTEM_LOCAL ) elif self._predicate_type in ( PREDICATE_TYPE_TAG, PREDICATE_TYPE_NAMESPACE, PREDICATE_TYPE_WILDCARD ): return Predicate( self._predicate_type, self._value, not self._inclusive ) elif self._predicate_type in ( PREDICATE_TYPE_SYSTEM_HAS_AUDIO, PREDICATE_TYPE_SYSTEM_HAS_EXIF, PREDICATE_TYPE_SYSTEM_HAS_HUMAN_READABLE_EMBEDDED_METADATA, PREDICATE_TYPE_SYSTEM_HAS_ICC_PROFILE ): return Predicate( self._predicate_type, not self._value ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_KING: return Predicate( self._predicate_type, not self._value ) else: return None def GetMatchableSearchTexts( self ): return self._matchable_search_texts def GetParentPredicates( self ): return self._parent_predicates def GetTextsAndNamespaces( self, render_for_user: bool, or_under_construction: bool = False ): if self._predicate_type == PREDICATE_TYPE_OR_CONTAINER: texts_and_namespaces = [] if or_under_construction: texts_and_namespaces.append( ( 'OR: ', 'system' ) ) for or_predicate in self._value: texts_and_namespaces.append( ( or_predicate.ToString(), or_predicate.GetNamespace() ) ) texts_and_namespaces.append( ( ' OR ', 'system' ) ) texts_and_namespaces = texts_and_namespaces[ : -1 ] else: texts_and_namespaces = [ ( self.ToString( render_for_user = render_for_user ), self.GetNamespace() ) ] return texts_and_namespaces def GetType( self ): return self._predicate_type def GetUnnamespacedCopy( self ): if self._predicate_type == PREDICATE_TYPE_TAG: ( namespace, subtag ) = HydrusTags.SplitTag( self._value ) return Predicate( self._predicate_type, subtag, self._inclusive, count = self._count.Duplicate() ) return self.GetCopy() def GetValue( self ): return self._value def HasIdealSibling( self ): return self._ideal_sibling is not None def HasParentPredicates( self ): return len( self._parent_predicates ) > 0 def IsEditable( self ): return self._predicate_type in EDIT_PRED_TYPES def IsInclusive( self ): return self._inclusive def IsInvertible( self ): return self.GetInverseCopy() is not None def IsMutuallyExclusive( self, predicate ): if self._predicate_type == PREDICATE_TYPE_SYSTEM_EVERYTHING: return True if self.IsInvertible() and predicate == self.GetInverseCopy(): return True my_type = self._predicate_type other_type = predicate.GetType() if my_type == other_type: if my_type in ( PREDICATE_TYPE_SYSTEM_LIMIT, PREDICATE_TYPE_SYSTEM_HASH, PREDICATE_TYPE_SYSTEM_SIMILAR_TO ): return True return False def IsUIEditable( self, ideal_predicate: "Predicate" ) -> bool: # bleh if self._predicate_type != ideal_predicate.GetType(): return False ideal_value = ideal_predicate.GetValue() if self._value is None and ideal_value is not None: return False if self._predicate_type in ( PREDICATE_TYPE_SYSTEM_AGE, PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, PREDICATE_TYPE_SYSTEM_MODIFIED_TIME ): # age_type if self._value[1] != ideal_value[1]: return False elif self._predicate_type == PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS: # view_type if self._value[0] != ideal_value[0]: return False elif self._predicate_type == PREDICATE_TYPE_SYSTEM_KNOWN_URLS: # rule type if self._value[1] != ideal_value[1]: return False return True def SetCountTextSuffix( self, suffix: str ): self._count_text_suffix = suffix def SetIdealSibling( self, tag: str ): self._ideal_sibling = tag def SetInclusive( self, inclusive ): self._inclusive = inclusive self._RecalcPythonHash() def SetKnownParents( self, parents: typing.Set[ str ] ): self._parents = parents self._parent_predicates = [ Predicate( PREDICATE_TYPE_PARENT, parent ) for parent in self._parents ] def SetKnownSiblings( self, siblings: typing.Set[ str ] ): self._siblings = siblings self._RecalculateMatchableSearchTexts() def ToString( self, with_count: bool = True, tag_display_type: int = ClientTags.TAG_DISPLAY_ACTUAL, render_for_user: bool = False, or_under_construction: bool = False ) -> str: base = '' count_text = '' if with_count: suffix = self._count.GetSuffixString() if len( suffix ) > 0: count_text += ' {}'.format( suffix ) if self._count_text_suffix != '': count_text += ' ({})'.format( self._count_text_suffix ) if self._predicate_type in SYSTEM_PREDICATE_TYPES: if self._predicate_type == PREDICATE_TYPE_SYSTEM_EVERYTHING: base = 'everything' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_INBOX: base = 'inbox' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_ARCHIVE: base = 'archive' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_UNTAGGED: base = 'untagged' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_LOCAL: base = 'local' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NOT_LOCAL: base = 'not local' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_EMBEDDED_METADATA: base = 'embedded metadata' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_DIMENSIONS: base = 'dimensions' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_TIME: base = 'time' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NOTES: base = 'notes' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS: base = 'file relationships' elif self._predicate_type in ( PREDICATE_TYPE_SYSTEM_WIDTH, PREDICATE_TYPE_SYSTEM_HEIGHT, PREDICATE_TYPE_SYSTEM_NUM_NOTES, PREDICATE_TYPE_SYSTEM_NUM_WORDS, PREDICATE_TYPE_SYSTEM_NUM_FRAMES ): has_phrase = None not_has_phrase = None if self._predicate_type == PREDICATE_TYPE_SYSTEM_WIDTH: base = 'width' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_HEIGHT: base = 'height' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NUM_NOTES: base = 'number of notes' has_phrase = ': has notes' not_has_phrase = ': no notes' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NUM_WORDS: base = 'number of words' has_phrase = ': has words' not_has_phrase = ': no words' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NUM_FRAMES: base = 'number of frames' has_phrase = ': has frames' not_has_phrase = ': no frames' if self._value is not None: ( operator, value ) = self._value if operator == '>' and value == 0 and has_phrase is not None: base += has_phrase elif ( ( operator == '=' and value == 0 ) or ( operator == '<' and value == 1 ) ) and not_has_phrase is not None: base += not_has_phrase else: base += ' {} {}'.format( operator, HydrusData.ToHumanInt( value ) ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_DURATION: base = 'duration' if self._value is not None: ( operator, value ) = self._value if operator == '>' and value == 0: base = 'has duration' elif operator == '=' and value == 0: base = 'no duration' else: base += ' {} {}'.format( operator, HydrusData.ConvertMillisecondsToPrettyTime( value ) ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_FRAMERATE: base = 'framerate' if self._value is not None: ( operator, value ) = self._value base += ' {} {}fps'.format( operator, HydrusData.ToHumanInt( value ) ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME: base = 'has note' if self._value is not None: ( operator, name ) = self._value if operator: base = 'has note with name "{}"'.format( name ) else: base = 'does not have note with name "{}"'.format( name ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NUM_TAGS: base = 'number of tags' if self._value is not None: ( namespace, operator, value ) = self._value number_test = NumberTest.STATICCreateFromCharacters( operator, value ) any_namespace = namespace is None or namespace == '*' if number_test.IsAnythingButZero(): if any_namespace: base = 'has tags' else: # shouldn't see this, as it'll be converted to a namespace pred, but here anyway base = 'has {} tags'.format( ClientTags.RenderNamespaceForUser( namespace ) ) elif number_test.IsZero(): if any_namespace: base = 'untagged' else: # shouldn't see this, as it'll be converted to a namespace pred, but here anyway base = 'no {} tags'.format( ClientTags.RenderNamespaceForUser( namespace ) ) else: if not any_namespace: base = 'number of {} tags'.format( ClientTags.RenderNamespaceForUser( namespace ) ) base += ' {} {}'.format( operator, HydrusData.ToHumanInt( value ) ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_RATIO: base = 'ratio' if self._value is not None: ( operator, ratio_width, ratio_height ) = self._value base += ' ' + operator + ' ' + str( ratio_width ) + ':' + str( ratio_height ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_SIZE: base = 'filesize' if self._value is not None: ( operator, size, unit ) = self._value base += ' ' + operator + ' ' + str( size ) + HydrusData.ConvertIntToUnit( unit ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_LIMIT: base = 'limit' if self._value is not None: value = self._value base += ' is ' + HydrusData.ToHumanInt( value ) elif self._predicate_type in ( PREDICATE_TYPE_SYSTEM_AGE, PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, PREDICATE_TYPE_SYSTEM_MODIFIED_TIME ): if self._predicate_type == PREDICATE_TYPE_SYSTEM_AGE: base = 'import time' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME: base = 'last view time' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_MODIFIED_TIME: base = 'modified time' if self._value is not None: ( operator, age_type, age_value ) = self._value if age_type == 'delta': ( years, months, days, hours ) = age_value DAY = 86400 MONTH = DAY * 30 YEAR = DAY * 365 time_delta = 0 time_delta += hours * 3600 time_delta += days * DAY time_delta += months * MONTH time_delta += years * YEAR if operator == '<': pretty_operator = 'since ' elif operator == '>': pretty_operator = 'before ' elif operator == CC.UNICODE_ALMOST_EQUAL_TO: pretty_operator = 'around ' base += ': ' + pretty_operator + HydrusData.TimeDeltaToPrettyTimeDelta( time_delta ) + ' ago' elif age_type == 'date': ( year, month, day ) = age_value dt = datetime.datetime( year, month, day ) try: # make a timestamp (IN GMT SECS SINCE 1970) from the local meaning of 2018/02/01 timestamp = int( time.mktime( dt.timetuple() ) ) except: timestamp = HydrusData.GetNow() if operator == '<': pretty_operator = 'before ' elif operator == '>': pretty_operator = 'since ' elif operator == '=': pretty_operator = 'on the day of ' elif operator == CC.UNICODE_ALMOST_EQUAL_TO: pretty_operator = 'a month either side of ' # convert this GMT TIMESTAMP to a pretty local string base += ': ' + pretty_operator + HydrusData.ConvertTimestampToPrettyTime( timestamp, include_24h_time = False ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NUM_PIXELS: base = 'number of pixels' if self._value is not None: ( operator, num_pixels, unit ) = self._value base += ' ' + operator + ' ' + str( num_pixels ) + ' ' + HydrusData.ConvertIntToPixels( unit ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_KNOWN_URLS: base = 'known url' if self._value is not None: ( operator, rule_type, rule, description ) = self._value base = description elif self._predicate_type == PREDICATE_TYPE_SYSTEM_HAS_AUDIO: base = 'has audio' if self._value is not None: has_audio = self._value if not has_audio: base = 'no audio' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_HAS_EXIF: base = 'image has exif' if self._value is not None: has_exif = self._value if not has_exif: base = 'no exif' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_HAS_HUMAN_READABLE_EMBEDDED_METADATA: base = 'image has human-readable embedded metadata' if self._value is not None: has_human_readable_embedded_metadata = self._value if not has_human_readable_embedded_metadata: base = 'no human-readable embedded metadata' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_HAS_ICC_PROFILE: base = 'image has icc profile' if self._value is not None: has_icc_profile = self._value if not has_icc_profile: base = 'no icc profile' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_HASH: base = 'hash' if self._value is not None: ( hashes, hash_type ) = self._value if self._inclusive: is_phrase = 'is' else: is_phrase = 'is not' if len( hashes ) == 1: base = '{} hash {} {}'.format( hash_type, is_phrase, hashes[0].hex() ) else: base = '{} hash {} in {} hashes'.format( hash_type, is_phrase, HydrusData.ToHumanInt( len( hashes ) ) ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_MIME: base = 'filetype' if self._value is not None: summary_mimes = self._value mime_text = ConvertSummaryFiletypesToString( summary_mimes ) base += ' is ' + mime_text elif self._predicate_type == PREDICATE_TYPE_SYSTEM_RATING: base = 'rating' if self._value is not None: ( operator, value, service_key ) = self._value try: service = HG.client_controller.services_manager.GetService( service_key ) name = service.GetName() if value == 'rated': base = 'has {} rating'.format( name ) elif value == 'not rated': base = 'no {} rating'.format( name ) else: pretty_value = service.ConvertNoneableRatingToString( value ) base += ' for {} {} {}'.format( service.GetName(), operator, pretty_value ) except HydrusExceptions.DataMissing: base = 'unknown rating service system predicate' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_SIMILAR_TO: base = 'similar to' if self._value is not None: ( hashes, max_hamming ) = self._value base += ' {} files using max hamming of {}'.format( HydrusData.ToHumanInt( len( hashes ) ), max_hamming ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_FILE_SERVICE: if self._value is None: base = 'file service' else: ( operator, status, service_key ) = self._value base = 'is' if operator else 'is not' if status == HC.CONTENT_STATUS_CURRENT: base += ' currently in ' elif status == HC.CONTENT_STATUS_DELETED: base += ' deleted from ' elif status == HC.CONTENT_STATUS_PENDING: base += ' pending to ' elif status == HC.CONTENT_STATUS_PETITIONED: base += ' petitioned from ' try: service = HG.client_controller.services_manager.GetService( service_key ) base += service.GetName() except HydrusExceptions.DataMissing: base = 'unknown file service system predicate' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER: if self._value is None: base = 'tag as number' else: ( namespace, operator, num ) = self._value if namespace == '*': n_text = 'any namespace' elif namespace == '': n_text = 'lone number' else: n_text = namespace if operator == CC.UNICODE_ALMOST_EQUAL_TO: o_text = ' about ' elif operator == '<': o_text = ' less than ' elif operator == '>': o_text = ' more than ' base = n_text + o_text + HydrusData.ToHumanInt( num ) elif self._predicate_type == PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_COUNT: base = 'num file relationships' if self._value is not None: ( operator, num_relationships, dupe_type ) = self._value if operator == CC.UNICODE_ALMOST_EQUAL_TO: o_text = ' about ' elif operator == '<': o_text = ' less than ' elif operator == '>': o_text = ' more than ' elif operator == '=': o_text = ' ' base += ' - has' + o_text + HydrusData.ToHumanInt( num_relationships ) + ' ' + HC.duplicate_type_string_lookup[ dupe_type ] elif self._predicate_type == PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_KING: base = '' if self._value is not None: king = self._value if king: o_text = 'is the best quality file of its duplicate group' else: o_text = 'is not the best quality file of its duplicate group' base += o_text elif self._predicate_type == PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS: base = 'file viewing statistics' if self._value is not None: ( view_type, viewing_locations, operator, viewing_value ) = self._value include_media = 'media' in viewing_locations include_previews = 'preview' in viewing_locations if include_media and include_previews: domain = 'all' elif include_media: domain = 'media' elif include_previews: domain = 'preview' else: domain = 'unknown' if view_type == 'views': value_string = HydrusData.ToHumanInt( viewing_value ) elif view_type == 'viewtime': value_string = HydrusData.TimeDeltaToPrettyTimeDelta( viewing_value ) base = '{} {} {} {}'.format( domain, view_type, operator, value_string ) base = HydrusTags.CombineTag( 'system', base ) base = ClientTags.RenderTag( base, render_for_user ) base += count_text elif self._predicate_type == PREDICATE_TYPE_TAG: tag = self._value if not self._inclusive: base = '-' else: base = '' base += ClientTags.RenderTag( tag, render_for_user ) base += count_text elif self._predicate_type == PREDICATE_TYPE_PARENT: base = ' ' tag = self._value base += ClientTags.RenderTag( tag, render_for_user ) base += count_text elif self._predicate_type == PREDICATE_TYPE_NAMESPACE: namespace = self._value if not self._inclusive: base = '-' else: base = '' pretty_namespace = ClientTags.RenderNamespaceForUser( namespace ) anything_tag = HydrusTags.CombineTag( pretty_namespace, '*anything*' ) anything_tag = ClientTags.RenderTag( anything_tag, render_for_user ) base += anything_tag elif self._predicate_type == PREDICATE_TYPE_WILDCARD: if self._value.startswith( '*:' ): ( any_namespace, subtag ) = HydrusTags.SplitTag( self._value ) wildcard = '{} (any namespace)'.format( subtag ) else: wildcard = self._value + ' (wildcard search)' if not self._inclusive: base = '-' else: base = '' base += wildcard elif self._predicate_type == PREDICATE_TYPE_OR_CONTAINER: or_predicates = self._value base = '' if or_under_construction: base += 'OR: ' base += ' OR '.join( ( or_predicate.ToString( render_for_user = render_for_user ) for or_predicate in or_predicates ) ) # pylint: disable=E1101 elif self._predicate_type == PREDICATE_TYPE_LABEL: label = self._value base = label return base HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_PREDICATE ] = Predicate SYSTEM_PREDICATE_INBOX = Predicate( PREDICATE_TYPE_SYSTEM_INBOX, None ) SYSTEM_PREDICATE_ARCHIVE = Predicate( PREDICATE_TYPE_SYSTEM_ARCHIVE, None ) SYSTEM_PREDICATE_LOCAL = Predicate( PREDICATE_TYPE_SYSTEM_LOCAL, None ) SYSTEM_PREDICATE_NOT_LOCAL = Predicate( PREDICATE_TYPE_SYSTEM_NOT_LOCAL, None ) def FilterPredicatesBySearchText( service_key, search_text, predicates: typing.Collection[ Predicate ] ): def compile_re( s ): regular_parts_of_s = s.split( '*' ) escaped_parts_of_s = [ re.escape( rpos ) for rpos in regular_parts_of_s ] s = '.*'.join( escaped_parts_of_s ) # \A is start of string # \Z is end of string # \s is whitespace # ':' is no longer escaped to '\:' in py 3.7 lmaooooo, so some quick hackery if re.escape( ':' ) == r'\:': s = s.replace( r'\:', ':' ) if ':' in s: ( namespace, subtag ) = s.split( ':', 1 ) if namespace == '.*': beginning = r'(\A|:|\s)' s = subtag else: beginning = r'\A' s = r'{}:(.*\s)?{}'.format( namespace, subtag ) elif s.startswith( '.*' ): beginning = r'(\A|:)' else: beginning = r'(\A|:|\s)' if s.endswith( '.*' ): end = r'\Z' # end of string else: end = r'(\s|\Z)' # whitespace or end of string return re.compile( beginning + s + end ) re_predicate = compile_re( search_text ) matches = [] for predicate in predicates: ( predicate_type, value, inclusive ) = predicate.GetInfo() if predicate_type != PREDICATE_TYPE_TAG: continue possible_tags = predicate.GetMatchableSearchTexts() searchable_tags = { ConvertTagToSearchable( possible_tag ) for possible_tag in possible_tags } for searchable_tag in searchable_tags: if re_predicate.search( searchable_tag ) is not None: matches.append( predicate ) break return matches def MergePredicates( predicates ): master_predicate_dict = {} for predicate in predicates: # this works because predicate.__hash__ exists if predicate in master_predicate_dict: master_predicate_dict[ predicate ].GetCount().AddCounts( predicate.GetCount() ) else: master_predicate_dict[ predicate ] = predicate return list( master_predicate_dict.values() ) def SearchTextIsFetchAll( search_text: str ): ( namespace, subtag ) = HydrusTags.SplitTag( search_text ) if namespace in ( '', '*' ) and subtag == '*': return True return False def SearchTextIsNamespaceBareFetchAll( search_text: str ): ( namespace, subtag ) = HydrusTags.SplitTag( search_text ) if namespace not in ( '', '*' ) and subtag == '': return True return False def SearchTextIsNamespaceFetchAll( search_text: str ): ( namespace, subtag ) = HydrusTags.SplitTag( search_text ) if namespace not in ( '', '*' ) and subtag == '*': return True return False def SubtagIsEmpty( search_text: str ): ( namespace, subtag ) = HydrusTags.SplitTag( search_text ) return subtag == '' class ParsedAutocompleteText( object ): def __init__( self, raw_input: str, tag_autocomplete_options: ClientTagsHandling.TagAutocompleteOptions, collapse_search_characters: bool ): self.raw_input = raw_input self._tag_autocomplete_options = tag_autocomplete_options self._collapse_search_characters = collapse_search_characters self.inclusive = not self.raw_input.startswith( '-' ) self.raw_content = HydrusTags.CleanTag( self.raw_input ) def __eq__( self, other ): if isinstance( other, ParsedAutocompleteText ): return self.__hash__() == other.__hash__() return NotImplemented def __hash__( self ): return ( self.raw_input, self._collapse_search_characters ).__hash__() def __repr__( self ): return 'AC Tag Text: {}'.format( self.raw_input ) def _GetSearchText( self, always_autocompleting: bool, force_do_not_collapse: bool = False, allow_unnamespaced_search_gives_any_namespace_wildcards: bool = True ) -> str: text = CollapseWildcardCharacters( self.raw_content ) if len( text ) == 0: return '' if self._collapse_search_characters and not force_do_not_collapse: text = ConvertTagToSearchable( text ) if allow_unnamespaced_search_gives_any_namespace_wildcards and self._tag_autocomplete_options.UnnamespacedSearchGivesAnyNamespaceWildcards(): if ':' not in text: ( namespace, subtag ) = HydrusTags.SplitTag( text ) if namespace == '': if subtag == '': return '' text = '*:{}'.format( subtag ) if always_autocompleting: ( namespace, subtag ) = HydrusTags.SplitTag( text ) should_have_it = len( namespace ) > 0 or len( subtag ) > 0 if should_have_it and not subtag.endswith( '*' ): text = '{}*'.format( text ) return text def GetAddTagPredicate( self ): return Predicate( PREDICATE_TYPE_TAG, self.raw_content ) def GetImmediateFileSearchPredicate( self ): non_tag_predicates = self.GetNonTagFileSearchPredicates() if len( non_tag_predicates ) > 0: return non_tag_predicates[0] tag_search_predicate = Predicate( PREDICATE_TYPE_TAG, self.raw_content, self.inclusive ) return tag_search_predicate def GetNonTagFileSearchPredicates( self ): predicates = [] if self.IsAcceptableForFileSearches(): if self.IsNamespaceSearch(): search_text = self._GetSearchText( False ) ( namespace, subtag ) = HydrusTags.SplitTag( search_text ) predicate = Predicate( PREDICATE_TYPE_NAMESPACE, namespace, self.inclusive ) predicates.append( predicate ) elif self.IsExplicitWildcard(): search_texts = [] allow_unnamespaced_search_gives_any_namespace_wildcards_values = [ True ] always_autocompleting_values = [ True, False ] if '*' in self.raw_content: # don't spam users who type something with this setting turned on allow_unnamespaced_search_gives_any_namespace_wildcards_values.append( False ) for allow_unnamespaced_search_gives_any_namespace_wildcards in allow_unnamespaced_search_gives_any_namespace_wildcards_values: for always_autocompleting in always_autocompleting_values: search_texts.append( self._GetSearchText( always_autocompleting, allow_unnamespaced_search_gives_any_namespace_wildcards = allow_unnamespaced_search_gives_any_namespace_wildcards, force_do_not_collapse = True ) ) for s in list( search_texts ): if ':' not in s: search_texts.append( '*:{}'.format( s ) ) search_texts = HydrusData.DedupeList( search_texts ) predicates.extend( ( Predicate( PREDICATE_TYPE_WILDCARD, search_text, self.inclusive ) for search_text in search_texts ) ) return predicates def GetSearchText( self, always_autocompleting: bool ): return self._GetSearchText( always_autocompleting ) def GetTagAutocompleteOptions( self ): return self._tag_autocomplete_options def IsAcceptableForTagSearches( self ): search_text = self._GetSearchText( False ) if search_text == '': return False bnfa = SearchTextIsNamespaceBareFetchAll( search_text ) nfa = SearchTextIsNamespaceFetchAll( search_text ) fa = SearchTextIsFetchAll( search_text ) bare_ok = self._tag_autocomplete_options.NamespaceBareFetchAllAllowed() or self._tag_autocomplete_options.SearchNamespacesIntoFullTags() namespace_ok = self._tag_autocomplete_options.NamespaceBareFetchAllAllowed() or self._tag_autocomplete_options.NamespaceFetchAllAllowed() or self._tag_autocomplete_options.SearchNamespacesIntoFullTags() fa_ok = self._tag_autocomplete_options.FetchAllAllowed() if bnfa and not bare_ok: return False if nfa and not namespace_ok: return False if fa and not fa_ok: return False return True def IsAcceptableForFileSearches( self ): search_text = self._GetSearchText( False ) if len( search_text ) == 0: return False if SearchTextIsFetchAll( search_text ): return False return True def IsEmpty( self ): return self.raw_input == '' def IsExplicitWildcard( self ): # user has intentionally put a '*' in return '*' in self.raw_content or self._GetSearchText( False ).startswith( '*:' ) def IsNamespaceSearch( self ): search_text = self._GetSearchText( False ) return SearchTextIsNamespaceFetchAll( search_text ) or SearchTextIsNamespaceBareFetchAll( search_text ) def IsTagSearch( self ): if self.IsEmpty() or self.IsExplicitWildcard() or self.IsNamespaceSearch(): return False search_text = self._GetSearchText( False ) if SubtagIsEmpty( search_text ): return False return True def SetInclusive( self, inclusive: bool ): self.inclusive = inclusive class PredicateResultsCache( object ): def __init__( self, predicates: typing.Iterable[ Predicate ] ): self._predicates = list( predicates ) def CanServeTagResults( self, parsed_autocomplete_text: ParsedAutocompleteText, exact_match: bool ): return False def FilterPredicates( self, service_key: bytes, search_text: str ): return FilterPredicatesBySearchText( service_key, search_text, self._predicates ) def GetPredicates( self ): return self._predicates class PredicateResultsCacheInit( PredicateResultsCache ): def __init__( self ): PredicateResultsCache.__init__( self, [] ) class PredicateResultsCacheSystem( PredicateResultsCache ): pass class PredicateResultsCacheMedia( PredicateResultsCache ): # we could do a bunch of 'valid while media hasn't changed since last time', but experimentally, this is swapped out with a system cache on every new blank input, so no prob pass class PredicateResultsCacheTag( PredicateResultsCache ): def __init__( self, predicates: typing.Iterable[ Predicate ], strict_search_text: str, exact_match: bool ): PredicateResultsCache.__init__( self, predicates ) self._strict_search_text = strict_search_text ( self._strict_search_text_namespace, self._strict_search_text_subtag ) = HydrusTags.SplitTag( self._strict_search_text ) self._exact_match = exact_match def CanServeTagResults( self, parsed_autocomplete_text: ParsedAutocompleteText, exact_match: bool ): strict_search_text = parsed_autocomplete_text.GetSearchText( False ) if self._exact_match: if exact_match and strict_search_text == self._strict_search_text: return True else: return False else: tag_autocomplete_options = parsed_autocomplete_text.GetTagAutocompleteOptions() ( strict_search_text_namespace, strict_search_text_subtag ) = HydrusTags.SplitTag( strict_search_text ) # if SearchTextIsFetchAll( self._strict_search_text ): # if '*' searches are ok, we should have all results return tag_autocomplete_options.FetchAllAllowed() # subtag_to_namespace_search = self._strict_search_text_namespace == '' and self._strict_search_text_subtag != '' and strict_search_text_namespace != '' if subtag_to_namespace_search: # if a user searches 'char*' and then later 'character:samus*', we may have the results # namespace changed, so if we do not satisfy this slim case, we can't provide any results we_searched_namespace_as_subtag = strict_search_text_namespace.startswith( self._strict_search_text_subtag ) return we_searched_namespace_as_subtag and tag_autocomplete_options.SearchNamespacesIntoFullTags() # if self._strict_search_text_namespace != strict_search_text_namespace: return False # # if user searched 'character:' or 'character:*', we may have the results # if we do, we have all possible results if SearchTextIsNamespaceBareFetchAll( self._strict_search_text ): return tag_autocomplete_options.NamespaceBareFetchAllAllowed() if SearchTextIsNamespaceFetchAll( self._strict_search_text ): return tag_autocomplete_options.NamespaceFetchAllAllowed() # # 'sam' will match 'samus', character:sam will match character:samus return strict_search_text_subtag.startswith( self._strict_search_text_subtag )