searching with wildcards
+The autocomplete tag dropdown supports wildcard searching with '*'.
+ +The '*' will match any number of characters. Every normal search has a secret '*' on the end that you don't see, which is how full words get matched from you only typing in a few letters.
+This is useful when you can only remember part of a word, or can't spell its first half. You can put the '*' anywhere, but you may want to experiment to get used to the exact way these searches work. Some results can be surprising!
+ +You can select the special predicate inserted at the top of your autocomplete results (the highlighted '*vagelion' and '*va*ge*' above), and it will work like any other predicate. It will return all files that match that wildcard, i.e. every file for every other tag in the current dropdown list.
+This is particularly useful if you have a number of files with commonly structured over-informationed tags, like this:
+ +In this case, selecting the 'title:cool pic*' predicate will return all three images in the same search, where you can conveniently give them some more-easily searched tags like 'series:cool pic' and 'page:1', 'page:2', 'page:3'.
using flash in fullscreen view
Flash files are sometimes interested in inputs (like spacebar or mouse-scrollwheel) that mean something to hydrus's fullscreen view, and the libraries I have to use to show flash don't handle these events like normal windows. I now have it set so if your mouse is inside the flash window, the input will go to the flash, and if it is outside, it goes to the fullscreen window.
So, if you want to play a flash game in fullscreen, keep your mouse inside the window.
diff --git a/help/changelog.html b/help/changelog.html index d331ae84..9f0407ff 100755 --- a/help/changelog.html +++ b/help/changelog.html @@ -8,6 +8,26 @@changelog
-
+
version 142
+ - added wildcards to autocomplete results and file queries +
- autocomplete results will match your wildcard _exactly_ +
- a new predicate type will appear at the top of wildcard queries; selecting it will search files with that wildcard +
- the new wildcard predicate can be prepended with a minus sign to exclude from results just like normal tag and namespace queries +
- in wildcard predicates, namespace and/or tag can contain wildcard characters +
- added some wildcard help +
- putting in '***' as an autocomplete query is now a pretty bad idea! +
- fixed some logic in how tags are matched against unusual search input in the db +
- sped up and cleaned how tags are matched against search input +
- fixed some namespace logic in how tags are matched against search input +
- below-character-threshold autocomplete queries will now return all applicable namespace results (e.g. putting in '1' will return [ 1, page:1, chapter:1, volume:1 ], not just [ 1 ]) +
- added 'open externally' to launch the file in default external program to thumbnail and media viewer menu +
- added a five second delay between gallery-page fetches in the downloader to reduce chance of 429 errors (this was affecting big sankaku searches) +
- added danbooru webm downloading +
- fixed a typo in the thread watcher +
- fixed a bit-rot bug that was stopping the 'like' ratings filter from launching +
- fixed right click menu in custom filter +
version 141
- combined mappings are no longer calculated and stored diff --git a/help/wildcard_cool_pic.png b/help/wildcard_cool_pic.png new file mode 100644 index 00000000..c55a45f3 Binary files /dev/null and b/help/wildcard_cool_pic.png differ diff --git a/help/wildcard_gelion.png b/help/wildcard_gelion.png new file mode 100644 index 00000000..62c9c8fe Binary files /dev/null and b/help/wildcard_gelion.png differ diff --git a/help/wildcard_vage.png b/help/wildcard_vage.png new file mode 100644 index 00000000..49bcd7b9 Binary files /dev/null and b/help/wildcard_vage.png differ diff --git a/include/ClientConstants.py b/include/ClientConstants.py index f3d8de7f..22a57957 100755 --- a/include/ClientConstants.py +++ b/include/ClientConstants.py @@ -1571,6 +1571,19 @@ class FileSearchContext( object ): elif operator == '-': self._namespaces_to_exclude.append( namespace ) + wildcard_predicates = [ predicate for predicate in predicates if predicate.GetPredicateType() == HC.PREDICATE_TYPE_WILDCARD ] + + self._wildcards_to_include = [] + self._wildcards_to_exclude = [] + + for predicate in wildcard_predicates: + + ( operator, wildcard ) = predicate.GetValue() + + if operator == '+': self._wildcards_to_include.append( wildcard ) + elif operator == '-': self._wildcards_to_exclude.append( wildcard ) + + def GetFileServiceKey( self ): return self._file_service_key def GetNamespacesToExclude( self ): return self._namespaces_to_exclude @@ -1580,6 +1593,8 @@ class FileSearchContext( object ): def GetTagServiceKey( self ): return self._tag_service_key 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 IncludeCurrentTags( self ): return self._include_current_tags def IncludePendingTags( self ): return self._include_pending_tags diff --git a/include/ClientDB.py b/include/ClientDB.py index c74a87bc..64c7f2e7 100755 --- a/include/ClientDB.py +++ b/include/ClientDB.py @@ -1835,7 +1835,7 @@ class ServiceDB( FileDB, MessageDB, TagDB, RatingDB ): if len( half_complete_tag ) > 0: - normal_characters = 'abcdefghijklmnopqrstuvwxyz0123456789' + normal_characters = set( 'abcdefghijklmnopqrstuvwxyz0123456789' ) half_complete_tag_can_be_matched = True @@ -1855,8 +1855,21 @@ class ServiceDB( FileDB, MessageDB, TagDB, RatingDB ): # a search for '[s' actually only does 's' # so, let's do the old and slower LIKE instead of MATCH in weird cases + # note that queries with '*' are passed to LIKE, because MATCH only supports appended wildcards 'gun*', and not complex stuff like '*gun*' + if half_complete_tag_can_be_matched: return [ tag_id for ( tag_id, ) in self._c.execute( 'SELECT docid FROM tags_fts4 WHERE tag MATCH ?;', ( '"' + half_complete_tag + '*"', ) ) ] - else: return [ tag_id for ( tag_id, ) in self._c.execute( 'SELECT tag_id FROM tags WHERE tag LIKE ?;', ( '%' + half_complete_tag + '%', ) ) ] + else: + + possible_tag_ids_half_complete_tag = half_complete_tag + + if '*' in possible_tag_ids_half_complete_tag: + + possible_tag_ids_half_complete_tag = possible_tag_ids_half_complete_tag.replace( '*', '%' ) + + else: possible_tag_ids_half_complete_tag += '%' + + return [ tag_id for ( tag_id, ) in self._c.execute( 'SELECT tag_id FROM tags WHERE tag LIKE ? OR tag LIKE ?;', ( possible_tag_ids_half_complete_tag, ' ' + possible_tag_ids_half_complete_tag ) ) ] + if ':' in half_complete_tag: @@ -1894,7 +1907,8 @@ class ServiceDB( FileDB, MessageDB, TagDB, RatingDB ): ( namespace_id, tag_id ) = self._GetNamespaceIdTagId( tag ) - predicates_phrase = 'namespace_id = ' + HC.u( namespace_id ) + ' AND tag_id = ' + HC.u( tag_id ) + if ':' in tag: predicates_phrase = 'namespace_id = ' + HC.u( namespace_id ) + ' AND tag_id = ' + HC.u( tag_id ) + else: predicates_phrase = 'tag_id = ' + HC.u( tag_id ) except: predicates_phrase = '1 = 1' @@ -2044,6 +2058,9 @@ class ServiceDB( FileDB, MessageDB, TagDB, RatingDB ): namespaces_to_include = search_context.GetNamespacesToInclude() namespaces_to_exclude = search_context.GetNamespacesToExclude() + wildcards_to_include = search_context.GetWildcardsToInclude() + wildcards_to_exclude = search_context.GetWildcardsToExclude() + include_current_tags = search_context.IncludeCurrentTags() include_pending_tags = search_context.IncludePendingTags() @@ -2120,7 +2137,7 @@ class ServiceDB( FileDB, MessageDB, TagDB, RatingDB ): else: sql_predicates.append( '( duration < ' + HC.u( max_duration ) + ' OR duration IS NULL )' ) - if len( tags_to_include ) > 0 or len( namespaces_to_include ) > 0: + if len( tags_to_include ) > 0 or len( namespaces_to_include ) > 0 or len( wildcards_to_include ) > 0: query_hash_ids = None @@ -2134,6 +2151,14 @@ class ServiceDB( FileDB, MessageDB, TagDB, RatingDB ): else: query_hash_ids.intersection_update( namespace_query_hash_ids ) + if len( wildcards_to_include ) > 0: + + wildcard_query_hash_ids = HC.IntelligentMassIntersect( ( self._GetHashIdsFromWildcard( file_service_key, tag_service_key, wildcard, include_current_tags, include_pending_tags ) for wildcard in wildcards_to_include ) ) + + if query_hash_ids is None: query_hash_ids = wildcard_query_hash_ids + else: query_hash_ids.intersection_update( wildcard_query_hash_ids ) + + if len( sql_predicates ) > 1: query_hash_ids.intersection_update( [ id for ( id, ) in self._c.execute( 'SELECT hash_id FROM files_info WHERE ' + ' AND '.join( sql_predicates ) + ';' ) ] ) else: @@ -2229,6 +2254,8 @@ class ServiceDB( FileDB, MessageDB, TagDB, RatingDB ): for namespace in namespaces_to_exclude: exclude_query_hash_ids.update( self._GetHashIdsFromNamespace( file_service_key, tag_service_key, namespace, include_current_tags, include_pending_tags ) ) + for wildcard in wildcards_to_exclude: exclude_query_hash_ids.update( self._GetHashIdsFromWildcard( file_service_key, tag_service_key, wildcard, include_current_tags, include_pending_tags ) ) + if file_service_type == HC.FILE_REPOSITORY and HC.options[ 'exclude_deleted_files' ]: exclude_query_hash_ids.update( [ hash_id for ( hash_id, ) in self._c.execute( 'SELECT hash_id FROM deleted_files WHERE service_id = ?;', ( self._local_file_service_id, ) ) ] ) query_hash_ids.difference_update( exclude_query_hash_ids ) @@ -2513,6 +2540,93 @@ class ServiceDB( FileDB, MessageDB, TagDB, RatingDB ): return hash_ids + def _GetHashIdsFromWildcard( self, file_service_key, tag_service_key, wildcard, include_current_tags, include_pending_tags ): + + statuses = [] + + if include_current_tags: statuses.append( HC.CURRENT ) + if include_pending_tags: statuses.append( HC.PENDING ) + + if len( statuses ) == 0: return {} + + predicates = [] + + if len( statuses ) > 0: predicates.append( 'mappings.status IN ' + HC.SplayListForDB( statuses ) ) + + if file_service_key == HC.COMBINED_FILE_SERVICE_KEY: + + table_phrase = 'mappings' + + else: + + table_phrase = 'mappings, files_info USING ( hash_id )' + + file_service_id = self._GetServiceId( file_service_key ) + + predicates.append( 'files_info.service_id = ' + HC.u( file_service_id ) ) + + + if tag_service_key != HC.COMBINED_TAG_SERVICE_KEY: + + tag_service_id = self._GetServiceId( tag_service_key ) + + predicates.append( 'mappings.service_id = ' + HC.u( tag_service_id ) ) + + + if len( predicates ) > 0: predicates_phrase = ' AND '.join( predicates ) + ' AND ' + else: predicates_phrase = '' + + def GetNamespaceIdsFromWildcard( w ): + + if '*' in w: + + w = w.replace( '*', '%' ) + + return { namespace_id for ( namespace_id, ) in self._c.execute( 'SELECT namespace_id FROM namespaces WHERE namespace LIKE ?;', ( w, ) ) } + + else: + + namespace_id = self._GetNamespaceId( w ) + + return [ namespace_id ] + + + + def GetTagIdsFromWildcard( w ): + + if '*' in w: + + w = w.replace( '*', '%' ) + + return { tag_id for ( tag_id, ) in self._c.execute( 'SELECT tag_id FROM tags WHERE tag LIKE ?;', ( w, ) ) } + + else: + + ( namespace_id, tag_id ) = self._GetNamespaceIdTagId( w ) + + return [ tag_id ] + + + + if ':' in wildcard: + + ( namespace_wildcard, tag_wildcard ) = wildcard.split( ':', 1 ) + + possible_namespace_ids = GetNamespaceIdsFromWildcard( namespace_wildcard ) + possible_tag_ids = GetTagIdsFromWildcard( tag_wildcard ) + + hash_ids = { id for ( id, ) in self._c.execute( 'SELECT hash_id FROM ' + table_phrase + ' WHERE ' + predicates_phrase + 'namespace_id IN ' + HC.SplayListForDB( possible_namespace_ids ) + ' AND tag_id IN ' + HC.SplayListForDB( possible_tag_ids ) + ';' ) } + + else: + + possible_tag_ids = GetTagIdsFromWildcard( wildcard ) + + hash_ids = { id for ( id, ) in self._c.execute( 'SELECT hash_id FROM ' + table_phrase + ' WHERE ' + predicates_phrase + 'tag_id IN ' + HC.SplayListForDB( possible_tag_ids ) + ';' ) } + + + return hash_ids + + def _GetHydrusSessions( self ): now = HC.GetNow() @@ -5178,15 +5292,6 @@ class DB( ServiceDB ): def _UpdateDB( self, version ): - if version == 91: - - ( HC.options, ) = self._c.execute( 'SELECT options FROM options;' ).fetchone() - - HC.options[ 'num_autocomplete_chars' ] = 2 - - self._c.execute( 'UPDATE options SET options = ?;', ( HC.options, ) ) - - if version == 93: self._c.execute( 'CREATE TABLE gui_sessions ( name TEXT, info TEXT_YAML );' ) diff --git a/include/ClientGUICanvas.py b/include/ClientGUICanvas.py index 5dcffa25..f7b24a03 100755 --- a/include/ClientGUICanvas.py +++ b/include/ClientGUICanvas.py @@ -703,6 +703,16 @@ class Canvas( object ): + def _OpenExternally( self ): + + hash = self._current_display_media.GetHash() + mime = self._current_display_media.GetMime() + + path = CC.GetFilePath( hash, mime ) + + subprocess.call( 'start "" "' + path + '"', shell = True ) + + def _PrefetchImages( self ): pass def _RecalcZoom( self ): @@ -1523,6 +1533,7 @@ class CanvasFullscreenMediaListBrowser( CanvasFullscreenMediaList ): elif command == 'inbox': self._Inbox() elif command == 'manage_ratings': self._ManageRatings() elif command == 'manage_tags': self._ManageTags() + elif command == 'open_externally': self._OpenExternally() elif command in ( 'pan_up', 'pan_down', 'pan_left', 'pan_right' ): distance = 20 @@ -1617,6 +1628,10 @@ class CanvasFullscreenMediaListBrowser( CanvasFullscreenMediaList ): menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'remove', HC.LOCAL_FILE_SERVICE_KEY ), '&remove' ) menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'delete', HC.LOCAL_FILE_SERVICE_KEY ), '&delete' ) + menu.AppendSeparator() + + menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'open_externally', HC.LOCAL_FILE_SERVICE_KEY ), '&open externally' ) + share_menu = wx.Menu() copy_menu = wx.Menu() @@ -1907,6 +1922,7 @@ class CanvasFullscreenMediaListCustomFilter( CanvasFullscreenMediaList ): elif command == 'inbox': self._Inbox() elif command == 'manage_ratings': self._ManageRatings() elif command == 'manage_tags': self._ManageTags() + elif command == 'open_externally': self._OpenExternally() elif command == 'remove': self._Remove() elif command == 'slideshow': self._StartSlideshow( data ) elif command == 'slideshow_pause_play': self._PausePlaySlideshow() @@ -1938,6 +1954,14 @@ class CanvasFullscreenMediaListCustomFilter( CanvasFullscreenMediaList ): def EventShowMenu( self, event ): + services = HC.app.GetManager( 'services' ).GetServices() + + local_ratings_services = [ service for service in services if service.GetServiceType() in ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) ] + + i_can_post_ratings = len( local_ratings_services ) > 0 + + # + self._last_drag_coordinates = None # to stop successive right-click drag warp bug menu = wx.Menu() @@ -1988,6 +2012,8 @@ class CanvasFullscreenMediaListCustomFilter( CanvasFullscreenMediaList ): menu.AppendSeparator() + menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'open_externally', HC.LOCAL_FILE_SERVICE_KEY ), '&open externally' ) + share_menu = wx.Menu() copy_menu = wx.Menu() @@ -2651,7 +2677,7 @@ class RatingsFilterFrameLike( CanvasFullscreenMediaListFilter ): def __init__( self, my_parent, page_key, service_key, media_results ): - CanvasFullscreenMediaListFilter.__init__( self, my_parent, page_key, HC.LOCAL_FILE_SERVICE_KEY, [], media_results ) + CanvasFullscreenMediaListFilter.__init__( self, my_parent, page_key, HC.LOCAL_FILE_SERVICE_KEY, media_results ) self._rating_service_key = service_key self._service = HC.app.GetManager( 'services' ).GetService( service_key ) diff --git a/include/ClientGUICommon.py b/include/ClientGUICommon.py index 813d242a..970ed1c1 100755 --- a/include/ClientGUICommon.py +++ b/include/ClientGUICommon.py @@ -625,6 +625,8 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ): must_do_a_search = False + if '*' in search_text: must_do_a_search = True + if ':' in search_text: ( namespace, half_complete_tag ) = search_text.split( ':' ) @@ -652,7 +654,6 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ): media = self._media_callable() - # if synchro not on, then can't rely on current media as being accurate for current preds, so search db normally if media is not None and self._synchronised.IsOn(): fetch_from_db = False @@ -662,18 +663,18 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ): results = HC.app.Read( 'autocomplete_tags', file_service_key = self._file_service_key, tag_service_key = self._tag_service_key, tag = search_text, include_current = self._include_current, include_pending = self._include_pending ) - matches = results.GetMatches( half_complete_tag ) + matches = results.GetMatches( search_text ) else: - if must_do_a_search or self._first_letters == '' or not half_complete_tag.startswith( self._first_letters ): + if must_do_a_search or self._first_letters == '' or not search_text.startswith( self._first_letters ): - self._first_letters = half_complete_tag + self._first_letters = search_text self._cached_results = HC.app.Read( 'autocomplete_tags', file_service_key = self._file_service_key, tag_service_key = self._tag_service_key, half_complete_tag = search_text, include_current = self._include_current, include_pending = self._include_pending ) - matches = self._cached_results.GetMatches( half_complete_tag ) + matches = self._cached_results.GetMatches( search_text ) else: @@ -695,14 +696,8 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ): current_tags_flat_iterable = itertools.chain.from_iterable( lists_of_current_tags ) pending_tags_flat_iterable = itertools.chain.from_iterable( lists_of_pending_tags ) - current_tags_flat = [ tag for tag in current_tags_flat_iterable if HC.SearchEntryMatchesTag( half_complete_tag, tag ) ] - pending_tags_flat = [ tag for tag in pending_tags_flat_iterable if HC.SearchEntryMatchesTag( half_complete_tag, tag ) ] - - if self._current_namespace != '': - - current_tags_flat = [ tag for tag in current_tags_flat if tag.startswith( self._current_namespace + ':' ) ] - pending_tags_flat = [ tag for tag in pending_tags_flat if tag.startswith( self._current_namespace + ':' ) ] - + current_tags_flat = [ tag for tag in current_tags_flat_iterable if HC.SearchEntryMatchesTag( search_text, tag ) ] + pending_tags_flat = [ tag for tag in pending_tags_flat_iterable if HC.SearchEntryMatchesTag( search_text, tag ) ] current_tags_to_count = collections.Counter( current_tags_flat ) pending_tags_to_count = collections.Counter( pending_tags_flat ) @@ -714,11 +709,12 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ): results = CC.AutocompleteMatchesPredicates( self._tag_service_key, [ HC.Predicate( HC.PREDICATE_TYPE_TAG, ( operator, tag ), { HC.CURRENT : current_tags_to_count[ tag ], HC.PENDING : pending_tags_to_count[ tag ] } ) for tag in tags_to_do ] ) - matches = results.GetMatches( half_complete_tag ) + matches = results.GetMatches( search_text ) if self._current_namespace != '': matches.insert( 0, HC.Predicate( HC.PREDICATE_TYPE_NAMESPACE, ( operator, namespace ) ) ) + if '*' in search_text: matches.insert( 0, HC.Predicate( HC.PREDICATE_TYPE_WILDCARD, ( operator, search_text ) ) ) entry_predicate = HC.Predicate( HC.PREDICATE_TYPE_TAG, ( operator, search_text ) ) diff --git a/include/ClientGUIMedia.py b/include/ClientGUIMedia.py index 30fa0216..4a0798fa 100755 --- a/include/ClientGUIMedia.py +++ b/include/ClientGUIMedia.py @@ -10,6 +10,7 @@ import HydrusThreading import itertools import os import random +import subprocess import threading import time import traceback @@ -466,6 +467,16 @@ class MediaPanel( ClientGUIMixins.ListeningMediaList, wx.ScrolledWindow ): if len( hashes ) > 0: HC.pubsub.pub( 'new_thread_dumper', hashes ) + def _OpenExternally( self ): + + hash = self._focussed_media.GetHash() + mime = self._focussed_media.GetMime() + + path = CC.GetFilePath( hash, mime ) + + subprocess.call( 'start "" "' + path + '"', shell = True ) + + def _PetitionFiles( self, file_service_key ): hashes = self._GetSelectedHashes() @@ -1551,6 +1562,7 @@ class MediaPanelThumbnails( MediaPanel ): elif command == 'manage_tags': self._ManageTags() elif command == 'modify_account': self._ModifyUploaders( data ) elif command == 'new_thread_dumper': self._NewThreadDumper() + elif command == 'open_externally': self._OpenExternally() elif command == 'petition': self._PetitionFiles( data ) elif command == 'ratings_filter': self._RatingsFilter( data ) elif command == 'remove': self._Remove() @@ -1931,6 +1943,10 @@ class MediaPanelThumbnails( MediaPanel ): # share + menu.AppendSeparator() + + menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'open_externally', HC.LOCAL_FILE_SERVICE_KEY ), '&open externally' ) + share_menu = wx.Menu() # diff --git a/include/HydrusConstants.py b/include/HydrusConstants.py index e74f875d..b1cc0f63 100755 --- a/include/HydrusConstants.py +++ b/include/HydrusConstants.py @@ -65,7 +65,7 @@ options = {} # Misc NETWORK_VERSION = 15 -SOFTWARE_VERSION = 141 +SOFTWARE_VERSION = 142 UNSCALED_THUMBNAIL_DIMENSIONS = ( 200, 200 ) @@ -403,6 +403,7 @@ PREDICATE_TYPE_SYSTEM = 0 PREDICATE_TYPE_TAG = 1 PREDICATE_TYPE_NAMESPACE = 2 PREDICATE_TYPE_PARENT = 3 +PREDICATE_TYPE_WILDCARD = 4 SITE_TYPE_DEVIANT_ART = 0 SITE_TYPE_GIPHY = 1 @@ -1273,7 +1274,30 @@ def SearchEntryMatchesPredicate( search_entry, predicate ): def SearchEntryMatchesTag( search_entry, tag, search_siblings = True ): - if ':' in search_entry: ( search_entry_namespace, search_entry ) = search_entry.split( ':', 1 ) + def compile_re( s ): + + regular_parts_of_s = s.split( '*' ) + + escaped_parts_of_s = [ re.escape( part ) for part in regular_parts_of_s ] + + s = '.*'.join( escaped_parts_of_s ) + + return re.compile( '(\\A|\\s)' + s + '(\\s|\\Z)', flags = re.UNICODE ) + + + if ':' in search_entry: + + search_namespace = True + + ( namespace_entry, search_entry ) = search_entry.split( ':', 1 ) + + namespace_re_predicate = compile_re( namespace_entry ) + + else: search_namespace = False + + if '*' not in search_entry: search_entry += '*' + + re_predicate = compile_re( search_entry ) if search_siblings: @@ -1289,11 +1313,18 @@ def SearchEntryMatchesTag( search_entry, tag, search_siblings = True ): ( n, t ) = tag.split( ':', 1 ) + if search_namespace and re.search( namespace_re_predicate, n ) is None: continue + comparee = t - else: comparee = tag + else: + + if search_namespace: continue + + comparee = tag + - if comparee.startswith( search_entry ) or ' ' + search_entry in comparee: return True + if re.search( re_predicate, comparee ) is not None: return True return False @@ -2333,6 +2364,15 @@ class Predicate( HydrusYAMLBase ): base += namespace + u':*' + elif self._predicate_type == PREDICATE_TYPE_WILDCARD: + + ( operator, wildcard ) = self._value + + if operator == '-': base = u'-' + elif operator == '+': base = u'' + + base += wildcard + return base diff --git a/include/HydrusDownloading.py b/include/HydrusDownloading.py index eeaca024..ec39f087 100644 --- a/include/HydrusDownloading.py +++ b/include/HydrusDownloading.py @@ -256,13 +256,15 @@ class DownloaderBooru( Downloader ): image_string = soup.find( text = re.compile( 'Save this file' ) ) + if image_string is None: image_string = soup.find( text = re.compile( 'Save this video' ) ) + image = image_string.parent image_url = image[ 'href' ] else: - if image.name == 'img': + if image.name in ( 'img', 'video' ): image_url = image[ 'src' ] @@ -1648,6 +1650,8 @@ class ImportQueueGeneratorGallery( ImportQueueGenerator ): self._job_key.SetVariable( 'queue', queue ) + time.sleep( 5 ) + for downloader in downloaders_to_remove: downloaders.remove( downloader ) @@ -1751,7 +1755,7 @@ class ImportQueueGeneratorThread( ImportQueueGenerator ): if HC.shutdown or self._job_key.IsDone(): break - if HC.shutdown or self._ob_key.IsDone(): break + if HC.shutdown or self._job_key.IsDone(): break thread_time = self._job_key.GetVariable( 'thread_time' )
-
+