diff --git a/docs/changelog.md b/docs/changelog.md
index dc75f1225..92d35c748 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -7,6 +7,45 @@ title: Changelog
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
+## [Version 576](https://github.com/hydrusnetwork/hydrus/releases/tag/v576)
+
+### file access latency
+
+* the mpv player no longer hangs the UI thread on file load if the file manager is busy. it now just shows a black square until things are freed up. sorry this took so long to fix!
+* the client file storage system has a new two-layer locking mechanism that allows for massively more parallel access, even when files are importing. file imports should lag out file/thumbnail load significantly less
+* the 'check for file orphans' maintenance job is now a significantly less-blocking process. it'll lock each of the 512 subfolders in turn, which will delay some file/thumb access, but it won't need an exclusive write lock on the whole client files manager for the entire job any more
+* also, the 'check for file orphans' job now saves thumbnails, sticking them in a subdirectory of the export location you designate. some users wanted to try using saucenao-type services to try and recover when they had a thumb but no file, so let's see how this works out
+
+### import options in watchers and gallery downloaders
+
+* instead of the mysterious 'set options to queries' button, there is now a button beside the 'import options' one that is only visible when the current selection of downloaders has differing file limit or import options than the main page. although this is still a complicated idea, I hope this will make it a little more obvious what is going on
+* I did the same deal for the watchers page, for checker options or the import options
+* it may be that some import options appear to differ after a client restart despite having the same settings. if you get this, let me know the details and I'll fix it!
+* the 'set options to watchers' command now updates note import options
+* fixed gallery imports not always saving changes to their note/tag import options in the main gui session, particularly if they are paused and the client is closed soon after options change
+* improved the import options button's handling of certain options objects when editing, I suspect this fixed some weird edge-case situations of 'I thought I did not set that there' kind of thing, particularly when doing multiple sets of editing to a page and then sub-queries within it
+* the import options button also has a stricter 'set default' command, clearing out old data more thoroughly to help with inter-widget comparisons here
+
+### misc
+
+* thanks to a user, we now have support for legacy Microsoft Office documents (.doc, .ppt, .xls), and a framework for other OLE based documents in future
+* this new feature requires the `olefile` library. this is optional, and everyone who runs the normal built release now gets it, but if you run from source you might like to re-run the `setup_venv` script this week so you get it
+* thanks to a user, the danbooru parsers now grab a danbooru post time accurate and precise to the second (previously they were getting 24-hour resolution, I think UTC midnight)
+* uploading large files to the file repository should now use significantly less memory and be far less error prone. due to an in-elegant network request, it was previously timing out the connection if files took too long to upload. the code now streams the upload more cleverly. thanks to the users who helped with this one
+* (tl;dr: if you have a darkmode stylesheet, the colour picker dialog is now fast) it looks like Qt fixed the weird bug that meant certain stylesheets broke the colour picker, so my test that says 'if the user is on Qt 6 and they have a hover-includiig stylesheet, then force a fake stylesheet without that tech before they open the colour-picker dialog and then restore the old one after they close, adding multiple seconds of entry and exit lag to this dialog argh' now no longer applies if you are on Qt 6.6 or later, which is anyone on the built release. let me know if you still have any problems!
+* URLs are now tested against URL Classes by descending order of domain length. this ensures that if you have a URL class for 'api.example.com' and another for 'example.com', and this latter one is set to also apply to subdomains, the specific 'api.example.com' URL Class will be tested first! this was frequently working as desired before, but only for accidental reasons; it is now explicit in all cases
+
+### boring stuff
+
+* cleaned up the the regex list in the filename tagging panel, which had some ancient bad code from the wx days that stored the data in the string labels
+* similarly significantly dejanked the 'ListBook' widget used in the options dialog
+* overhauled my four(!!) separate radiobox classes, merging the best of all into one unified class and getting rid of some similar ancient and horrible 'select by label' tech. about twenty or thirty radioboxes across the program, particularly the stuff you see in system predicate panels, now operate on slightly saner principles
+* fixed up the 'default gui session' combobox in the options, which was also inexplicably using ancient tech
+* updated some misc UI typos and unhelpful tooltips
+* refactored some of the client files manager to work with a 'prefix chunk', which will represent an umbrella prefix in the future system that supports overlapping folders and folders with differing prefix lengths'
+* deleted some old client files manager code
+* thanks to a user, the macOS setup_venv is fixed to point at the correct Cocoa/Quartz requirements.txt file
+
## [Version 575](https://github.com/hydrusnetwork/hydrus/releases/tag/v575)
### misc
@@ -372,45 +411,3 @@ title: Changelog
* untangled some of the presentation import options. stuff like 'is new or in inbox' gets slightly better description labels and cleaner actual logic code
* fixed some type issues, some typo'd pubsubs, and other misc linting
* tried last week's aborted github build update again. the build is now Node 20 compatible
-
-## [Version 565](https://github.com/hydrusnetwork/hydrus/releases/tag/v565)
-
-### tag sorting bonanza
-
-* _options->sort/collect_ now offers four places to customise default tag sort. instead of having one default sort for everything, there's now sort for search pages, media viewers, and the manage tags dialogs launched off of them
-
-### tag filter
-
-* when you copy namespaces from the tag filter list, it now copies the actual underlying data text like `character:`, which you can paste in elsewhere, rather than the pretty `"character" tags` display text
-* brushed up some of the UI and help text on the tag filter UI
-* fixed a couple places where the tag copy menus were trying to let you copy an empty string, which ended up with `-invalid label-`
-* fixed some extremely janked-out logic in the tag filter that was sending `(un)namespaced tags` to the 'except for these' advanced whitelist in many cases. it was technically ok, but not ideal and overall inhuman
-
-### concatenated source urls
-
-* on rule34.xxx and probably some other places, when the file has multiple source urls, the gelbooru-style parsers were pulling the urls in the format [ A, B, C, 'A B C' ], adding this weird extra string concatenation that is obviously invalid. I fixed the parsers so it won't happen again
-* **on update, you are going to get a couple of yes/no dialogs asking if you want to scan for and delete existing instances of these URLs**. if you have a big client, it will take some time to do this scan. the yes/no dialogs will auto-yes after ten minutes, so if you are doing a headless update via docker or something, please be patient--it will go through
-
-### note sidecars
-
-* the note->media sidecar exporter module now has a 'forced name' input. if you want to parse a single note from a .txt or .json that doesn't have a name, you can now force it
-* the sidecard txt separator dropdown in the .txt importer module now has a 'four pipes (||||)' entry in the dropdown as a quick-select beside 'newline'. four pipes is a useful separator of multi-line notes content since it almost certainly won't come up in a normal note
-* some tooltips and stuff are updated around here to better explain what the hell is going on
-* added a unit test to test the forced name
-
-### misc
-
-* to help the recent shortcuts change that merged `numpad` variants of + and left arrow and so on into being seen as the `unmodified` variants, if you have a saved shortcut that _is_ still the `numpad` variant, it will now match the `unmodified` input when the merge mode is on. just means you don't have to remap everything with this mode on--everything merged matches everything
-* added 'copy file known urls' to the 'media' shortcut set
-* I forgot to mention last week that we figured out more native global menubar tech (where the top menubar of the program will embed into your OS's top system menubar) in last week's release, for non-macOS (some versions of Linux) users. the new checkbox is under _gui->Use Native MenuBar_. it defaults to on for macOS and off for everyone else, but feel free to try it. there was a related 'my menubar is now messed up, why?' bug that hit some people in v564 that is fixed today. sorry if you got boshed by this, since it was tricky to manually fix. in future, note you can hit ctrl+p in a default client to bring up the command palette, and then you can type 'options' and can open the options that way, if your menubar isn't working!
-* fixed the `ideal usage` calculation in _database->move media files_ when there are three or more competing storage locations with two or more having a max size that is exceeded by their weight, and one or more having a max size that is only exceeded by their weight a little bit. due to a mistake in how total remaining weight was calculated in the little behind the scenes elimination game here, a location in this situation was exceeding its max size amount by a multiple of `1/(1-total_normalised_weight_of_restricted_locations)`, typically +10-30%. thank you for the report here, it was interesting to figure out!
-* I removed a hack that made the repositories (like the PTR) work for users running super old versions of the client. the hack has now been in place for more than a year. if you run into repository syncing problems, please update to after v511!
-* fixed a dumb status line in the 'check for missing/invalid files' checker thah was double-counting bad files in the popup
-* fixed some media duration 'second' components being rendered with extraneous .0, like '30.0 seconds'
-* fixed a db routine that fetches a huge table in pieces to not repeat a few rows when the ids it is fetching are non-contiguous, and to report the correct quantity of work done as a result (it was saying like 17,563/17,562)
-* the new _help->about_ Qt platformName addition will now say if the actual platformName differs from the running platformName (e.g. if it was set otherwise with a Qt launch parameter)
-
-### client api
-
-* just a small thing, but the under-documented `/manage_database/get_client_options` call now says the four types of default tag sort. I left the old key, `default_tag_sort`, in so as not to break stuff, but it is just a copy of the `search_page` variant in the new `default_tag_sort_xxx` foursome
-* client api version is now 62
diff --git a/docs/old_changelog.html b/docs/old_changelog.html
index b2c33e6f9..79bebbd3f 100644
--- a/docs/old_changelog.html
+++ b/docs/old_changelog.html
@@ -34,6 +34,40 @@
+ -
+
+
+ file access latency
+ - the mpv player no longer hangs the UI thread on file load if the file manager is busy. it now just shows a black square until things are freed up. sorry this took so long to fix!
+ - the client file storage system has a new two-layer locking mechanism that allows for massively more parallel access, even when files are importing. file imports should lag out file/thumbnail load significantly less
+ - the 'check for file orphans' maintenance job is now a significantly less-blocking process. it'll lock each of the 512 subfolders in turn, which will delay some file/thumb access, but it won't need an exclusive write lock on the whole client files manager for the entire job any more
+ - also, the 'check for file orphans' job now saves thumbnails, sticking them in a subdirectory of the export location you designate. some users wanted to try using saucenao-type services to try and recover when they had a thumb but no file, so let's see how this works out
+ import options in watchers and gallery downloaders
+ - instead of the mysterious 'set options to queries' button, there is now a button beside the 'import options' one that is only visible when the current selection of downloaders has differing file limit or import options than the main page. although this is still a complicated idea, I hope this will make it a little more obvious what is going on
+ - I did the same deal for the watchers page, for checker options or the import options
+ - it may be that some import options appear to differ after a client restart despite having the same settings. if you get this, let me know the details and I'll fix it!
+ - the 'set options to watchers' command now updates note import options
+ - fixed gallery imports not always saving changes to their note/tag import options in the main gui session, particularly if they are paused and the client is closed soon after options change
+ - improved the import options button's handling of certain options objects when editing, I suspect this fixed some weird edge-case situations of 'I thought I did not set that there' kind of thing, particularly when doing multiple sets of editing to a page and then sub-queries within it
+ - the import options button also has a stricter 'set default' command, clearing out old data more thoroughly to help with inter-widget comparisons here
+ misc
+ - thanks to a user, we now have support for legacy Microsoft Office documents (.doc, .ppt, .xls), and a framework for other OLE based documents in future
+ - this new feature requires the `olefile` library. this is optional, and everyone who runs the normal built release now gets it, but if you run from source you might like to re-run the `setup_venv` script this week so you get it
+ - thanks to a user, the danbooru parsers now grab a danbooru post time accurate and precise to the second (previously they were getting 24-hour resolution, I think UTC midnight)
+ - uploading large files to the file repository should now use significantly less memory and be far less error prone. due to an in-elegant network request, it was previously timing out the connection if files took too long to upload. the code now streams the upload more cleverly. thanks to the users who helped with this one
+ - (tl;dr: if you have a darkmode stylesheet, the colour picker dialog is now fast) it looks like Qt fixed the weird bug that meant certain stylesheets broke the colour picker, so my test that says 'if the user is on Qt 6 and they have a hover-includiig stylesheet, then force a fake stylesheet without that tech before they open the colour-picker dialog and then restore the old one after they close, adding multiple seconds of entry and exit lag to this dialog argh' now no longer applies if you are on Qt 6.6 or later, which is anyone on the built release. let me know if you still have any problems!
+ - URLs are now tested against URL Classes by descending order of domain length. this ensures that if you have a URL class for 'api.example.com' and another for 'example.com', and this latter one is set to also apply to subdomains, the specific 'api.example.com' URL Class will be tested first! this was frequently working as desired before, but only for accidental reasons; it is now explicit in all cases
+ boring stuff
+ - cleaned up the the regex list in the filename tagging panel, which had some ancient bad code from the wx days that stored the data in the string labels
+ - similarly significantly dejanked the 'ListBook' widget used in the options dialog
+ - overhauled my four(!!) separate radiobox classes, merging the best of all into one unified class and getting rid of some similar ancient and horrible 'select by label' tech. about twenty or thirty radioboxes across the program, particularly the stuff you see in system predicate panels, now operate on slightly saner principles
+ - fixed up the 'default gui session' combobox in the options, which was also inexplicably using ancient tech
+ - updated some misc UI typos and unhelpful tooltips
+ - refactored some of the client files manager to work with a 'prefix chunk', which will represent an umbrella prefix in the future system that supports overlapping folders and folders with differing prefix lengths'
+ - deleted some old client files manager code
+ - thanks to a user, the macOS setup_venv is fixed to point at the correct Cocoa/Quartz requirements.txt file
+
+
-
@@ -48,7 +82,7 @@
- fixed the rendering of some 16-bit pngs that seem to be getting a slightly different image mode on the new version of PIL
- the debug 'gui report mode' now reports extensive info about virtual taglist heights. if I have been working with you on taglists, mostly on the manage tags dialog, that spawn without a scrollbar even though they should, please run this mode and then try to capture the error. hit me up and we'll see if the numbers explain what's going on. I may have also simply fixed the bug
- I think I sped up adding tags to a local tag service that has a lot of siblings/parents
- - updated the default danbooru parsers to get the original and/or translated artist notes. I don't know if a user did this or I did, but my dev machine somehow already had the tech while the defaults did not--if you did this, thinks!
+ - updated the default danbooru parsers to get the original and/or translated artist notes. I don't know if a user did this or I did, but my dev machine somehow already had the tech while the defaults did not--if you did this, thanks!
- added more tweet URL Classes for the default downloader. you should now be able to drag and drop a vxtwitter or fxtwitter URL on the client and it'll work
auto-duplicate resolution
- I have nothing real to show today, but I have a skeleton of code and a good plan on how to get the client resolving easy duplicate pairs by itself. so far, it looks easier than I feared, but, as always, there will be a lot to do. I will keep chipping away at this and will release features in tentative waves for advanced users to play with
diff --git a/hydrus/client/ClientFiles.py b/hydrus/client/ClientFiles.py
index 27489a0d7..0cc93f292 100644
--- a/hydrus/client/ClientFiles.py
+++ b/hydrus/client/ClientFiles.py
@@ -356,11 +356,15 @@ def __init__( self, controller ):
self._controller = controller
- self._file_storage_rwlock = ClientThreading.FileRWLock()
+ # the lock for any file access and for altering the list of locations
+ self._master_locations_rwlock = ClientThreading.FileRWLock()
- self._prefixes_to_client_files_subfolders = collections.defaultdict( list )
- self._smallest_prefix = 2
- self._largest_prefix = 2
+ # the locks for the sub-locations, broken into related umbrella groups
+ self._prefix_umbrellas_to_rwlocks = collections.defaultdict( ClientThreading.FileRWLock )
+
+ self._prefix_umbrellas_to_client_files_subfolders = collections.defaultdict( list )
+ self._shortest_prefix = 2
+ self._longest_prefix = 2
self._physical_file_delete_wait = threading.Event()
@@ -579,6 +583,8 @@ def _ChangeFileExt( self, hash, old_mime, mime ):
def _GenerateExpectedFilePath( self, hash, mime ):
+ # TODO: this guy is presumably nuked or altered when we move to overlapping locations. there is no 'expected' to check, but there might be multiple, or a 'preferred' for imports
+
self._WaitOnWakeup()
subfolder = self._GetSubfolderForFile( hash, 'f' )
@@ -627,13 +633,25 @@ def _GenerateThumbnailBytes( self, file_path, media ):
return thumbnail_bytes
+ def _GetAllSubfolders( self ) -> typing.List[ ClientFilesPhysical.FilesStorageSubfolder ]:
+
+ result = []
+
+ for ( prefix_umbrella, subfolders ) in self._prefix_umbrellas_to_client_files_subfolders.items():
+
+ result.extend( subfolders )
+
+
+ return result
+
+
def _GetCurrentSubfolderBaseLocations( self, only_files = False ):
known_base_locations = set()
- for ( prefix, subfolders ) in self._prefixes_to_client_files_subfolders.items():
+ for ( prefix_umbrella, subfolders ) in self._prefix_umbrellas_to_client_files_subfolders.items():
- if only_files and not prefix.startswith( 'f' ):
+ if only_files and not prefix_umbrella.startswith( 'f' ):
continue
@@ -689,33 +707,38 @@ def _GetFileStorageFreeSpace( self, hash: bytes ) -> int:
def _GetPossibleSubfoldersForFile( self, hash: bytes, prefix_type: str ) -> typing.List[ ClientFilesPhysical.FilesStorageSubfolder ]:
- hash_encoded = hash.hex()
+ prefix_umbrella = self._GetPrefixUmbrella( hash, prefix_type )
- result = []
-
- for i in range( self._smallest_prefix, self._largest_prefix + 1 ):
-
- prefix = prefix_type + hash_encoded[ : i ]
+ if prefix_umbrella in self._prefix_umbrellas_to_client_files_subfolders:
- if prefix in self._prefixes_to_client_files_subfolders:
-
- result.extend( self._prefixes_to_client_files_subfolders[ prefix ] )
-
+ return self._prefix_umbrellas_to_client_files_subfolders[ prefix_umbrella ]
- return result
+ return []
- def _GetAllSubfolders( self ) -> typing.List[ ClientFilesPhysical.FilesStorageSubfolder ]:
+ def _GetPrefixUmbrella( self, hash: bytes, prefix_type: str ) -> str:
- result = []
+ # a prefix umbrella oversees potentially several overlapping prefixes of higher granularity
+ # 'f95' might oversee two instances of 'f95', and one or more 'f95x' subfolder
+ # we know a file fits into a particular umbrella even though we don't know which subfolder it fits into
+ # this is so we can:
+ # A) search for a file in multiple subfolders
+ # B) add a file to one of multiple subfolders
+ # C) move files between these subfolders
+ # without having to worry about crazy locks
- for ( prefix, subfolders ) in self._prefixes_to_client_files_subfolders.items():
-
- result.extend( subfolders )
-
+ return prefix_type + hash.hex()[ : self._shortest_prefix ]
- return result
+
+ def _GetPrefixUmbrellaRWLock( self, hash: bytes, prefix_type: str ) -> ClientThreading.FileRWLock:
+ """
+ You can only call this guy if you have the total lock already!
+ """
+
+ prefix_umbrella = self._GetPrefixUmbrella( hash, prefix_type )
+
+ return self._prefix_umbrellas_to_rwlocks[ prefix_umbrella ]
def _GetRebalanceTuple( self ):
@@ -738,13 +761,13 @@ def _GetRebalanceTuple( self ):
current_base_locations_to_normalised_weights = collections.Counter()
current_base_locations_to_size_estimate = collections.Counter()
- file_prefixes = [ prefix for prefix in self._prefixes_to_client_files_subfolders.keys() if prefix.startswith( 'f' ) ]
+ file_prefix_umbrellas = [ prefix_umbrella for prefix_umbrella in self._prefix_umbrellas_to_client_files_subfolders.keys() if prefix_umbrella.startswith( 'f' ) ]
all_media_base_locations = set( ideal_media_base_locations )
- for file_prefix in file_prefixes:
+ for file_prefix_umbrella in file_prefix_umbrellas:
- subfolders = self._prefixes_to_client_files_subfolders[ file_prefix ]
+ subfolders = self._prefix_umbrellas_to_client_files_subfolders[ file_prefix_umbrella ]
subfolder = subfolders[0]
@@ -867,11 +890,11 @@ def _GetRebalanceTuple( self ):
source_base_location = potential_sources.pop( 0 )
destination_base_location = potential_destinations.pop( 0 )
- random.shuffle( file_prefixes )
+ random.shuffle( file_prefix_umbrellas )
- for file_prefix in file_prefixes:
+ for file_prefix_umbrella in file_prefix_umbrellas:
- subfolders = self._prefixes_to_client_files_subfolders[ file_prefix ]
+ subfolders = self._prefix_umbrellas_to_client_files_subfolders[ file_prefix_umbrella ]
subfolder = subfolders[0]
@@ -879,8 +902,8 @@ def _GetRebalanceTuple( self ):
if base_location == source_base_location:
- overweight_subfolder = ClientFilesPhysical.FilesStorageSubfolder( file_prefix, source_base_location )
- underweight_subfolder = ClientFilesPhysical.FilesStorageSubfolder( file_prefix, destination_base_location )
+ overweight_subfolder = ClientFilesPhysical.FilesStorageSubfolder( subfolder.prefix, source_base_location )
+ underweight_subfolder = ClientFilesPhysical.FilesStorageSubfolder( subfolder.prefix, destination_base_location )
return ( overweight_subfolder, underweight_subfolder )
@@ -888,28 +911,29 @@ def _GetRebalanceTuple( self ):
else:
- thumbnail_prefixes = [ prefix for prefix in self._prefixes_to_client_files_subfolders.keys() if prefix.startswith( 't' ) ]
+ thumbnail_prefix_umbrellas = [ prefix_umbrella for prefix_umbrella in self._prefix_umbrellas_to_client_files_subfolders.keys() if prefix_umbrella.startswith( 't' ) ]
- for thumbnail_prefix in thumbnail_prefixes:
+ for thumbnail_prefix_umbrella in thumbnail_prefix_umbrellas:
if ideal_thumbnail_override_base_location is None:
- file_prefix = 'f' + thumbnail_prefix[1:]
+ file_prefix_umbrella = 'f' + thumbnail_prefix_umbrella[1:]
subfolders = None
- if file_prefix in self._prefixes_to_client_files_subfolders:
+ if file_prefix_umbrella in self._prefix_umbrellas_to_client_files_subfolders:
- subfolders = self._prefixes_to_client_files_subfolders[ file_prefix ]
+ subfolders = self._prefix_umbrellas_to_client_files_subfolders[ file_prefix_umbrella ]
else:
# TODO: Consider better that thumbs might not be split but files would.
# We need to better deal with t43 trying to find its place in f431, and t431 to f43, which means triggering splits or whatever (when we get to that code)
+ # Update: Yeah I've now moved to prefix_umbrellas, and this looks even crazier
- for ( possible_file_prefix, possible_subfolders ) in self._prefixes_to_client_files_subfolders.items():
+ for ( possible_file_prefix_umbrella, possible_subfolders ) in self._prefix_umbrellas_to_client_files_subfolders.items():
- if possible_file_prefix.startswith( file_prefix ) or file_prefix.startswith( possible_file_prefix ):
+ if possible_file_prefix_umbrella.startswith( file_prefix_umbrella ) or file_prefix_umbrella.startswith( possible_file_prefix_umbrella ):
subfolders = possible_subfolders
@@ -934,7 +958,7 @@ def _GetRebalanceTuple( self ):
correct_base_location = ideal_thumbnail_override_base_location
- subfolders = self._prefixes_to_client_files_subfolders[ thumbnail_prefix ]
+ subfolders = self._prefix_umbrellas_to_client_files_subfolders[ thumbnail_prefix_umbrella ]
subfolder = subfolders[0]
@@ -942,8 +966,8 @@ def _GetRebalanceTuple( self ):
if current_thumbnails_base_location != correct_base_location:
- current_subfolder = ClientFilesPhysical.FilesStorageSubfolder( thumbnail_prefix, current_thumbnails_base_location )
- correct_subfolder = ClientFilesPhysical.FilesStorageSubfolder( thumbnail_prefix, correct_base_location )
+ current_subfolder = ClientFilesPhysical.FilesStorageSubfolder( subfolder.prefix, current_thumbnails_base_location )
+ correct_subfolder = ClientFilesPhysical.FilesStorageSubfolder( subfolder.prefix, correct_base_location )
return ( current_subfolder, correct_subfolder )
@@ -979,48 +1003,6 @@ def _HandleCriticalDriveError( self ):
self._controller.pub( 'notify_refresh_network_menu' )
- def _IterateAllFilePaths( self ):
-
- for ( prefix, subfolders ) in self._prefixes_to_client_files_subfolders.items():
-
- if prefix.startswith( 'f' ):
-
- for subfolder in subfolders:
-
- files_dir = subfolder.path
-
- filenames = list( os.listdir( files_dir ) )
-
- for filename in filenames:
-
- yield os.path.join( files_dir, filename )
-
-
-
-
-
-
- def _IterateAllThumbnailPaths( self ):
-
- for ( prefix, subfolders ) in self._prefixes_to_client_files_subfolders.items():
-
- if prefix.startswith( 't' ):
-
- for subfolder in subfolders:
-
- files_dir = subfolder.path
-
- filenames = list( os.listdir( files_dir ) )
-
- for filename in filenames:
-
- yield os.path.join( files_dir, filename )
-
-
-
-
-
-
def _LookForFilePath( self, hash ):
for potential_mime in HC.ALLOWED_MIMES:
@@ -1155,22 +1137,34 @@ def _ReinitSubfolders( self ):
subfolders = self._controller.Read( 'client_files_subfolders' )
- self._prefixes_to_client_files_subfolders = collections.defaultdict( list )
+ self._prefix_umbrellas_to_client_files_subfolders = collections.defaultdict( list )
for subfolder in subfolders:
- self._prefixes_to_client_files_subfolders[ subfolder.prefix ].append( subfolder )
+ self._prefix_umbrellas_to_client_files_subfolders[ subfolder.prefix ].append( subfolder )
- self._smallest_prefix = min( ( len( prefix ) for prefix in self._prefixes_to_client_files_subfolders.keys() ) ) - 1
- self._largest_prefix = max( ( len( prefix ) for prefix in self._prefixes_to_client_files_subfolders.keys() ) ) - 1
+ all_subfolders = []
+
+ for subfolders in self._prefix_umbrellas_to_client_files_subfolders.values():
+
+ all_subfolders.extend( subfolders )
+
+
+ all_prefixes = { subfolder.prefix for subfolder in all_subfolders }
+ all_prefix_lengths = { len( prefix ) for prefix in all_prefixes }
+
+ self._shortest_prefix = min( all_prefix_lengths ) - 1
+ self._longest_prefix = max( all_prefix_lengths ) - 1
+
+ self._prefix_umbrellas_to_rwlocks.clear()
def _ReinitMissingLocations( self ):
self._missing_subfolders = set()
- for ( prefix, subfolders ) in self._prefixes_to_client_files_subfolders.items():
+ for subfolders in self._prefix_umbrellas_to_client_files_subfolders.values():
for subfolder in subfolders:
@@ -1197,7 +1191,7 @@ def _WaitOnWakeup( self ):
def AllLocationsAreDefault( self ):
- with self._file_storage_rwlock.read:
+ with self._master_locations_rwlock.read:
db_dir = self._controller.GetDBDir()
@@ -1228,22 +1222,31 @@ def LocklessAddFileFromBytes( self, hash, mime, file_bytes ):
def AddFile( self, hash, mime, source_path, thumbnail_bytes = None ):
- with self._file_storage_rwlock.write:
+ with self._master_locations_rwlock.read:
- self._AddFile( hash, mime, source_path )
+ with self._GetPrefixUmbrellaRWLock( hash, 'f' ).write:
+
+ self._AddFile( hash, mime, source_path )
+
if thumbnail_bytes is not None:
- self._AddThumbnailFromBytes( hash, thumbnail_bytes )
+ with self._GetPrefixUmbrellaRWLock( hash, 't' ).write:
+
+ self._AddThumbnailFromBytes( hash, thumbnail_bytes )
+
def AddThumbnailFromBytes( self, hash, thumbnail_bytes, silent = False ):
- with self._file_storage_rwlock.write:
+ with self._master_locations_rwlock.read:
- self._AddThumbnailFromBytes( hash, thumbnail_bytes, silent = silent )
+ with self._GetPrefixUmbrellaRWLock( hash, 't' ).write:
+
+ self._AddThumbnailFromBytes( hash, thumbnail_bytes, silent = silent )
+
@@ -1254,15 +1257,28 @@ def ChangeFileExt( self, hash, old_mime, mime ):
return False
- with self._file_storage_rwlock.write:
+ with self._master_locations_rwlock.read:
- return self._ChangeFileExt( hash, old_mime, mime )
+ with self._GetPrefixUmbrellaRWLock( hash, 'f' ).write:
+
+ return self._ChangeFileExt( hash, old_mime, mime )
+
def ClearOrphans( self, move_location = None ):
- with self._file_storage_rwlock.write:
+ files_move_location = move_location
+ thumbnails_move_location = None
+
+ if move_location is not None:
+
+ thumbnails_move_location = os.path.join( move_location, 'thumbnails' )
+
+ HydrusPaths.MakeSureDirectoryExists( thumbnails_move_location )
+
+
+ with self._master_locations_rwlock.read:
job_status = ClientThreading.JobStatus( cancellable = True )
@@ -1274,163 +1290,181 @@ def ClearOrphans( self, move_location = None ):
orphan_paths = []
orphan_thumbnails = []
- for ( i, path ) in enumerate( self._IterateAllFilePaths() ):
-
- ( i_paused, should_quit ) = job_status.WaitIfNeeded()
-
- if should_quit:
-
- return
-
-
- if i % 100 == 0:
-
- status = 'reviewed ' + HydrusData.ToHumanInt( i ) + ' files, found ' + HydrusData.ToHumanInt( len( orphan_paths ) ) + ' orphans'
-
- job_status.SetStatusText( status )
-
+ num_files_reviewed = 0
+ num_thumbnails_reviewed = 0
+
+ all_subfolders_in_order = sorted( self._prefix_umbrellas_to_client_files_subfolders.items() )
+
+ for ( prefix_umbrella, subfolders ) in all_subfolders_in_order:
- try:
+ with self._prefix_umbrellas_to_rwlocks[ prefix_umbrella ].write:
- ( directory, filename ) = os.path.split( path )
+ job_status.SetStatusText( f'checking {prefix_umbrella}' )
- should_be_a_hex_hash = filename[:64]
-
- hash = bytes.fromhex( should_be_a_hex_hash )
-
- is_an_orphan = CG.client_controller.Read( 'is_an_orphan', 'file', hash )
-
- except:
-
- is_an_orphan = True
-
-
- if is_an_orphan:
-
- if move_location is not None:
-
- ( source_dir, filename ) = os.path.split( path )
+ for subfolder in subfolders:
- dest = os.path.join( move_location, filename )
-
- dest = HydrusPaths.AppendPathUntilNoConflicts( dest )
-
- HydrusData.Print( 'Moving the orphan ' + path + ' to ' + dest )
-
- try:
+ for path in subfolder.IterateAllFiles():
- HydrusPaths.MergeFile( path, dest )
+ ( i_paused, should_quit ) = job_status.WaitIfNeeded()
- except Exception as e:
+ if should_quit:
+
+ return
+
- HydrusData.ShowText( f'Had trouble moving orphan from {path} to {dest}! Abandoning job!' )
+ if subfolder.IsForFiles():
+
+ if num_files_reviewed % 100 == 0:
+
+ status = 'reviewed ' + HydrusData.ToHumanInt( num_files_reviewed ) + ' files, found ' + HydrusData.ToHumanInt( len( orphan_paths ) ) + ' orphans'
+
+ job_status.SetStatusText( status, level = 2 )
+
+
+ else:
+
+ if num_thumbnails_reviewed % 100 == 0:
+
+ status = 'reviewed ' + HydrusData.ToHumanInt( num_thumbnails_reviewed ) + ' thumbnails, found ' + HydrusData.ToHumanInt( len( orphan_thumbnails ) ) + ' orphans'
+
+ job_status.SetStatusText( status, level = 2 )
+
+
- HydrusData.ShowException( e, do_wait = False )
+ try:
+
+ ( directory, filename ) = os.path.split( path )
+
+ should_be_a_hex_hash = filename[:64]
+
+ hash = bytes.fromhex( should_be_a_hex_hash )
+
+ orphan_type = 'file' if subfolder.IsForFiles() else 'thumbnail'
+
+ is_an_orphan = CG.client_controller.Read( 'is_an_orphan', orphan_type, hash )
+
+ except:
+
+ is_an_orphan = True
+
- job_status.Cancel()
+ if is_an_orphan:
+
+ if move_location is not None:
+
+ ( source_dir, filename ) = os.path.split( path )
+
+ if subfolder.IsForFiles():
+
+ dest = os.path.join( files_move_location, filename )
+
+ else:
+
+ dest = os.path.join( thumbnails_move_location, filename )
+
+
+ dest = HydrusPaths.AppendPathUntilNoConflicts( dest )
+
+ HydrusData.Print( 'Moving the orphan ' + path + ' to ' + dest )
+
+ try:
+
+ HydrusPaths.MergeFile( path, dest )
+
+ except Exception as e:
+
+ HydrusData.ShowText( f'Had trouble moving orphan from {path} to {dest}! Abandoning job!' )
+
+ HydrusData.ShowException( e, do_wait = False )
+
+ job_status.Cancel()
+
+ return
+
+
+
+ if subfolder.IsForFiles():
+
+ orphan_paths.append( path )
+
+ else:
+
+ orphan_thumbnails.append( path )
+
+
- return
+ if subfolder.IsForFiles():
+
+ num_files_reviewed += 1
+
+ else:
+
+ num_thumbnails_reviewed += 1
+
- orphan_paths.append( path )
-
+ job_status.SetStatusText( 'finished checking' )
+ job_status.DeleteStatusText( level = 2 )
+
time.sleep( 2 )
- for ( i, path ) in enumerate( self._IterateAllThumbnailPaths() ):
-
- ( i_paused, should_quit ) = job_status.WaitIfNeeded()
-
- if should_quit:
-
- return
-
+ if move_location is None:
- if i % 100 == 0:
+ if len( orphan_paths ) > 0:
- status = 'reviewed ' + HydrusData.ToHumanInt( i ) + ' thumbnails, found ' + HydrusData.ToHumanInt( len( orphan_thumbnails ) ) + ' orphans'
+ status = 'found ' + HydrusData.ToHumanInt( len( orphan_paths ) ) + ' orphan files, now deleting'
job_status.SetStatusText( status )
-
- try:
-
- is_an_orphan = False
-
- ( directory, filename ) = os.path.split( path )
-
- should_be_a_hex_hash = filename[:64]
-
- hash = bytes.fromhex( should_be_a_hex_hash )
-
- is_an_orphan = CG.client_controller.Read( 'is_an_orphan', 'thumbnail', hash )
-
- except:
-
- is_an_orphan = True
+ time.sleep( 5 )
-
- if is_an_orphan:
-
- orphan_thumbnails.append( path )
-
-
-
- time.sleep( 2 )
-
- if move_location is None and len( orphan_paths ) > 0:
-
- status = 'found ' + HydrusData.ToHumanInt( len( orphan_paths ) ) + ' orphans, now deleting'
-
- job_status.SetStatusText( status )
-
- time.sleep( 5 )
-
- for ( i, path ) in enumerate( orphan_paths ):
-
- ( i_paused, should_quit ) = job_status.WaitIfNeeded()
-
- if should_quit:
+ for ( i, path ) in enumerate( orphan_paths ):
- return
+ ( i_paused, should_quit ) = job_status.WaitIfNeeded()
+
+ if should_quit:
+
+ return
+
+
+ HydrusData.Print( 'Deleting the orphan ' + path )
+
+ status = 'deleting orphan files: ' + HydrusData.ConvertValueRangeToPrettyString( i + 1, len( orphan_paths ) )
+
+ job_status.SetStatusText( status )
+
+ ClientPaths.DeletePath( path )
- HydrusData.Print( 'Deleting the orphan ' + path )
+
+ if len( orphan_thumbnails ) > 0:
- status = 'deleting orphan files: ' + HydrusData.ConvertValueRangeToPrettyString( i + 1, len( orphan_paths ) )
+ status = 'found ' + HydrusData.ToHumanInt( len( orphan_thumbnails ) ) + ' orphan thumbnails, now deleting'
job_status.SetStatusText( status )
- ClientPaths.DeletePath( path )
+ time.sleep( 5 )
-
-
- if len( orphan_thumbnails ) > 0:
-
- status = 'found ' + HydrusData.ToHumanInt( len( orphan_thumbnails ) ) + ' orphan thumbnails, now deleting'
-
- job_status.SetStatusText( status )
-
- time.sleep( 5 )
-
- for ( i, path ) in enumerate( orphan_thumbnails ):
-
- ( i_paused, should_quit ) = job_status.WaitIfNeeded()
-
- if should_quit:
+ for ( i, path ) in enumerate( orphan_thumbnails ):
- return
+ ( i_paused, should_quit ) = job_status.WaitIfNeeded()
+
+ if should_quit:
+
+ return
+
+
+ HydrusData.Print( 'Deleting the orphan ' + path )
+
+ status = 'deleting orphan thumbnails: ' + HydrusData.ConvertValueRangeToPrettyString( i + 1, len( orphan_thumbnails ) )
+
+ job_status.SetStatusText( status )
+
+ ClientPaths.DeletePath( path, always_delete_fully = True )
-
- status = 'deleting orphan thumbnails: ' + HydrusData.ConvertValueRangeToPrettyString( i + 1, len( orphan_thumbnails ) )
-
- job_status.SetStatusText( status )
-
- HydrusData.Print( 'Deleting the orphan ' + path )
-
- ClientPaths.DeletePath( path, always_delete_fully = True )
@@ -1453,38 +1487,41 @@ def ClearOrphans( self, move_location = None ):
def DeleteNeighbourDupes( self, hash, true_mime ):
- with self._file_storage_rwlock.write:
-
- correct_path = self._GenerateExpectedFilePath( hash, true_mime )
+ with self._master_locations_rwlock.read:
- if not os.path.exists( correct_path ):
+ with self._GetPrefixUmbrellaRWLock( hash, 'f' ).write:
- return # misfire, let's not actually delete the right one
-
-
- for mime in HC.ALLOWED_MIMES:
+ correct_path = self._GenerateExpectedFilePath( hash, true_mime )
- if mime == true_mime:
+ if not os.path.exists( correct_path ):
- continue
+ return # misfire, let's not actually delete the right one
- incorrect_path = self._GenerateExpectedFilePath( hash, mime )
-
- if incorrect_path == correct_path:
+ for mime in HC.ALLOWED_MIMES:
- # some diff mimes have the same ext
-
- continue
+ if mime == true_mime:
+
+ continue
+
-
- if os.path.exists( incorrect_path ):
+ incorrect_path = self._GenerateExpectedFilePath( hash, mime )
- delete_ok = HydrusPaths.DeletePath( incorrect_path )
+ if incorrect_path == correct_path:
+
+ # some diff mimes have the same ext
+
+ continue
+
- if not delete_ok and random.randint( 1, 52 ) != 52:
+ if os.path.exists( incorrect_path ):
+
+ delete_ok = HydrusPaths.DeletePath( incorrect_path )
- self._controller.WriteSynchronous( 'file_maintenance_add_jobs_hashes', { hash }, REGENERATE_FILE_DATA_JOB_DELETE_NEIGHBOUR_DUPES, HydrusTime.GetNow() + ( 7 * 86400 ) )
+ if not delete_ok and random.randint( 1, 52 ) != 52:
+
+ self._controller.WriteSynchronous( 'file_maintenance_add_jobs_hashes', { hash }, REGENERATE_FILE_DATA_JOB_DELETE_NEIGHBOUR_DUPES, HydrusTime.GetNow() + ( 7 * 86400 ) )
+
@@ -1500,7 +1537,7 @@ def DoDeferredPhysicalDeletes( self ):
while not HG.started_shutdown:
- with self._file_storage_rwlock.write:
+ with self._master_locations_rwlock.read:
( file_hash, thumbnail_hash ) = self._controller.Read( 'deferred_physical_delete' )
@@ -1511,38 +1548,44 @@ def DoDeferredPhysicalDeletes( self ):
if file_hash is not None:
- media_result = self._controller.Read( 'media_result', file_hash )
-
- expected_mime = media_result.GetMime()
-
- try:
+ with self._GetPrefixUmbrellaRWLock( file_hash, 'f' ).write:
- path = self._GenerateExpectedFilePath( file_hash, expected_mime )
+ media_result = self._controller.Read( 'media_result', file_hash )
- if not os.path.exists( path ):
+ expected_mime = media_result.GetMime()
+
+ try:
- ( path, actual_mime ) = self._LookForFilePath( file_hash )
+ path = self._GenerateExpectedFilePath( file_hash, expected_mime )
+
+ if not os.path.exists( path ):
+
+ ( path, actual_mime ) = self._LookForFilePath( file_hash )
+
+
+ ClientPaths.DeletePath( path )
+
+ num_files_deleted += 1
+
+ except HydrusExceptions.FileMissingException:
+
+ HydrusData.Print( 'Wanted to physically delete the "{}" file, with expected mime "{}", but it was not found!'.format( file_hash.hex(), HC.mime_string_lookup[ expected_mime ] ) )
-
- ClientPaths.DeletePath( path )
-
- num_files_deleted += 1
-
- except HydrusExceptions.FileMissingException:
-
- HydrusData.Print( 'Wanted to physically delete the "{}" file, with expected mime "{}", but it was not found!'.format( file_hash.hex(), HC.mime_string_lookup[ expected_mime ] ) )
if thumbnail_hash is not None:
- path = self._GenerateExpectedThumbnailPath( thumbnail_hash )
-
- if os.path.exists( path ):
+ with self._GetPrefixUmbrellaRWLock( thumbnail_hash, 't' ).write:
- ClientPaths.DeletePath( path, always_delete_fully = True )
+ path = self._GenerateExpectedThumbnailPath( thumbnail_hash )
- num_thumbnails_deleted += 1
+ if os.path.exists( path ):
+
+ ClientPaths.DeletePath( path, always_delete_fully = True )
+
+ num_thumbnails_deleted += 1
+
@@ -1569,7 +1612,7 @@ def DoDeferredPhysicalDeletes( self ):
def GetAllDirectoriesInUse( self ):
- with self._file_storage_rwlock.read:
+ with self._master_locations_rwlock.read:
subfolders = self._GetAllSubfolders()
@@ -1581,7 +1624,7 @@ def GetAllDirectoriesInUse( self ):
def GetCurrentFileBaseLocations( self ):
- with self._file_storage_rwlock.read:
+ with self._master_locations_rwlock.read:
return self._GetCurrentSubfolderBaseLocations( only_files = True )
@@ -1589,41 +1632,44 @@ def GetCurrentFileBaseLocations( self ):
def GetFilePath( self, hash, mime = None, check_file_exists = True ):
- with self._file_storage_rwlock.read:
+ with self._master_locations_rwlock.read:
if HG.file_report_mode:
HydrusData.ShowText( 'File path request: ' + str( ( hash, mime ) ) )
- if mime is None:
-
- ( path, mime ) = self._LookForFilePath( hash )
-
- else:
-
- path = self._GenerateExpectedFilePath( hash, mime )
+ with self._GetPrefixUmbrellaRWLock( hash, 'f' ).read:
- if check_file_exists and not os.path.exists( path ):
+ if mime is None:
- try:
-
- # let's see if the file exists, but with the wrong ext!
+ ( path, mime ) = self._LookForFilePath( hash )
+
+ else:
+
+ path = self._GenerateExpectedFilePath( hash, mime )
+
+ if check_file_exists and not os.path.exists( path ):
- ( actual_path, old_mime ) = self._LookForFilePath( hash )
+ try:
+
+ # let's see if the file exists, but with the wrong ext!
+
+ ( actual_path, old_mime ) = self._LookForFilePath( hash )
+
+ except HydrusExceptions.FileMissingException:
+
+ raise HydrusExceptions.FileMissingException( 'No file found at path {}!'.format( path ) )
+
- except HydrusExceptions.FileMissingException:
+ self._ChangeFileExt( hash, old_mime, mime )
- raise HydrusExceptions.FileMissingException( 'No file found at path {}!'.format( path ) )
+ # we have now fixed the path, it is good to return
- self._ChangeFileExt( hash, old_mime, mime )
-
- # we have now fixed the path, it is good to return
-
-
- return path
+ return path
+
@@ -1642,11 +1688,14 @@ def GetThumbnailPath( self, media ):
HydrusData.ShowText( 'Thumbnail path request: ' + str( ( hash, mime ) ) )
- with self._file_storage_rwlock.read:
-
- path = self._GenerateExpectedThumbnailPath( hash )
+ with self._master_locations_rwlock.read:
- thumb_missing = not os.path.exists( path )
+ with self._GetPrefixUmbrellaRWLock( hash, 't' ).read:
+
+ path = self._GenerateExpectedThumbnailPath( hash )
+
+ thumb_missing = not os.path.exists( path )
+
if thumb_missing:
@@ -1692,7 +1741,7 @@ def Rebalance( self, job_status ):
return
- with self._file_storage_rwlock.write:
+ with self._master_locations_rwlock.write:
rebalance_tuple = self._GetRebalanceTuple()
@@ -1732,7 +1781,7 @@ def Rebalance( self, job_status ):
def RebalanceWorkToDo( self ):
- with self._file_storage_rwlock.read:
+ with self._master_locations_rwlock.read:
return self._GetRebalanceTuple() is not None
@@ -1748,24 +1797,29 @@ def RegenerateThumbnail( self, media ):
return
- with self._file_storage_rwlock.read:
+ with self._master_locations_rwlock.read:
- file_path = self._GenerateExpectedFilePath( hash, mime )
-
- if not os.path.exists( file_path ):
+ with self._GetPrefixUmbrellaRWLock( hash, 'f' ).read:
- raise HydrusExceptions.FileMissingException( 'The thumbnail for file ' + hash.hex() + ' could not be regenerated from the original file because the original file is missing! This event could indicate hard drive corruption. Please check everything is ok.')
+ file_path = self._GenerateExpectedFilePath( hash, mime )
+
+ if not os.path.exists( file_path ):
+
+ raise HydrusExceptions.FileMissingException( 'The thumbnail for file ' + hash.hex() + ' could not be regenerated from the original file because the original file is missing! This event could indicate hard drive corruption. Please check everything is ok.')
+
+ # in another world I do this inside the file read lock, but screw it I'd rather have the time spent outside
thumbnail_bytes = self._GenerateThumbnailBytes( file_path, media )
-
- with self._file_storage_rwlock.write:
-
- self._AddThumbnailFromBytes( hash, thumbnail_bytes )
+ with self._GetPrefixUmbrellaRWLock( hash, 't' ).write:
+
+ self._AddThumbnailFromBytes( hash, thumbnail_bytes )
+
+
return True
-
+
def RegenerateThumbnailIfWrongSize( self, media ):
@@ -1831,27 +1885,30 @@ def UpdateFileModifiedTimestampMS( self, media, modified_timestamp_ms: int ):
hash = media.GetHash()
mime = media.GetMime()
- path = self._GenerateExpectedFilePath( hash, mime )
-
- with self._file_storage_rwlock.write:
+ with self._master_locations_rwlock.read:
- if os.path.exists( path ):
-
- existing_access_time = os.path.getatime( path )
- existing_modified_time = os.path.getmtime( path )
+ with self._GetPrefixUmbrellaRWLock( hash, 'f' ).write:
- # floats are ok here!
- modified_timestamp = modified_timestamp_ms / 1000
+ path = self._GenerateExpectedFilePath( hash, mime )
- try:
-
- os.utime( path, ( existing_access_time, modified_timestamp ) )
+ if os.path.exists( path ):
- HydrusData.Print( 'Successfully changed modified time of "{}" from {} to {}.'.format( path, HydrusTime.TimestampToPrettyTime( int( existing_modified_time ) ), HydrusTime.TimestampToPrettyTime( int( modified_timestamp ) ) ))
+ existing_access_time = os.path.getatime( path )
+ existing_modified_time = os.path.getmtime( path )
- except PermissionError:
+ # floats are ok here!
+ modified_timestamp = modified_timestamp_ms / 1000
- HydrusData.Print( 'Tried to set modified time of {} to file "{}", but did not have permission!'.format( HydrusTime.TimestampToPrettyTime( int( modified_timestamp ) ), path ) )
+ try:
+
+ os.utime( path, ( existing_access_time, modified_timestamp ) )
+
+ HydrusData.Print( 'Successfully changed modified time of "{}" from {} to {}.'.format( path, HydrusTime.TimestampToPrettyTime( int( existing_modified_time ) ), HydrusTime.TimestampToPrettyTime( int( modified_timestamp ) ) ))
+
+ except PermissionError:
+
+ HydrusData.Print( 'Tried to set modified time of {} to file "{}", but did not have permission!'.format( HydrusTime.TimestampToPrettyTime( int( modified_timestamp ) ), path ) )
+
diff --git a/hydrus/client/ClientFilesPhysical.py b/hydrus/client/ClientFilesPhysical.py
index f636bfb42..f1dfd5a2f 100644
--- a/hydrus/client/ClientFilesPhysical.py
+++ b/hydrus/client/ClientFilesPhysical.py
@@ -5,6 +5,8 @@
from hydrus.core import HydrusPaths
from hydrus.core import HydrusExceptions
+from hydrus.client import ClientThreading
+
def CheckFullPrefixCoverage( merge_target, prefixes ):
missing_prefixes = GetMissingPrefixes( merge_target, prefixes )
@@ -294,6 +296,7 @@ def __init__( self, prefix: str, base_location: FilesStorageBaseLocation, purge:
our_subfolders[0] = first_char + our_subfolders[0]
self.path = os.path.join( self.base_location.path, *our_subfolders )
+ self.rwlock = ClientThreading.FileRWLock()
def __repr__( self ):
@@ -331,6 +334,16 @@ def IsForFiles( self ):
return self.prefix[0] == 'f'
+ def IterateAllFiles( self ):
+
+ filenames = list( os.listdir( self.path ) )
+
+ for filename in filenames:
+
+ yield os.path.join( self.path, filename )
+
+
+
def MakeSureExists( self ):
HydrusPaths.MakeSureDirectoryExists( self.path )
diff --git a/hydrus/client/ClientServices.py b/hydrus/client/ClientServices.py
index f8c437d2b..f280cd316 100644
--- a/hydrus/client/ClientServices.py
+++ b/hydrus/client/ClientServices.py
@@ -1193,7 +1193,7 @@ def PausePlayNetworkSync( self ):
- def Request( self, method, command, request_args = None, request_headers = None, report_hooks = None, temp_path = None ):
+ def Request( self, method, command, request_args = None, request_headers = None, report_hooks = None, temp_path = None, file_body_path = None ):
if request_args is None: request_args = {}
if request_headers is None: request_headers = {}
@@ -1201,6 +1201,9 @@ def Request( self, method, command, request_args = None, request_headers = None,
try:
+ query = ''
+ body = ''
+
if method == HC.GET:
query = HydrusNetworkVariableHandling.DumpToGETQuery( request_args )
@@ -1217,10 +1220,6 @@ def Request( self, method, command, request_args = None, request_headers = None,
content_type = HC.APPLICATION_OCTET_STREAM
- body = request_args[ 'file' ]
-
- del request_args[ 'file' ]
-
else:
content_type = HC.APPLICATION_JSON
@@ -1249,7 +1248,7 @@ def Request( self, method, command, request_args = None, request_headers = None,
method = 'POST'
- network_job = ClientNetworkingJobs.NetworkJobHydrus( self._service_key, method, url, body = body, temp_path = temp_path )
+ network_job = ClientNetworkingJobs.NetworkJobHydrus( self._service_key, method, url, body = body, temp_path = temp_path, file_body_path = file_body_path )
if command not in ( 'update', 'metadata', 'file', 'thumbnail' ):
diff --git a/hydrus/client/db/ClientDB.py b/hydrus/client/db/ClientDB.py
index 16ae59afc..cb7caf1ff 100644
--- a/hydrus/client/db/ClientDB.py
+++ b/hydrus/client/db/ClientDB.py
@@ -10372,6 +10372,37 @@ def ask_what_to_do_zip_docx_scan():
+ if version == 575:
+
+ try:
+
+ domain_manager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
+
+ domain_manager.Initialise()
+
+ domain_manager.OverwriteDefaultParsers( [
+ 'danbooru file page parser - get webm ugoira',
+ 'danbooru file page parser'
+ ] )
+
+ #
+
+ domain_manager.TryToLinkURLClassesAndParsers()
+
+ #
+
+ self.modules_serialisable.SetJSONDump( domain_manager )
+
+ except Exception as e:
+
+ HydrusData.PrintException( e )
+
+ message = 'Trying to update some downloaders failed! Please let hydrus dev know!'
+
+ self.pub_initial_message( message )
+
+
+
self._controller.frame_splash_status.SetTitleText( 'updated db to v{}'.format( HydrusData.ToHumanInt( version + 1 ) ) )
self._Execute( 'UPDATE version SET version = ?;', ( version + 1, ) )
diff --git a/hydrus/client/duplicates/ClientAutoDuplicates.py b/hydrus/client/duplicates/ClientAutoDuplicates.py
index 3cb219347..83d2353d5 100644
--- a/hydrus/client/duplicates/ClientAutoDuplicates.py
+++ b/hydrus/client/duplicates/ClientAutoDuplicates.py
@@ -138,7 +138,7 @@ def GetMatchingMedia( self, media_result_1, media_result_2 ):
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_AUTO_DUPLICATES_PAIR_SELECTOR_AND_COMPARATOR ] = PairSelectorAndComparator
-class AutoDuplicatesRule( HydrusSerialisable.SerialisableBaseNamed ):
+class DuplicatesAutoResolutionRule( HydrusSerialisable.SerialisableBaseNamed ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_AUTO_DUPLICATES_RULE
SERIALISABLE_NAME = 'Auto-Duplicates Rule'
@@ -171,15 +171,15 @@ def __init__( self, name ):
# 'here's a pair of media results, pass/fail?'
-HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_AUTO_DUPLICATES_RULE ] = AutoDuplicatesRule
+HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_AUTO_DUPLICATES_RULE ] = DuplicatesAutoResolutionRule
-class AutoDuplicatesManager( object ):
+class DuplicatesAutoResolutionManager( object ):
my_instance = None
def __init__( self ):
- AutoDuplicatesManager.my_instance = self
+ DuplicatesAutoResolutionManager.my_instance = self
# my rules, start with empty and then load from db or whatever on controller init
@@ -187,13 +187,13 @@ def __init__( self ):
@staticmethod
- def instance() -> 'AutoDuplicatesManager':
+ def instance() -> 'DuplicatesAutoResolutionManager':
- if AutoDuplicatesManager.my_instance is None:
+ if DuplicatesAutoResolutionManager.my_instance is None:
- AutoDuplicatesManager()
+ DuplicatesAutoResolutionManager()
- return AutoDuplicatesManager.my_instance
+ return DuplicatesAutoResolutionManager.my_instance
diff --git a/hydrus/client/gui/ClientGUI.py b/hydrus/client/gui/ClientGUI.py
index 1a8d5b272..9d337979e 100644
--- a/hydrus/client/gui/ClientGUI.py
+++ b/hydrus/client/gui/ClientGUI.py
@@ -306,12 +306,7 @@ def THREADUploadPending( service_key ):
path = client_files_manager.GetFilePath( hash, mime )
- with open( path, 'rb' ) as f:
-
- file_bytes = f.read()
-
-
- service.Request( HC.POST, 'file', { 'file' : file_bytes } )
+ service.Request( HC.POST, 'file', file_body_path = path )
file_info_manager = media_result.GetFileInfoManager()
@@ -863,10 +858,10 @@ def _AboutWindow( self ):
library_version_lines.append( 'html5lib present: {}'.format( ClientParsing.HTML5LIB_IS_OK ) )
library_version_lines.append( 'lxml present: {}'.format( ClientParsing.LXML_IS_OK ) )
library_version_lines.append( 'lz4 present: {}'.format( HydrusCompression.LZ4_OK ) )
+ library_version_lines.append( 'olefile present: {}'.format( HydrusOLEHandling.OLEFILE_OK ) )
library_version_lines.append( 'pympler present: {}'.format( HydrusMemory.PYMPLER_OK ) )
library_version_lines.append( 'pyopenssl present: {}'.format( HydrusEncryption.OPENSSL_OK ) )
library_version_lines.append( 'psd_tools present: {}'.format( HydrusPSDHandling.PSD_TOOLS_OK ) )
- library_version_lines.append( 'olefile present: {}'.format( HydrusOLEHandling.OLEFILE_OK ) )
library_version_lines.append( 'speedcopy (experimental test) present: {}'.format( HydrusFileHandling.SPEEDCOPY_OK ) )
library_version_lines.append( 'install dir: {}'.format( HC.BASE_DIR ) )
library_version_lines.append( 'db dir: {}'.format( CG.client_controller.db_dir ) )
@@ -1198,11 +1193,11 @@ def _ClearFileViewingStats( self ):
def _ClearOrphanFiles( self ):
- text = 'This job will iterate through every file in your database\'s file storage, extracting any it does not expect to be there. This is particularly useful for \'re-syncing\' your file storage to what it should be, and is particularly useful if you are marrying an older/newer database with a newer/older file storage.'
+ text = 'This job will iterate through every file in your database\'s file storage, extracting any it does not expect to be there. This is particularly useful for \'re-syncing\' your file storage to what it should be after, say, marrying an older/newer database with a newer/older file storage.'
text += '\n' * 2
- text += 'You can choose to move the orphans in your file directories somewhere or delete them. Orphans in your thumbnail directories will always be deleted.'
+ text += 'You can choose to move the orphans in your file directories somewhere or delete them. Orphan thumbnails will be put in a subdirectory, in case you wish to perform reverse lookups.'
text += '\n' * 2
- text += 'Files and thumbnails will be inaccessible while this runs, so it is best to leave the client alone until it is done. It may take some time.'
+ text += 'Access to files and thumbnails will be slightly limited while this runs, and it may take some time.'
yes_tuples = []
@@ -3461,8 +3456,8 @@ def flip_macos_antiflicker():
ClientGUIMenus.AppendMenuItem( gui_actions, 'make some popups', 'Throw some varied popups at the message manager, just to check it is working.', self._DebugMakeSomePopups )
ClientGUIMenus.AppendMenuItem( gui_actions, 'publish some sub files in five seconds', 'Publish some files like a subscription would.', self._controller.CallLater, 5, lambda: CG.client_controller.pub( 'imported_files_to_page', [ HydrusData.GenerateKey() for i in range( 5 ) ], 'example sub files' ) )
ClientGUIMenus.AppendMenuItem( gui_actions, 'refresh pages menu in five seconds', 'Delayed refresh the pages menu, giving you time to minimise or otherwise alter the client before it arrives.', self._controller.CallLater, 5, self._menu_updater_pages.update )
- ClientGUIMenus.AppendMenuItem( gui_actions, 'reload current g ui session', 'Reload the current QSS stylesheet.', self._ReloadCurrentGUISession )
- ClientGUIMenus.AppendMenuItem( gui_actions, 'reload current stylesheet', 'Reload the current QSS stylesheet.', ClientGUIStyle.ReloadStyleSheet )
+ ClientGUIMenus.AppendMenuItem( gui_actions, 'reload current gui session', 'Save, clear, and then reload the current GUI Session. Might help with some forced style reloading.', self._ReloadCurrentGUISession )
+ ClientGUIMenus.AppendMenuItem( gui_actions, 'reload current stylesheet', 'Reload the current QSS stylesheet. Helps if you just edited it on disk and do not want to restart.', ClientGUIStyle.ReloadStyleSheet )
ClientGUIMenus.AppendMenuItem( gui_actions, 'reset multi-column list settings to default', 'Reset all multi-column list widths and other display settings to default.', self._DebugResetColumnListManager )
ClientGUIMenus.AppendMenuItem( gui_actions, 'save \'last session\' gui session', 'Make an immediate save of the \'last session\' gui session. Mostly for testing crashes, where last session is not saved correctly.', self.ProposeSaveGUISession, CC.LAST_SESSION_SESSION_NAME )
@@ -6264,7 +6259,7 @@ def qt_test_ac():
CG.client_controller.CallLaterQtSafe( self, t, 'test job', uias.Char, ac_widget, QC.Qt.Key_Return )
- for i in range( 16 ):
+ for i in range( 20 ):
t += SYS_PRED_REFRESH
diff --git a/hydrus/client/gui/ClientGUIDownloaders.py b/hydrus/client/gui/ClientGUIDownloaders.py
index e9a000797..e11f55674 100644
--- a/hydrus/client/gui/ClientGUIDownloaders.py
+++ b/hydrus/client/gui/ClientGUIDownloaders.py
@@ -1340,6 +1340,8 @@ def __init__( self, parent: QW.QWidget, url_class: ClientNetworkingURLClass.URLC
tt += 'For instance, if this url class has domain \'example.com\', should it match a url with \'boards.example.com\' or \'artistname.example.com\'?'
tt += '\n' * 2
tt += 'Any subdomain starting with \'www\' is automatically matched, so do not worry about having to account for that.'
+ tt += '\n' * 2
+ tt += 'Also, if you have \'example.com\' here, but another URL class exists for \'api.example.com\', if an URL comes in with a domain of \'api.example.com\', that more specific URL Class one will always be tested before this one.'
self._match_subdomains.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py b/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py
index 8908528b5..74ab97fb0 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py
@@ -583,7 +583,7 @@ def __init__( self, parent: QW.QWidget, media, default_reason, suggested_file_se
self._InitialisePermittedActionChoices()
- self._action_radio = ClientGUICommon.BetterRadioBox( self, choices = self._permitted_action_choices, vertical = True )
+ self._action_radio = ClientGUICommon.BetterRadioBox( self, self._permitted_action_choices, vertical = True )
self._action_radio.Select( 0 )
@@ -673,7 +673,7 @@ def __init__( self, parent: QW.QWidget, media, default_reason, suggested_file_se
selection_index = len( permitted_reason_choices ) - 1
- self._reason_radio = ClientGUICommon.BetterRadioBox( self._reason_panel, choices = permitted_reason_choices, vertical = True )
+ self._reason_radio = ClientGUICommon.BetterRadioBox( self._reason_panel, permitted_reason_choices, vertical = True )
self._custom_reason = QW.QLineEdit( self._reason_panel )
@@ -1188,7 +1188,7 @@ def GetValue( self ):
if reasons_ok and not user_selected_existing_or_make_no_change:
- if self._reason_radio.GetCurrentIndex() <= 0:
+ if reason == self._default_reason:
last_advanced_file_deletion_reason = None
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
index d40aa2e71..44451a3fc 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
@@ -1584,7 +1584,7 @@ def __init__( self, parent, new_options ):
self._sessions_panel = ClientGUICommon.StaticBox( self, 'sessions' )
- self._default_gui_session = QW.QComboBox( self._sessions_panel )
+ self._default_gui_session = ClientGUICommon.BetterChoice( self._sessions_panel )
self._last_session_save_period_minutes = ClientGUICommon.BetterSpinBox( self._sessions_panel, min = 1, max = 1440 )
@@ -1679,14 +1679,7 @@ def __init__( self, parent, new_options ):
self._default_gui_session.addItem( name, name )
- try:
-
- QP.SetStringSelection( self._default_gui_session, HC.options['default_gui_session'] )
-
- except:
-
- self._default_gui_session.setCurrentIndex( 0 )
-
+ self._default_gui_session.SetValue( HC.options['default_gui_session'] )
self._last_session_save_period_minutes.setValue( self._new_options.GetInteger( 'last_session_save_period_minutes' ) )
@@ -1801,7 +1794,7 @@ def __init__( self, parent, new_options ):
def UpdateOptions( self ):
- HC.options[ 'default_gui_session' ] = self._default_gui_session.currentText()
+ HC.options[ 'default_gui_session' ] = self._default_gui_session.GetValue()
self._new_options.SetInteger( 'notebook_tab_alignment', self._notebook_tab_alignment.GetValue() )
diff --git a/hydrus/client/gui/ClientGUISubscriptions.py b/hydrus/client/gui/ClientGUISubscriptions.py
index 9836238ff..993c83be6 100644
--- a/hydrus/client/gui/ClientGUISubscriptions.py
+++ b/hydrus/client/gui/ClientGUISubscriptions.py
@@ -233,7 +233,7 @@ def __init__( self, parent: QW.QWidget, subscription: ClientImportSubscriptions.
self._initial_file_limit.setToolTip( ClientGUIFunctions.WrapToolTip( 'The first sync will add no more than this many URLs.' ) )
self._periodic_file_limit = ClientGUICommon.BetterSpinBox( self._file_limits_panel, min=1, max=limits_max )
- self._periodic_file_limit.setToolTip( ClientGUIFunctions.WrapToolTip( 'Normal syncs will add no more than this many URLs, stopping early if they find several URLs the query has seen before.' ) )
+ self._periodic_file_limit.setToolTip( ClientGUIFunctions.WrapToolTip( 'Normal syncs will add no more than this many URLs, stopping early if they find several URLs the query has seen before. Note that this generally means top-level posts. If those posts include multiple files, e.g. as on Pixiv, they will still only count for one URL at the stage when this is checked.' ) )
self._this_is_a_random_sample_sub = QW.QCheckBox( self._file_limits_panel )
self._this_is_a_random_sample_sub.setToolTip( ClientGUIFunctions.WrapToolTip( 'If you check this, you will not get warnings if the normal file limit is hit. Useful if you have a randomly sorted gallery, or you just want a recurring small sample of files.' ) )
@@ -309,8 +309,8 @@ def __init__( self, parent: QW.QWidget, subscription: ClientImportSubscriptions.
rows = []
- rows.append( ( 'on first check, get at most this many files: ', self._initial_file_limit ) )
- rows.append( ( 'on normal checks, get at most this many newer files: ', self._periodic_file_limit ) )
+ rows.append( ( 'on first check, get at most this many urls/posts: ', self._initial_file_limit ) )
+ rows.append( ( 'on normal checks, get at most this many newer urls/posts: ', self._periodic_file_limit ) )
rows.append( ( 'do not worry about subscription gaps: ', self._this_is_a_random_sample_sub ) )
gridbox = ClientGUICommon.WrapInGrid( self._file_limits_panel, rows )
diff --git a/hydrus/client/gui/QtPorting.py b/hydrus/client/gui/QtPorting.py
index 0d0920e44..0eb230fee 100644
--- a/hydrus/client/gui/QtPorting.py
+++ b/hydrus/client/gui/QtPorting.py
@@ -1424,17 +1424,6 @@ def ListWidgetGetSelection( widget ):
return -1
-def ListWidgetGetStrings( widget ):
-
- strings = []
-
- for i in range( widget.count() ):
-
- strings.append( widget.item( i ).text() )
-
- return strings
-
-
def ListWidgetSetSelection( widget, idxs ):
widget.clearSelection()
@@ -1497,14 +1486,6 @@ def SetBackgroundColour( widget, colour ):
widget.setStyleSheet( '#{} {{ background-color: {} }}'.format( object_name, QG.QColor( colour ).name() ) )
-def SetStringSelection( combobox, string ):
-
- index = combobox.findText( string )
-
- if index != -1:
-
- combobox.setCurrentIndex( index )
-
def SetMinClientSize( widget, size ):
@@ -1563,6 +1544,7 @@ def SetStatusText( self, text, index, tooltip = None ):
+
class UIActionSimulator:
def __init__( self ):
@@ -1583,210 +1565,6 @@ def Char( self, widget, key, text = None ):
QW.QApplication.instance().postEvent( widget, ev1 )
QW.QApplication.instance().postEvent( widget, ev2 )
-
-class RadioBox( QW.QFrame ):
-
- radioBoxChanged = QC.Signal()
-
- def __init__( self, parent, choices, vertical = False ):
-
- QW.QFrame.__init__( self, parent )
-
- self.setFrameStyle( QW.QFrame.Box | QW.QFrame.Raised )
-
- if vertical:
-
- self.setLayout( VBoxLayout() )
-
- else:
-
- self.setLayout( HBoxLayout() )
-
-
- self._choices = []
-
- for choice in choices:
-
- radiobutton = QW.QRadioButton( choice, self )
-
- self._choices.append( radiobutton )
-
- radiobutton.clicked.connect( self.radioBoxChanged )
-
- self.layout().addWidget( radiobutton )
-
-
- if vertical and len( self._choices ):
-
- self._choices[0].setChecked( True )
-
- elif len( self._choices ):
-
- self._choices[-1].setChecked( True )
-
-
-
- def _GetCurrentChoiceWidget( self ):
-
- for choice in self._choices:
-
- if choice.isChecked():
-
- return choice
-
-
-
- return None
-
-
- def GetCurrentIndex( self ):
-
- for i in range( len( self._choices ) ):
-
- if self._choices[ i ].isChecked(): return i
-
-
- return -1
-
-
- def SetStringSelection( self, str ):
-
- for i in range( len( self._choices ) ):
-
- if self._choices[ i ].text() == str:
-
- self._choices[ i ].setChecked( True )
-
- return
-
-
-
-
- def GetStringSelection( self ):
-
- for i in range( len( self._choices ) ):
-
- if self._choices[ i ].isChecked(): return self._choices[ i ].text()
-
- return None
-
- def setFocus( self, reason ):
-
- item = self._GetCurrentChoiceWidget()
-
- if item is not None:
-
- item.setFocus( reason )
-
- else:
-
- QW.QFrame.setFocus( self, reason )
-
-
-
- def SetValue( self, data ):
-
- pass
-
-
- def Select( self, idx ):
-
- self._choices[ idx ].setChecked( True )
-
-
-class DataRadioBox( QW.QFrame ):
-
- radioBoxChanged = QC.Signal()
-
- def __init__( self, parent, choice_tuples, vertical = False ):
-
- QW.QFrame.__init__( self, parent )
-
- self.setFrameStyle( QW.QFrame.Box | QW.QFrame.Raised )
-
- if vertical:
-
- self.setLayout( VBoxLayout() )
-
- else:
-
- self.setLayout( HBoxLayout() )
-
-
- self._choices = []
- self._buttons_to_data = {}
-
- for ( text, data ) in choice_tuples:
-
- radiobutton = QW.QRadioButton( text, self )
-
- self._choices.append( radiobutton )
-
- self._buttons_to_data[ radiobutton ] = data
-
- radiobutton.clicked.connect( self.radioBoxChanged )
-
- self.layout().addWidget( radiobutton )
-
-
- if vertical and len( self._choices ):
-
- self._choices[0].setChecked( True )
-
- elif len( self._choices ) > 0:
-
- self._choices[-1].setChecked( True )
-
-
-
- def _GetCurrentChoiceWidget( self ):
-
- for choice in self._choices:
-
- if choice.isChecked():
-
- return choice
-
-
-
- return None
-
-
- def GetValue( self ):
-
- for ( button, data ) in self._buttons_to_data.items():
-
- if button.isChecked():
-
- return data
-
-
-
- raise Exception( 'No button selected!' )
-
-
- def setFocus( self, reason ):
-
- for button in self._choices:
-
- if button.isChecked():
-
- button.setFocus( reason )
-
- return
-
-
-
- QW.QFrame.setFocus( self, reason )
-
-
- def SetValue( self, select_data ):
-
- for ( button, data ) in self._buttons_to_data.items():
-
- button.setChecked( data == select_data )
-
-
# Adapted from https://doc.qt.io/qt-5/qtwidgets-widgets-elidedlabel-example.html
diff --git a/hydrus/client/gui/canvas/ClientGUIMPV.py b/hydrus/client/gui/canvas/ClientGUIMPV.py
index e4f68365a..4975094fa 100644
--- a/hydrus/client/gui/canvas/ClientGUIMPV.py
+++ b/hydrus/client/gui/canvas/ClientGUIMPV.py
@@ -16,6 +16,7 @@
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientThreading
+from hydrus.client.gui import ClientGUIAsync
from hydrus.client.gui import ClientGUIDialogsMessage
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import QtPorting as QP
@@ -916,52 +917,77 @@ def SetMedia( self, media: typing.Optional[ ClientMedia.MediaSingleton ], start_
else:
- self._have_shown_human_error_on_this_file = False
+ media = self._media
- hash = self._media.GetHash()
- mime = self._media.GetMime()
-
- # some videos have an audio channel that is silent. hydrus thinks these dudes are 'no audio', but when we throw them at mpv, it may play audio for them
- # would be fine, you think, except in one reported case this causes scratches and pops and hell whitenoise
- # so let's see what happens here
- mute_override = not self._media.HasAudio()
-
- client_files_manager = CG.client_controller.client_files_manager
-
- path = client_files_manager.GetFilePath( hash, mime )
-
- self._player.visibility = 'always'
-
- self._stop_for_slideshow = False
-
- self._player.pause = True
+ def work_callable():
+
+ hash = media.GetHash()
+ mime = media.GetMime()
+
+ path = CG.client_controller.client_files_manager.GetFilePath( hash, mime )
+
+ return ( media, path )
+
- if mime in HC.VIEWABLE_ANIMATIONS and not CG.client_controller.new_options.GetBoolean( 'always_loop_gifs' ):
+ def publish_callable( media_and_path ):
+
+ ( media, path ) = media_and_path
- if mime == HC.ANIMATION_GIF:
+ if media != self._media:
- self._times_to_play_animation = HydrusAnimationHandling.GetTimesToPlayPILAnimation( path )
+ return
- elif mime == HC.ANIMATION_APNG:
+
+ self._have_shown_human_error_on_this_file = False
+
+ # some videos have an audio channel that is silent. hydrus thinks these dudes are 'no audio', but when we throw them at mpv, it may play audio for them
+ # would be fine, you think, except in one reported case this causes scratches and pops and hell whitenoise
+ # so let's see what happens here
+ mute_override = not self._media.HasAudio()
+
+ self._player.visibility = 'always'
+
+ self._stop_for_slideshow = False
+
+ self._player.pause = True
+
+ mime = self._media.GetMime()
+
+ if mime in HC.VIEWABLE_ANIMATIONS and not CG.client_controller.new_options.GetBoolean( 'always_loop_gifs' ):
- self._times_to_play_animation = HydrusAnimationHandling.GetTimesToPlayAPNG( path )
+ if mime == HC.ANIMATION_GIF:
+
+ self._times_to_play_animation = HydrusAnimationHandling.GetTimesToPlayPILAnimation( path )
+
+ elif mime == HC.ANIMATION_APNG:
+
+ self._times_to_play_animation = HydrusAnimationHandling.GetTimesToPlayAPNG( path )
+
-
- try:
+ try:
+
+ self._player.loadfile( path )
+
+ except Exception as e:
+
+ HydrusData.ShowException( e )
+
- self._player.loadfile( path )
+ self._player.volume = ClientGUIMediaVolume.GetCorrectCurrentVolume( self._canvas_type )
+ self._player.mute = mute_override or ClientGUIMediaVolume.GetCorrectCurrentMute( self._canvas_type )
+ self._player.pause = start_paused
- except Exception as e:
+
+ def errback_ui_cleanup_callable():
- HydrusData.ShowException( e )
+ self.SetMedia( None )
- self._player.volume = ClientGUIMediaVolume.GetCorrectCurrentVolume( self._canvas_type )
- self._player.mute = mute_override or ClientGUIMediaVolume.GetCorrectCurrentMute( self._canvas_type )
- self._player.pause = start_paused
+ job = ClientGUIAsync.AsyncQtJob( self, work_callable, publish_callable, errback_ui_cleanup_callable = errback_ui_cleanup_callable )
+
+ job.start()
-
except mpv.ShutdownError:
diff --git a/hydrus/client/gui/importing/ClientGUIImport.py b/hydrus/client/gui/importing/ClientGUIImport.py
index 0c0ba5af8..b21dfe736 100644
--- a/hydrus/client/gui/importing/ClientGUIImport.py
+++ b/hydrus/client/gui/importing/ClientGUIImport.py
@@ -247,7 +247,7 @@ def __init__( self, parent, service_key, filename_tagging_options, present_for_a
for regex in regexes:
- self._regexes.addItem( regex )
+ self._regexes.Append( regex, regex )
#
@@ -375,7 +375,7 @@ def AddRegex( self ):
return
- self._regexes.addItem( regex )
+ self._regexes.Append( regex, regex )
self._regex_box.clear()
@@ -385,16 +385,13 @@ def AddRegex( self ):
def EventRemoveRegex( self, item ):
- selection = QP.ListWidgetGetSelection( self._regexes )
-
- if selection != -1:
+ if self._regexes.GetNumSelected() > 0:
- if len( self._regex_box.text() ) == 0:
-
- self._regex_box.setText( self._regexes.item( selection ).text() )
-
+ selected = list( self._regexes.GetData( only_selected = True ) )
+
+ self._regex_box.setText( selected[0] )
- QP.ListWidgetDelete( self._regexes, selection )
+ self._regexes.DeleteSelected()
self.tagsChanged.emit()
@@ -423,7 +420,7 @@ def UpdateFilenameTaggingOptions( self, filename_tagging_options ):
quick_namespaces = self._quick_namespaces_list.GetData()
- regexes = QP.ListWidgetGetStrings( self._regexes )
+ regexes = self._regexes.GetData()
filename_tagging_options.AdvancedSetTuple( quick_namespaces, regexes )
@@ -1245,6 +1242,8 @@ def ScheduleRefreshTags( self ):
class GalleryImportPanel( ClientGUICommon.StaticBox ):
+ importOptionsChanged = QC.Signal()
+
def __init__( self, parent, page_key, name = 'gallery query' ):
ClientGUICommon.StaticBox.__init__( self, parent, name )
@@ -1333,6 +1332,9 @@ def __init__( self, parent, page_key, name = 'gallery query' ):
self._import_options_button.noteImportOptionsChanged.connect( self._SetNoteImportOptions )
self._import_options_button.tagImportOptionsChanged.connect( self._SetTagImportOptions )
+ self._file_limit.valueChanged.connect( self.importOptionsChanged )
+ self._import_options_button.importOptionsChanged.connect( self.importOptionsChanged )
+
self._UpdateControlsForNewGalleryImport()
CG.client_controller.gui.RegisterUIUpdateWindow( self )
@@ -1666,6 +1668,8 @@ def SetValue( self, gug_key_and_name ):
class WatcherReviewPanel( ClientGUICommon.StaticBox ):
+ importOptionsChanged = QC.Signal()
+
def __init__( self, parent, page_key, name = 'watcher' ):
ClientGUICommon.StaticBox.__init__( self, parent, name )
@@ -1779,6 +1783,9 @@ def __init__( self, parent, page_key, name = 'watcher' ):
self._checker_options_button.valueChanged.connect( self._SetCheckerOptions )
+ self._checker_options_button.valueChanged.connect( self.importOptionsChanged )
+ self._import_options_button.importOptionsChanged.connect( self.importOptionsChanged )
+
self._UpdateControlsForNewWatcher()
CG.client_controller.gui.RegisterUIUpdateWindow( self )
diff --git a/hydrus/client/gui/importing/ClientGUIImportOptions.py b/hydrus/client/gui/importing/ClientGUIImportOptions.py
index 59bfdf83c..f0b7c053f 100644
--- a/hydrus/client/gui/importing/ClientGUIImportOptions.py
+++ b/hydrus/client/gui/importing/ClientGUIImportOptions.py
@@ -1730,6 +1730,8 @@ def _UpdateTagImportOptionsTabName( self, is_default ):
def SetFileImportOptions( self, file_import_options: FileImportOptions.FileImportOptions ):
+ file_import_options = file_import_options.Duplicate()
+
if self._file_import_options_panel is not None:
raise Exception( 'This Import Options Panel already has File Import Options set!' )
@@ -1746,6 +1748,8 @@ def SetFileImportOptions( self, file_import_options: FileImportOptions.FileImpor
def SetNoteImportOptions( self, note_import_options: NoteImportOptions.NoteImportOptions ):
+ note_import_options = note_import_options.Duplicate()
+
if self._note_import_options_panel is not None:
raise Exception( 'This Import Options Panel already has Note Import Options set!' )
@@ -1762,6 +1766,8 @@ def SetNoteImportOptions( self, note_import_options: NoteImportOptions.NoteImpor
def SetTagImportOptions( self, tag_import_options: TagImportOptions.TagImportOptions ):
+ tag_import_options = tag_import_options.Duplicate()
+
if self._tag_import_options_panel is not None:
raise Exception( 'This Import Options Panel already has File Import Options set!' )
@@ -1779,6 +1785,7 @@ def SetTagImportOptions( self, tag_import_options: TagImportOptions.TagImportOpt
class ImportOptionsButton( ClientGUICommon.ButtonWithMenuArrow ):
+ importOptionsChanged = QC.Signal()
fileImportOptionsChanged = QC.Signal( FileImportOptions.FileImportOptions )
noteImportOptionsChanged = QC.Signal( NoteImportOptions.NoteImportOptions )
tagImportOptionsChanged = QC.Signal( TagImportOptions.TagImportOptions )
@@ -1803,6 +1810,10 @@ def __init__( self, parent, show_downloader_options: bool, allow_default_selecti
self._SetLabelAndToolTip()
+ self.fileImportOptionsChanged.connect( self.importOptionsChanged )
+ self.noteImportOptionsChanged.connect( self.importOptionsChanged )
+ self.tagImportOptionsChanged.connect( self.importOptionsChanged )
+
def _CopyFileImportOptions( self ):
@@ -2113,8 +2124,7 @@ def _SetFileImportOptionsDefault( self ):
return
- file_import_options = self._file_import_options.Duplicate()
-
+ file_import_options = FileImportOptions.FileImportOptions()
file_import_options.SetIsDefault( True )
self._SetFileImportOptions( file_import_options )
@@ -2127,8 +2137,7 @@ def _SetNoteImportOptionsDefault( self ):
return
- note_import_options = self._note_import_options.Duplicate()
-
+ note_import_options = NoteImportOptions.NoteImportOptions()
note_import_options.SetIsDefault( True )
self._SetNoteImportOptions( note_import_options )
@@ -2141,9 +2150,7 @@ def _SetTagImportOptionsDefault( self ):
return
- tag_import_options = self._tag_import_options.Duplicate()
-
- tag_import_options.SetIsDefault( True )
+ tag_import_options = TagImportOptions.TagImportOptions( is_default = True )
self._SetTagImportOptions( tag_import_options )
diff --git a/hydrus/client/gui/lists/ClientGUIListBook.py b/hydrus/client/gui/lists/ClientGUIListBook.py
index ad1eed79f..5d2304d2a 100644
--- a/hydrus/client/gui/lists/ClientGUIListBook.py
+++ b/hydrus/client/gui/lists/ClientGUIListBook.py
@@ -1,27 +1,11 @@
-import collections.abc
-import os
-import re
-import typing
-
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
-from qtpy import QtGui as QG
-from hydrus.core import HydrusConstants as HC
-from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
-from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
-from hydrus.client import ClientGlobals as CG
-from hydrus.client import ClientPaths
-from hydrus.client.gui import ClientGUICore as CGC
-from hydrus.client.gui import ClientGUIFunctions
-from hydrus.client.gui import ClientGUIMenus
-from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListBoxes
-from hydrus.client.networking import ClientNetworkingFunctions
class ListBook( QW.QWidget ):
@@ -30,7 +14,6 @@ def __init__( self, *args, **kwargs ):
QW.QWidget.__init__( self, *args, **kwargs )
self._keys_to_active_pages = {}
- self._keys_to_proto_pages = {}
self._list_box = ClientGUIListBoxes.BetterQListWidget( self )
self._list_box.setSelectionMode( QW.QListWidget.SingleSelection )
@@ -50,26 +33,11 @@ def __init__( self, *args, **kwargs ):
QP.AddToLayout( hbox, self._list_box, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( hbox, self._panel_sizer, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
- self._list_box.itemSelectionChanged.connect( self.EventSelection )
+ self._list_box.itemSelectionChanged.connect( self._ListBoxSelected )
self.setLayout( hbox )
- def _ActivatePage( self, key ):
-
- ( classname, args, kwargs ) = self._keys_to_proto_pages[ key ]
-
- page = classname( *args, **kwargs )
-
- page.setVisible( False )
-
- QP.AddToLayout( self._panel_sizer, page, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
-
- self._keys_to_active_pages[ key ] = page
-
- del self._keys_to_proto_pages[ key ]
-
-
def _GetIndex( self, key ):
for i in range( self._list_box.count() ):
@@ -85,36 +53,30 @@ def _GetIndex( self, key ):
return -1
- def _Select( self, selection ):
+ def _ListBoxSelected( self ):
- if selection == -1:
-
- self._current_key = None
+ selected_datas = list( self._list_box.GetData( only_selected = True ) )
+
+ if len( selected_datas ) > 0:
- else:
+ key = selected_datas[0]
- self._current_key = self._list_box.item( selection ).data( QC.Qt.UserRole )
+ self._ShowPage( key )
- self._current_panel.setVisible( False )
-
- self._list_box.blockSignals( True )
+
+ def _ShowPage( self, key ):
- QP.ListWidgetSetSelection( self._list_box, selection )
+ self._current_key = key
- self._list_box.blockSignals( False )
+ self._current_panel.setVisible( False )
- if selection == -1:
+ if self._current_key is None:
self._current_panel = self._empty_panel
else:
- if self._current_key in self._keys_to_proto_pages:
-
- self._ActivatePage( self._current_key )
-
-
self._current_panel = self._keys_to_active_pages[ self._current_key ]
@@ -125,7 +87,7 @@ def _Select( self, selection ):
def AddPage( self, display_name, key, page, select = False ):
- if self._GetIndex( key ) != -1:
+ if self.HasKey( key ):
raise HydrusExceptions.NameException( 'That entry already exists!' )
@@ -137,72 +99,20 @@ def AddPage( self, display_name, key, page, select = False ):
QP.AddToLayout( self._panel_sizer, page, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
- # Could call QListWidget.sortItems() here instead of doing it manually
-
- current_display_names = QP.ListWidgetGetStrings( self._list_box )
-
- insertion_index = len( current_display_names )
-
- for ( i, current_display_name ) in enumerate( current_display_names ):
+ if self._list_box.count() == 0:
- if current_display_name > display_name:
-
- insertion_index = i
-
- break
-
+ select = True
- item = QW.QListWidgetItem()
- item.setText( display_name )
- item.setData( QC.Qt.UserRole, key )
- self._list_box.insertItem( insertion_index, item )
self._keys_to_active_pages[ key ] = page
- if self._list_box.count() == 1:
-
- self._Select( 0 )
-
- elif select:
-
- index = self._GetIndex( key )
-
- self._Select( index )
-
+ self._list_box.Append( display_name, key, select = select )
-
- def AddPageArgs( self, display_name, key, classname, args, kwargs ):
+ self._list_box.sortItems()
- if self._GetIndex( key ) != -1:
+ if select:
- raise HydrusExceptions.NameException( 'That entry already exists!' )
-
-
- # Could call QListWidget.sortItems() here instead of doing it manually
-
- current_display_names = QP.ListWidgetGetStrings( self._list_box )
-
- insertion_index = len( current_display_names )
-
- for ( i, current_display_name ) in enumerate( current_display_names ):
-
- if current_display_name > display_name:
-
- insertion_index = i
-
- break
-
-
- item = QW.QListWidgetItem()
- item.setText( display_name )
- item.setData( QC.Qt.UserRole, key )
- self._list_box.insertItem( insertion_index, item )
-
- self._keys_to_proto_pages[ key ] = ( classname, args, kwargs )
-
- if self._list_box.count() == 1:
-
- self._Select( 0 )
+ self._list_box.SelectData( [ key ] )
@@ -219,53 +129,43 @@ def DeleteAllPages( self ):
self._current_panel = self._empty_panel
self._keys_to_active_pages = {}
- self._keys_to_proto_pages = {}
self._list_box.clear()
def DeleteCurrentPage( self ):
- selection = QP.ListWidgetGetSelection( self._list_box )
-
- if selection != -1:
+ if self._list_box.GetNumSelected() > 0:
key_to_delete = self._current_key
page_to_delete = self._current_panel
+ selected_indices = self._list_box.GetSelectedIndices()
+
+ selection = selected_indices[0]
+
next_selection = selection + 1
previous_selection = selection - 1
+ self._panel_sizer.removeWidget( page_to_delete )
+
+ page_to_delete.deleteLater()
+
+ del self._keys_to_active_pages[ key_to_delete ]
+
+ self._list_box.DeleteSelected()
+
if next_selection < self._list_box.count():
- self._Select( next_selection )
+ self._list_box.SelectData( [ self._list_box.item( next_selection ).data( QC.Qt.UserRole ) ] )
elif previous_selection >= 0:
- self._Select( previous_selection )
+ self._list_box.SelectData( [ self._list_box.item( previous_selection ).data( QC.Qt.UserRole ) ] )
else:
- self._Select( -1 )
-
-
- self._panel_sizer.removeWidget( page_to_delete )
-
- page_to_delete.deleteLater()
-
- del self._keys_to_active_pages[ key_to_delete ]
-
- QP.ListWidgetDelete( self._list_box, selection )
-
-
-
- def EventSelection( self ):
-
- selection = QP.ListWidgetGetSelection( self._list_box )
-
- if selection != self._GetIndex( self._current_key ):
-
- self._Select( selection )
+ self._ShowPage( None )
@@ -294,11 +194,6 @@ def GetActivePages( self ):
def GetPage( self, key ):
- if key in self._keys_to_proto_pages:
-
- self._ActivatePage( key )
-
-
if key in self._keys_to_active_pages:
return self._keys_to_active_pages[ key ]
@@ -309,70 +204,32 @@ def GetPage( self, key ):
def GetPageCount( self ):
- return len( self._keys_to_active_pages ) + len( self._keys_to_proto_pages )
+ return len( self._keys_to_active_pages )
- def KeyExists( self, key ):
+ def HasKey( self, key ):
- return key in self._keys_to_active_pages or key in self._keys_to_proto_pages
+ return key in self._keys_to_active_pages
def Select( self, key ):
- index = self._GetIndex( key )
-
- if index != -1 and index != QP.ListWidgetGetSelection( self._list_box ) :
+ if self.HasKey( key ):
- self._Select( index )
-
-
-
- def SelectDown( self ):
-
- current_selection = QP.ListWidgetGetSelection( self._list_box )
-
- if current_selection != -1:
-
- num_entries = self._list_box.count()
-
- if current_selection == num_entries - 1: selection = 0
- else: selection = current_selection + 1
-
- if selection != current_selection:
-
- self._Select( selection )
-
+ self._list_box.SelectData( [ key ] )
def SelectPage( self, page_to_select ):
- for ( key, page ) in list(self._keys_to_active_pages.items()):
+ for ( key, page ) in self._keys_to_active_pages.items():
if page == page_to_select:
- self._Select( self._GetIndex( key ) )
+ self._list_box.SelectData( [ key ] )
return
- def SelectUp( self ):
-
- current_selection = QP.ListWidgetGetSelection( self._list_box )
-
- if current_selection != -1:
-
- num_entries = self._list_box.count()
-
- if current_selection == 0: selection = num_entries - 1
- else: selection = current_selection - 1
-
- if selection != current_selection:
-
- self._Select( selection )
-
-
-
-
diff --git a/hydrus/client/gui/lists/ClientGUIListBoxes.py b/hydrus/client/gui/lists/ClientGUIListBoxes.py
index 4e6ca6a14..41367dbe3 100644
--- a/hydrus/client/gui/lists/ClientGUIListBoxes.py
+++ b/hydrus/client/gui/lists/ClientGUIListBoxes.py
@@ -72,7 +72,7 @@ def _GetDataIndices( self, datas: typing.Collection[ object ] ) -> typing.List[
return indices
- def _GetListWidgetItems( self, only_selected = False ):
+ def _GetListWidgetItems( self, only_selected = False ) -> typing.Collection[ QW.QListWidgetItem ]:
# not sure if selectedItems is always sorted, so just do it manually
@@ -176,6 +176,16 @@ def GetNumSelected( self ) -> int:
return len( indices )
+ def GetSelectedIndices( self ):
+
+ return self._GetSelectedIndices()
+
+
+ def HasData( self, obj ):
+
+ return obj in self.GetData()
+
+
def keyPressEvent( self, event: QG.QKeyEvent ):
if event.modifiers() & QC.Qt.ControlModifier and event.key() in ( QC.Qt.Key_C, QC.Qt.Key_Insert ):
@@ -260,6 +270,8 @@ def PopData( self, index: int ):
def SelectData( self, datas: typing.Collection[ object ] ):
+ datas = set( datas )
+
list_widget_items = self._GetListWidgetItems()
for list_widget_item in list_widget_items:
diff --git a/hydrus/client/gui/pages/ClientGUIManagementPanels.py b/hydrus/client/gui/pages/ClientGUIManagementPanels.py
index 898f949c4..9bd4a8dbe 100644
--- a/hydrus/client/gui/pages/ClientGUIManagementPanels.py
+++ b/hydrus/client/gui/pages/ClientGUIManagementPanels.py
@@ -1303,10 +1303,6 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._gallery_importers_listctrl_panel.AddButton( 'retry failed', self._RetryFailed, enabled_check_func = self._CanRetryFailed )
self._gallery_importers_listctrl_panel.AddButton( 'retry ignored', self._RetryIgnored, enabled_check_func = self._CanRetryIgnored )
- self._gallery_importers_listctrl_panel.NewButtonRow()
-
- self._gallery_importers_listctrl_panel.AddButton( 'set options to queries', self._SetOptionsToGalleryImports, enabled_only_on_selection = True )
-
self._gallery_importers_listctrl.Sort()
#
@@ -1335,6 +1331,10 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._import_options_button.SetTagImportOptions( tag_import_options )
self._import_options_button.SetNoteImportOptions( note_import_options )
+ self._set_options_to_queries_button = ClientGUICommon.BetterButton( self, 'update selected queries with current options', self._SetOptionsToGalleryImports )
+ self._set_options_to_queries_button.setToolTip( 'Each query has its own file limit and import options (you can review them in the highlight panel below). These are not updated if the main page\'s options are updated. It seems some downloaders in your selection differ with what the page currently has. Clicking here will update the selected queries with whatever the page currently has.' )
+ self._set_options_to_queries_button.setVisible( False )
+
#
input_hbox = QP.HBoxLayout()
@@ -1348,7 +1348,13 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._gallery_downloader_panel.Add( input_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
self._gallery_downloader_panel.Add( self._gug_key_and_name, CC.FLAGS_EXPAND_PERPENDICULAR )
self._gallery_downloader_panel.Add( self._file_limit, CC.FLAGS_EXPAND_PERPENDICULAR )
- self._gallery_downloader_panel.Add( self._import_options_button, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ import_buttons_hbox = QP.HBoxLayout()
+
+ QP.AddToLayout( import_buttons_hbox, self._import_options_button, CC.FLAGS_EXPAND_BOTH_WAYS )
+ QP.AddToLayout( import_buttons_hbox, self._set_options_to_queries_button, CC.FLAGS_EXPAND_BOTH_WAYS )
+
+ self._gallery_downloader_panel.Add( import_buttons_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
#
@@ -1386,6 +1392,11 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._import_options_button.noteImportOptionsChanged.connect( self._multiple_gallery_import.SetNoteImportOptions )
self._import_options_button.tagImportOptionsChanged.connect( self._multiple_gallery_import.SetTagImportOptions )
+ self._file_limit.valueChanged.connect( self._UpdateImportOptionsSetButton )
+ self._import_options_button.importOptionsChanged.connect( self._UpdateImportOptionsSetButton )
+ self._highlighted_gallery_import_panel.importOptionsChanged.connect( self._UpdateImportOptionsSetButton )
+ self._gallery_importers_listctrl.selectionModel().selectionChanged.connect( self._UpdateImportOptionsSetButton )
+
def _CanClearHighlight( self ):
@@ -1929,7 +1940,7 @@ def _SetOptionsToGalleryImports( self ):
return
- message = 'Set the current file limit, file import, and tag import options to all the selected queries? (by default, these options are only applied to new queries)'
+ message = 'Set the page\'s current file limit and import options to all the selected queries?'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -1948,6 +1959,8 @@ def _SetOptionsToGalleryImports( self ):
gallery_import.SetNoteImportOptions( note_import_options )
+ self._UpdateImportOptionsSetButton()
+
def _ShowCogMenu( self ):
@@ -2081,6 +2094,108 @@ def _ThisIsTheCurrentOrLoadingHighlight( self, gallery_import ):
+ def _UpdateImportOptionsSetButton( self ):
+
+ selected_gallery_imports = self._gallery_importers_listctrl.GetData( only_selected = True )
+
+ show_it = False
+
+ if len( selected_gallery_imports ) > 0:
+
+ # ok the serialisable comparison sucks, but we can cut down the repeated work to just one per future run of this method by updating our children with exactly our object
+
+ file_limit = self._file_limit.GetValue()
+ file_import_options = self._import_options_button.GetFileImportOptions()
+ note_import_options = self._import_options_button.GetNoteImportOptions()
+ tag_import_options = self._import_options_button.GetTagImportOptions()
+
+ file_import_options_string = None
+ note_import_options_string = None
+ tag_import_options_string = None
+
+ for gallery_import in selected_gallery_imports:
+
+ if gallery_import.GetFileLimit() != file_limit:
+
+ show_it = True
+
+ break
+
+
+ gallery_import_file_import_options = gallery_import.GetFileImportOptions()
+
+ if gallery_import_file_import_options != file_import_options:
+
+ if file_import_options_string is None:
+
+ file_import_options_string = file_import_options.DumpToString()
+
+
+ # not the same object, let's see if they have the same value
+ if gallery_import_file_import_options.DumpToString() == file_import_options_string:
+
+ # we have the same value here, just not the same object. let's make the check faster next time
+ gallery_import.SetFileImportOptions( file_import_options )
+
+ else:
+
+ show_it = True
+
+ break
+
+
+
+ gallery_import_note_import_options = gallery_import.GetNoteImportOptions()
+
+ if gallery_import_note_import_options != note_import_options:
+
+ if note_import_options_string is None:
+
+ note_import_options_string = note_import_options.DumpToString()
+
+
+ # not the same object, let's see if they have the same value
+ if gallery_import_note_import_options.DumpToString() == note_import_options_string:
+
+ # we have the same value here, just not the same object. let's make the check faster next time
+ gallery_import.SetNoteImportOptions( note_import_options )
+
+ else:
+
+ show_it = True
+
+ break
+
+
+
+ gallery_import_tag_import_options = gallery_import.GetTagImportOptions()
+
+ if gallery_import_tag_import_options != tag_import_options:
+
+ if tag_import_options_string is None:
+
+ tag_import_options_string = tag_import_options.DumpToString()
+
+
+ # not the same object, let's see if they have the same value
+ if gallery_import_tag_import_options.DumpToString() == tag_import_options_string:
+
+ # we have the same value here, just not the same object. let's make the check faster next time
+ gallery_import.SetTagImportOptions( tag_import_options )
+
+ else:
+
+ show_it = True
+
+ break
+
+
+
+
+
+ self._set_options_to_queries_button.setVisible( show_it )
+
+
def _UpdateImportStatus( self ):
if HydrusTime.TimeHasPassed( self._next_update_time ):
@@ -2244,10 +2359,6 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._watchers_listctrl_panel.AddButton( 'retry failed', self._RetryFailed, enabled_check_func = self._CanRetryFailed )
self._watchers_listctrl_panel.AddButton( 'retry ignored', self._RetryIgnored, enabled_check_func = self._CanRetryIgnored )
- self._watchers_listctrl_panel.NewButtonRow()
-
- self._watchers_listctrl_panel.AddButton( 'set options to watchers', self._SetOptionsToWatchers, enabled_only_on_selection = True )
-
self._watchers_listctrl.Sort()
self._watcher_url_input = ClientGUITextInput.TextAndPasteCtrl( self._watchers_panel, self._AddURLs )
@@ -2265,6 +2376,10 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._import_options_button.SetTagImportOptions( tag_import_options )
self._import_options_button.SetNoteImportOptions( note_import_options )
+ self._set_options_to_watchers_button = ClientGUICommon.BetterButton( self, 'update selected queries with current options', self._SetOptionsToWatchers )
+ self._set_options_to_watchers_button.setToolTip( 'Each watcher has its own checker and import options (you can review them in the highlight panel below). These are not updated if the main page\'s options are updated. It seems some watchers in your selection differ with what the page currently has. Clicking here will update the selected watchers with whatever the page currently has.' )
+ self._set_options_to_watchers_button.setVisible( False )
+
# suck up watchers from elsewhere in the program (presents a checkboxlistdialog)
#
@@ -2280,7 +2395,13 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._watchers_panel.Add( self._watchers_listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
self._watchers_panel.Add( self._watcher_url_input, CC.FLAGS_EXPAND_PERPENDICULAR )
self._watchers_panel.Add( self._checker_options, CC.FLAGS_EXPAND_PERPENDICULAR )
- self._watchers_panel.Add( self._import_options_button, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ import_buttons_hbox = QP.HBoxLayout()
+
+ QP.AddToLayout( import_buttons_hbox, self._import_options_button, CC.FLAGS_EXPAND_BOTH_WAYS )
+ QP.AddToLayout( import_buttons_hbox, self._set_options_to_watchers_button, CC.FLAGS_EXPAND_BOTH_WAYS )
+
+ self._watchers_panel.Add( import_buttons_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
#
@@ -2310,6 +2431,11 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._checker_options.valueChanged.connect( self._OptionsUpdated )
+ self._import_options_button.importOptionsChanged.connect( self._UpdateImportOptionsSetButton )
+ self._checker_options.valueChanged.connect( self._UpdateImportOptionsSetButton )
+ self._highlighted_watcher_panel.importOptionsChanged.connect( self._UpdateImportOptionsSetButton )
+ self._watchers_listctrl.selectionModel().selectionChanged.connect( self._UpdateImportOptionsSetButton )
+
def _AddURLs( self, urls, filterable_tags = None, additional_service_keys_to_tags = None ):
@@ -2939,7 +3065,7 @@ def _SetOptionsToWatchers( self ):
return
- message = 'Set the current checker, file import, and tag import options to all the selected watchers? (by default, these options are only applied to new watchers)'
+ message = 'Set the current checker and import options to all the selected watchers?'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -2947,15 +3073,19 @@ def _SetOptionsToWatchers( self ):
checker_options = self._checker_options.GetValue()
file_import_options = self._import_options_button.GetFileImportOptions()
+ note_import_options = self._import_options_button.GetNoteImportOptions()
tag_import_options = self._import_options_button.GetTagImportOptions()
for watcher in watchers:
watcher.SetCheckerOptions( checker_options )
watcher.SetFileImportOptions( file_import_options )
+ watcher.SetNoteImportOptions( note_import_options )
watcher.SetTagImportOptions( tag_import_options )
+ self._UpdateImportOptionsSetButton()
+
def _ShowSelectedImportersFileSeedCaches( self ):
@@ -3071,6 +3201,125 @@ def _ThisIsTheCurrentOrLoadingHighlight( self, watcher ):
+ def _UpdateImportOptionsSetButton( self ):
+
+ selected_watchers = self._watchers_listctrl.GetData( only_selected = True )
+
+ show_it = False
+
+ if len( selected_watchers ) > 0:
+
+ # ok the serialisable comparison sucks, but we can cut down the repeated work to just one per future run of this method by updating our children with exactly our object
+
+ checker_options = self._checker_options.GetValue()
+ file_import_options = self._import_options_button.GetFileImportOptions()
+ note_import_options = self._import_options_button.GetNoteImportOptions()
+ tag_import_options = self._import_options_button.GetTagImportOptions()
+
+ checker_options_string = None
+ file_import_options_string = None
+ note_import_options_string = None
+ tag_import_options_string = None
+
+ for watcher in selected_watchers:
+
+ watcher_checker_options = watcher.GetCheckerOptions()
+
+ if watcher_checker_options != checker_options:
+
+ if checker_options_string is None:
+
+ checker_options_string = checker_options.DumpToString()
+
+
+ # not the same object, let's see if they have the same value
+ if watcher_checker_options.DumpToString() == checker_options_string:
+
+ # we have the same value here, just not the same object. let's make the check faster next time
+ watcher.SetCheckerOptions( checker_options )
+
+ else:
+
+ show_it = True
+
+ break
+
+
+
+ watcher_file_import_options = watcher.GetFileImportOptions()
+
+ if watcher_file_import_options != file_import_options:
+
+ if file_import_options_string is None:
+
+ file_import_options_string = file_import_options.DumpToString()
+
+
+ # not the same object, let's see if they have the same value
+ if watcher_file_import_options.DumpToString() == file_import_options_string:
+
+ # we have the same value here, just not the same object. let's make the check faster next time
+ watcher.SetFileImportOptions( file_import_options )
+
+ else:
+
+ show_it = True
+
+ break
+
+
+
+ watcher_note_import_options = watcher.GetNoteImportOptions()
+
+ if watcher_note_import_options != note_import_options:
+
+ if note_import_options_string is None:
+
+ note_import_options_string = note_import_options.DumpToString()
+
+
+ # not the same object, let's see if they have the same value
+ if watcher_note_import_options.DumpToString() == note_import_options_string:
+
+ # we have the same value here, just not the same object. let's make the check faster next time
+ watcher.SetNoteImportOptions( note_import_options )
+
+ else:
+
+ show_it = True
+
+ break
+
+
+
+ watcher_tag_import_options = watcher.GetTagImportOptions()
+
+ if watcher_tag_import_options != tag_import_options:
+
+ if tag_import_options_string is None:
+
+ tag_import_options_string = tag_import_options.DumpToString()
+
+
+ # not the same object, let's see if they have the same value
+ if watcher_tag_import_options.DumpToString() == tag_import_options_string:
+
+ # we have the same value here, just not the same object. let's make the check faster next time
+ watcher.SetTagImportOptions( tag_import_options )
+
+ else:
+
+ show_it = True
+
+ break
+
+
+
+
+
+ self._set_options_to_watchers_button.setVisible( show_it )
+
+
def _UpdateImportStatus( self ):
if HydrusTime.TimeHasPassed( self._next_update_time ):
diff --git a/hydrus/client/gui/search/ClientGUIPredicatesMultiple.py b/hydrus/client/gui/search/ClientGUIPredicatesMultiple.py
index ed7c7aaec..ef09d7c4b 100644
--- a/hydrus/client/gui/search/ClientGUIPredicatesMultiple.py
+++ b/hydrus/client/gui/search/ClientGUIPredicatesMultiple.py
@@ -32,7 +32,7 @@ def __init__( self, parent: QW.QWidget, service_key: bytes, predicate: typing.Op
name_st.setAlignment( QC.Qt.AlignLeft | QC.Qt.AlignVCenter )
- choices = [
+ choice_tuples = [
( 'more than', '>' ),
( 'less than', '<' ),
( 'is', '=' ),
@@ -40,7 +40,7 @@ def __init__( self, parent: QW.QWidget, service_key: bytes, predicate: typing.Op
( 'do not search', '' )
]
- self._choice = QP.DataRadioBox( self, choices, vertical = True )
+ self._choice = ClientGUICommon.BetterRadioBox( self, choice_tuples, vertical = True )
self._rating_value = ClientGUICommon.BetterSpinBox( self, initial = 0, min = 0, max = 1000000 )
@@ -122,13 +122,13 @@ def __init__( self, parent: QW.QWidget, service_key: bytes, predicate: typing.Op
name_st.setAlignment( QC.Qt.AlignLeft | QC.Qt.AlignVCenter )
- choices = [
+ choice_tuples = [
( 'has rating', 'rated' ),
( 'is', '=' ),
( 'do not search', '' )
]
- self._choice = QP.DataRadioBox( self, choices, vertical = True )
+ self._choice = ClientGUICommon.BetterRadioBox( self, choice_tuples, vertical = True )
self._rating_control = ClientGUIRatings.RatingLikeDialog( self, service_key )
@@ -258,7 +258,7 @@ def __init__( self, parent: QW.QWidget, service_key: bytes, predicate: typing.Op
name_st.setAlignment( QC.Qt.AlignLeft | QC.Qt.AlignVCenter )
- choices = [
+ choice_tuples = [
( 'has rating', 'rated' ),
( 'more than', '>' ),
( 'less than', '<' ),
@@ -267,7 +267,7 @@ def __init__( self, parent: QW.QWidget, service_key: bytes, predicate: typing.Op
( 'do not search', '' )
]
- self._choice = QP.DataRadioBox( self, choices, vertical = True )
+ self._choice = ClientGUICommon.BetterRadioBox( self, choice_tuples, vertical = True )
self._rating_control = ClientGUIRatings.RatingNumericalDialog( self, service_key )
diff --git a/hydrus/client/gui/search/ClientGUIPredicatesSingle.py b/hydrus/client/gui/search/ClientGUIPredicatesSingle.py
index b8538e756..9974965e0 100644
--- a/hydrus/client/gui/search/ClientGUIPredicatesSingle.py
+++ b/hydrus/client/gui/search/ClientGUIPredicatesSingle.py
@@ -86,7 +86,7 @@ def GetPredicates( self ) -> typing.List[ ClientSearch.Predicate ]:
-class TimeDateOperator( QP.DataRadioBox ):
+class TimeDateOperator( ClientGUICommon.BetterRadioBox ):
def __init__( self, parent ):
@@ -97,11 +97,11 @@ def __init__( self, parent ):
( '+/- a month of', HC.UNICODE_APPROX_EQUAL )
]
- QP.DataRadioBox.__init__( self, parent, choice_tuples, vertical = True )
+ ClientGUICommon.BetterRadioBox.__init__( self, parent, choice_tuples, vertical = True )
-class TimeDeltaOperator( QP.DataRadioBox ):
+class TimeDeltaOperator( ClientGUICommon.BetterRadioBox ):
def __init__( self, parent ):
@@ -111,7 +111,7 @@ def __init__( self, parent ):
( '+/- 15% of', HC.UNICODE_APPROX_EQUAL )
]
- QP.DataRadioBox.__init__( self, parent, choice_tuples, vertical = True )
+ ClientGUICommon.BetterRadioBox.__init__( self, parent, choice_tuples, vertical = True )
@@ -760,13 +760,13 @@ def __init__( self, parent, predicate ):
choices = [ '<', HC.UNICODE_APPROX_EQUAL, '=', '>' ]
- self._sign = QP.RadioBox( self, choices = choices )
+ self._sign = ClientGUICommon.BetterRadioBox( self, [ ( c, c ) for c in choices ] )
self._num = ClientGUICommon.BetterSpinBox( self, min=0, max=65535 )
- choices = [ ( HC.duplicate_type_string_lookup[ status ], status ) for status in ( HC.DUPLICATE_MEMBER, HC.DUPLICATE_ALTERNATE, HC.DUPLICATE_FALSE_POSITIVE, HC.DUPLICATE_POTENTIAL ) ]
+ choice_tuples = [ ( HC.duplicate_type_string_lookup[ status ], status ) for status in ( HC.DUPLICATE_MEMBER, HC.DUPLICATE_ALTERNATE, HC.DUPLICATE_FALSE_POSITIVE, HC.DUPLICATE_POTENTIAL ) ]
- self._dupe_type = ClientGUICommon.BetterRadioBox( self, choices = choices, vertical = True )
+ self._dupe_type = ClientGUICommon.BetterRadioBox( self, choice_tuples, vertical = True )
#
@@ -774,7 +774,7 @@ def __init__( self, parent, predicate ):
( sign, num, dupe_type ) = predicate.GetValue()
- self._sign.SetStringSelection( sign )
+ self._sign.SetValue( sign )
self._num.setValue( num )
self._dupe_type.SetValue( dupe_type )
@@ -803,7 +803,7 @@ def GetDefaultPredicate( self ):
def GetPredicates( self ):
- predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_COUNT, ( self._sign.GetStringSelection(), self._num.value(), self._dupe_type.GetValue() ) ), )
+ predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_COUNT, ( self._sign.GetValue(), self._num.value(), self._dupe_type.GetValue() ) ), )
return predicates
@@ -866,22 +866,22 @@ def __init__( self, parent, predicate ):
PanelPredicateSystemSingle.__init__( self, parent )
- self._sign = ClientGUICommon.BetterRadioBox( self, choices = [ ( 'is', True ), ( 'is not', False ) ], vertical = True )
+ self._sign = ClientGUICommon.BetterRadioBox( self, [ ( 'is', True ), ( 'is not', False ) ], vertical = True )
- choices = [
+ choice_tuples = [
( 'currently in', HC.CONTENT_STATUS_CURRENT ),
( 'deleted from', HC.CONTENT_STATUS_DELETED ),
( 'pending to', HC.CONTENT_STATUS_PENDING ),
( 'petitioned from', HC.CONTENT_STATUS_PETITIONED )
]
- self._status = ClientGUICommon.BetterRadioBox( self, choices = choices, vertical = True )
+ self._status = ClientGUICommon.BetterRadioBox( self, choice_tuples, vertical = True )
services = CG.client_controller.services_manager.GetServices( HC.REAL_FILE_SERVICES )
- choices = [ ( service.GetName(), service.GetServiceKey() ) for service in services ]
+ choice_tuples = [ ( service.GetName(), service.GetServiceKey() ) for service in services ]
- self._file_service_key = ClientGUICommon.BetterRadioBox( self, choices = choices, vertical = True )
+ self._file_service_key = ClientGUICommon.BetterRadioBox( self, choice_tuples, vertical = True )
#
@@ -934,7 +934,9 @@ def __init__( self, parent, predicate ):
self._viewing_locations.Append( 'media views', 'media' )
self._viewing_locations.Append( 'preview views', 'preview' )
- self._sign = QP.RadioBox( self, choices=['<',HC.UNICODE_APPROX_EQUAL,'=','>'] )
+ choices = ['<',HC.UNICODE_APPROX_EQUAL,'=','>']
+
+ self._sign = ClientGUICommon.BetterRadioBox( self, [ ( c, c ) for c in choices ] )
self._num = ClientGUICommon.BetterSpinBox( self, min=0, max=1000000 )
@@ -950,7 +952,7 @@ def __init__( self, parent, predicate ):
self._viewing_locations.setMaximumHeight( height )
- self._sign.SetStringSelection( sign )
+ self._sign.SetValue( sign )
self._num.setValue( num )
@@ -986,7 +988,7 @@ def GetPredicates( self ):
viewing_locations = [ 'media' ]
- sign = self._sign.GetStringSelection()
+ sign = self._sign.GetValue()
num = self._num.value()
@@ -1006,7 +1008,9 @@ def __init__( self, parent, predicate ):
self._viewing_locations.Append( 'media viewtime', 'media' )
self._viewing_locations.Append( 'preview viewtime', 'preview' )
- self._sign = QP.RadioBox( self, choices=['<',HC.UNICODE_APPROX_EQUAL,'=','>'] )
+ choices = ['<',HC.UNICODE_APPROX_EQUAL,'=','>']
+
+ self._sign = ClientGUICommon.BetterRadioBox( self, [ ( c, c ) for c in choices ] )
self._time_delta = ClientGUITime.TimeDeltaCtrl( self, min = 0, days = True, hours = True, minutes = True, seconds = True )
@@ -1022,7 +1026,7 @@ def __init__( self, parent, predicate ):
self._viewing_locations.setMaximumHeight( height )
- self._sign.SetStringSelection( sign )
+ self._sign.SetValue( sign )
self._time_delta.SetValue( time_delta )
@@ -1058,7 +1062,7 @@ def GetPredicates( self ):
viewing_locations = [ 'media' ]
- sign = self._sign.GetStringSelection()
+ sign = self._sign.GetValue()
time_delta = self._time_delta.GetValue()
@@ -1124,11 +1128,11 @@ def __init__( self, parent, predicate ):
PanelPredicateSystemSingle.__init__( self, parent )
- self._sign = ClientGUICommon.BetterRadioBox( self, choices = [ ( 'is', True ), ( 'is not', False ) ], vertical = True )
+ self._sign = ClientGUICommon.BetterRadioBox( self, [ ( 'is', True ), ( 'is not', False ) ], vertical = True )
choices = [ 'sha256', 'md5', 'sha1', 'sha512' ]
- self._hash_type = QP.RadioBox( self, choices = choices, vertical = True )
+ self._hash_type = ClientGUICommon.BetterRadioBox( self, [ ( c, c ) for c in choices ], vertical = True )
self._hashes = QW.QPlainTextEdit( self )
@@ -1150,7 +1154,7 @@ def __init__( self, parent, predicate ):
self._hashes.setPlainText( hashes_text )
- self._hash_type.SetStringSelection( hash_type )
+ self._hash_type.SetValue( hash_type )
#
@@ -1178,7 +1182,7 @@ def GetPredicates( self ):
inclusive = self._sign.GetValue()
- hash_type = self._hash_type.GetStringSelection()
+ hash_type = self._hash_type.GetValue()
hex_hashes_raw = self._hashes.toPlainText()
@@ -1697,11 +1701,15 @@ def __init__( self, parent, predicate ):
PanelPredicateSystemSingle.__init__( self, parent )
- self._sign = QP.RadioBox( self, choices=[ '<', HC.UNICODE_APPROX_EQUAL, '=', HC.UNICODE_NOT_EQUAL, '>' ] )
+ choices = [ '<', HC.UNICODE_APPROX_EQUAL, '=', HC.UNICODE_NOT_EQUAL, '>' ]
+
+ self._sign = ClientGUICommon.BetterRadioBox( self, [ ( c, c ) for c in choices ] )
self._num_pixels = ClientGUICommon.BetterSpinBox( self, max=1048576, width = 60 )
- self._unit = QP.RadioBox( self, choices=['pixels','kilopixels','megapixels'] )
+ choice_tuples = [ ( 'pixels', 1 ), ( 'kilopixels', 1000 ), ( 'megapixels', 1000000 ) ]
+
+ self._unit = ClientGUICommon.BetterRadioBox( self, choice_tuples )
#
@@ -1709,11 +1717,11 @@ def __init__( self, parent, predicate ):
( sign, num_pixels, unit ) = predicate.GetValue()
- self._sign.SetStringSelection( sign )
+ self._sign.SetValue( sign )
self._num_pixels.setValue( num_pixels )
- self._unit.SetStringSelection( HydrusData.ConvertIntToPixels( unit ) )
+ self._unit.SetValue( unit )
#
@@ -1740,7 +1748,7 @@ def GetDefaultPredicate( self ):
def GetPredicates( self ):
- predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_PIXELS, ( self._sign.GetStringSelection(), self._num_pixels.value(), HydrusData.ConvertPixelsToInt( self._unit.GetStringSelection() ) ) ), )
+ predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_PIXELS, ( self._sign.GetValue(), self._num_pixels.value(), self._unit.GetValue() ) ), )
return predicates
@@ -1808,7 +1816,9 @@ def __init__( self, parent, predicate ):
self._namespace.setPlaceholderText( 'Leave empty for unnamespaced, \'*\' for all namespaces' )
self._namespace.setToolTip( ClientGUIFunctions.WrapToolTip( 'Leave empty for unnamespaced, \'*\' for all namespaces. Other wildcards also supported.' ) )
- self._sign = QP.RadioBox( self, choices=['<',HC.UNICODE_APPROX_EQUAL,'=','>'] )
+ choices = ['<',HC.UNICODE_APPROX_EQUAL,'=','>']
+
+ self._sign = ClientGUICommon.BetterRadioBox( self, [ ( c, c ) for c in choices ] )
self._num_tags = ClientGUICommon.BetterSpinBox( self, max=2000, width = 60 )
@@ -1825,7 +1835,7 @@ def __init__( self, parent, predicate ):
self._namespace.setText( namespace )
- self._sign.SetStringSelection( sign )
+ self._sign.SetValue( sign )
self._num_tags.setValue( num_tags )
@@ -1854,7 +1864,7 @@ def GetDefaultPredicate( self ):
def GetPredicates( self ):
- ( namespace, operator, value ) = ( self._namespace.text(), self._sign.GetStringSelection(), self._num_tags.value() )
+ ( namespace, operator, value ) = ( self._namespace.text(), self._sign.GetValue(), self._num_tags.value() )
predicate = None
@@ -2047,7 +2057,9 @@ def __init__( self, parent, predicate ):
PanelPredicateSystemSingle.__init__( self, parent )
- self._sign = QP.RadioBox( self, choices=['=','wider than','taller than',HC.UNICODE_APPROX_EQUAL,HC.UNICODE_NOT_EQUAL] )
+ choices = ['=','wider than','taller than',HC.UNICODE_APPROX_EQUAL,HC.UNICODE_NOT_EQUAL]
+
+ self._sign = ClientGUICommon.BetterRadioBox( self, [ ( c, c ) for c in choices ] )
self._width = ClientGUICommon.BetterSpinBox( self, max=50000, width = 60 )
@@ -2059,7 +2071,7 @@ def __init__( self, parent, predicate ):
( sign, width, height ) = predicate.GetValue()
- self._sign.SetStringSelection( sign )
+ self._sign.SetValue( sign )
self._width.setValue( width )
@@ -2091,7 +2103,7 @@ def GetDefaultPredicate( self ):
def GetPredicates( self ):
- predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO, ( self._sign.GetStringSelection(), self._width.value(), self._height.value() ) ), )
+ predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO, ( self._sign.GetValue(), self._width.value(), self._height.value() ) ), )
return predicates
@@ -2382,7 +2394,9 @@ def __init__( self, parent, predicate ):
PanelPredicateSystemSingle.__init__( self, parent )
- self._sign = QP.RadioBox( self, choices=['<',HC.UNICODE_APPROX_EQUAL,'=',HC.UNICODE_NOT_EQUAL,'>'] )
+ choices = ['<',HC.UNICODE_APPROX_EQUAL,'=',HC.UNICODE_NOT_EQUAL,'>']
+
+ self._sign = ClientGUICommon.BetterRadioBox( self, [ ( c, c ) for c in choices ] )
self._bytes = ClientGUIBytes.BytesControl( self )
@@ -2392,7 +2406,7 @@ def __init__( self, parent, predicate ):
( sign, size, unit ) = predicate.GetValue()
- self._sign.SetStringSelection( sign )
+ self._sign.SetValue( sign )
self._bytes.SetSeparatedValue( size, unit )
@@ -2422,7 +2436,7 @@ def GetPredicates( self ):
( size, unit ) = self._bytes.GetSeparatedValue()
- predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE, ( self._sign.GetStringSelection(), size, unit ) ), )
+ predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE, ( self._sign.GetValue(), size, unit ) ), )
return predicates
@@ -2437,7 +2451,7 @@ def __init__( self, parent, predicate ):
choices = [ '<', HC.UNICODE_APPROX_EQUAL, '>' ]
- self._sign = QP.RadioBox( self, choices = choices )
+ self._sign = ClientGUICommon.BetterRadioBox( self, [ ( c, c ) for c in choices ] )
self._num = ClientGUICommon.BetterSpinBox( self, min=-(2 ** 31), max= (2 ** 31) - 1 )
@@ -2451,7 +2465,7 @@ def __init__( self, parent, predicate ):
self._namespace.setPlaceholderText( 'Leave empty for unnamespaced, \'*\' for all namespaces' )
self._namespace.setToolTip( ClientGUIFunctions.WrapToolTip( 'Leave empty for unnamespaced, \'*\' for all namespaces. Other wildcards also supported.' ) )
- self._sign.SetStringSelection( sign )
+ self._sign.SetValue( sign )
self._num.setValue( num )
#
@@ -2477,7 +2491,7 @@ def GetDefaultPredicate( self ):
def GetPredicates( self ):
- predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER, ( self._namespace.text(), self._sign.GetStringSelection(), self._num.value() ) ), )
+ predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER, ( self._namespace.text(), self._sign.GetValue(), self._num.value() ) ), )
return predicates
diff --git a/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py b/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py
index 313ba5883..f488a0493 100644
--- a/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py
+++ b/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py
@@ -428,9 +428,9 @@ def __init__( self, parent: QW.QWidget, shortcuts_name: str ):
self._duplicates_type_panel = QW.QWidget( self )
- choices = [ ( HC.duplicate_type_string_lookup[ t ], t ) for t in ( HC.DUPLICATE_MEMBER, HC.DUPLICATE_ALTERNATE, HC.DUPLICATE_FALSE_POSITIVE, HC.DUPLICATE_POTENTIAL ) ]
+ choice_tuples = [ ( HC.duplicate_type_string_lookup[ t ], t ) for t in ( HC.DUPLICATE_MEMBER, HC.DUPLICATE_ALTERNATE, HC.DUPLICATE_FALSE_POSITIVE, HC.DUPLICATE_POTENTIAL ) ]
- self._duplicate_type = ClientGUICommon.BetterRadioBox( self._duplicates_type_panel, choices = choices )
+ self._duplicate_type = ClientGUICommon.BetterRadioBox( self._duplicates_type_panel, choice_tuples )
vbox = QP.VBoxLayout()
@@ -465,12 +465,12 @@ def __init__( self, parent: QW.QWidget, shortcuts_name: str ):
self._seek_panel = QW.QWidget( self )
- choices = [
+ choice_tuples = [
( 'back', -1 ),
( 'forwards', 1 )
]
- self._seek_direction = ClientGUICommon.BetterRadioBox( self._seek_panel, choices = choices )
+ self._seek_direction = ClientGUICommon.BetterRadioBox( self._seek_panel, choice_tuples )
self._seek_duration_s = ClientGUICommon.BetterSpinBox( self._seek_panel, max=3599, width = 60 )
self._seek_duration_ms = ClientGUICommon.BetterSpinBox( self._seek_panel, max=999, width = 60 )
@@ -494,9 +494,9 @@ def __init__( self, parent: QW.QWidget, shortcuts_name: str ):
self._thumbnail_move_panel = QW.QWidget( self )
- choices = [ ( CAC.selection_status_enum_to_str_lookup[ s ], s ) for s in [ CAC.SELECTION_STATUS_NORMAL, CAC.SELECTION_STATUS_SHIFT ] ]
+ choice_tuples = [ ( CAC.selection_status_enum_to_str_lookup[ s ], s ) for s in [ CAC.SELECTION_STATUS_NORMAL, CAC.SELECTION_STATUS_SHIFT ] ]
- self._selection_status = ClientGUICommon.BetterRadioBox( self._thumbnail_move_panel, choices = choices )
+ self._selection_status = ClientGUICommon.BetterRadioBox( self._thumbnail_move_panel, choice_tuples )
self._selection_status.SetValue( CAC.SELECTION_STATUS_NORMAL )
@@ -556,9 +556,9 @@ def __init__( self, parent: QW.QWidget, shortcuts_name: str ):
self._file_command_target_panel = QW.QWidget( self )
- choices = [ ( CAC.file_command_target_enum_to_str_lookup[ file_command_target ], file_command_target ) for file_command_target in ( CAC.FILE_COMMAND_TARGET_SELECTED_FILES, CAC.FILE_COMMAND_TARGET_FOCUSED_FILE ) ]
+ choice_tuples = [ ( CAC.file_command_target_enum_to_str_lookup[ file_command_target ], file_command_target ) for file_command_target in ( CAC.FILE_COMMAND_TARGET_SELECTED_FILES, CAC.FILE_COMMAND_TARGET_FOCUSED_FILE ) ]
- self._file_command_target = ClientGUICommon.BetterRadioBox( self._file_command_target_panel, choices = choices )
+ self._file_command_target = ClientGUICommon.BetterRadioBox( self._file_command_target_panel, choice_tuples )
self._file_command_target.SetValue( CAC.FILE_COMMAND_TARGET_SELECTED_FILES )
diff --git a/hydrus/client/gui/widgets/ClientGUIColourPicker.py b/hydrus/client/gui/widgets/ClientGUIColourPicker.py
index e2e965bf7..b9e9c8762 100644
--- a/hydrus/client/gui/widgets/ClientGUIColourPicker.py
+++ b/hydrus/client/gui/widgets/ClientGUIColourPicker.py
@@ -1,5 +1,6 @@
import re
+import qtpy
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
@@ -65,10 +66,23 @@ def EditColour( win: QW.QWidget, colour: QG.QColor ):
old_stylesheet = ClientGUIStyle.CURRENT_STYLESHEET
- # OK, this is a legit bug in Qt6. if QWidget::item:hover is set in the QSS, even as an empty block, the colour-picker gradient square thing will update on normal mouse moves, not just drags, lmao
+ try:
+
+ qt_version_tuple = tuple( map( int, qtpy.QT_VERSION.split( '.' ) ) ) # 6.6.0 -> ( 6, 6, 0 )
+
+ qt_version_is_dangerzone = qt_version_tuple[0] == 6 and qt_version_tuple[1] < 6
+
+ except:
+
+ qt_version_is_dangerzone = False # who knows what is going on, but let's not spam stylesheets on this crazy Qt!?
+
+
+ # OK, this is a legit bug in Qt6, all I know is <6.6.0 but it might have been fixed earlier
+ # I think it is this one https://bugreports.qt.io/browse/QTBUG-115516
+ # if QWidget::item:hover is set in the QSS, even as an empty block, the colour-picker gradient square thing will update on normal mouse moves, not just drags, lmao
# so we do this laggy nonsense, but it fixes it
- if QtInit.WE_ARE_QT6 and ( 'QWidget::item:hover' in ClientGUIStyle.CURRENT_STYLESHEET or 'QWidget:item:hover' in ClientGUIStyle.CURRENT_STYLESHEET ):
+ if qt_version_is_dangerzone and ( 'QWidget::item:hover' in ClientGUIStyle.CURRENT_STYLESHEET or 'QWidget:item:hover' in ClientGUIStyle.CURRENT_STYLESHEET ):
new_stylesheet = old_stylesheet.replace( 'QWidget::item:hover', 'QWidget::fugg:fugg' ).replace( 'QWidget:item:hover', 'QWidget:fugg:fugg' )
diff --git a/hydrus/client/gui/widgets/ClientGUICommon.py b/hydrus/client/gui/widgets/ClientGUICommon.py
index cabbc99b1..b6405d691 100644
--- a/hydrus/client/gui/widgets/ClientGUICommon.py
+++ b/hydrus/client/gui/widgets/ClientGUICommon.py
@@ -621,37 +621,117 @@ def eventFilter( self, watched, event ):
return False
-class BetterRadioBox( QP.RadioBox ):
+class BetterRadioBox( QW.QFrame ):
- def __init__( self, *args, **kwargs ):
+ radioBoxChanged = QC.Signal()
+
+ def __init__( self, parent, choice_tuples, vertical = False ):
+
+ QW.QFrame.__init__( self, parent )
+
+ self.setFrameStyle( QW.QFrame.Box | QW.QFrame.Raised )
+
+ if vertical:
+
+ self.setLayout( QP.VBoxLayout() )
+
+ else:
+
+ self.setLayout( QP.HBoxLayout() )
+
+
+ self._radio_buttons = []
+ self._buttons_to_data = {}
+
+ for ( text, data ) in choice_tuples:
+
+ radiobutton = QW.QRadioButton( text, self )
+
+ self._radio_buttons.append( radiobutton )
+
+ self._buttons_to_data[ radiobutton ] = data
+
+ radiobutton.clicked.connect( self.radioBoxChanged )
+
+ self.layout().addWidget( radiobutton )
+
- self._indices_to_data = { i : data for ( i, ( s, data ) ) in enumerate( kwargs[ 'choices' ] ) }
+ if vertical and len( self._radio_buttons ):
+
+ self._radio_buttons[0].setChecked( True )
+
+ elif len( self._radio_buttons ) > 0:
+
+ self._radio_buttons[-1].setChecked( True )
+
- kwargs[ 'choices' ] = [ s for ( s, data ) in kwargs[ 'choices' ] ]
+
+ def _GetCurrentChoiceWidget( self ):
- QP.RadioBox.__init__( self, *args, **kwargs )
+ for choice in self._radio_buttons:
+
+ if choice.isChecked():
+
+ return choice
+
+
+
+ return None
def GetValue( self ):
- index = self.GetCurrentIndex()
+ for ( button, data ) in self._buttons_to_data.items():
+
+ if button.isChecked():
+
+ return data
+
+
- return self._indices_to_data[ index ]
+ raise Exception( 'No button selected!' )
- def SetValue( self, data ):
+ def setFocus( self, reason ):
- for ( i, d ) in self._indices_to_data.items():
+ for button in self._radio_buttons:
- if d == data:
+ if button.isChecked():
- self.Select( i )
+ button.setFocus( reason )
return
+ QW.QFrame.setFocus( self, reason )
+
+
+ def Select( self, index ):
+
+ try:
+
+ radio_button = self._radio_buttons[ index ]
+
+ data = self._buttons_to_data[ radio_button ]
+
+ self.SetValue( data )
+
+ except:
+
+ pass
+
+
+ def SetValue( self, select_data ):
+
+ for ( button, data ) in self._buttons_to_data.items():
+
+ button.setChecked( data == select_data )
+
+
+
+
class BetterStaticText( QP.EllipsizedLabel ):
def __init__( self, parent, label = None, tooltip_label = False, **kwargs ):
@@ -1483,6 +1563,7 @@ def _ManageFavourites( self ):
+
class StaticBox( QW.QFrame ):
def __init__( self, parent, title ):
@@ -1525,46 +1606,7 @@ def SetTitle( self, title ):
self._title_st.setText( title )
-class RadioBox( StaticBox ):
-
- def __init__( self, parent, title, choice_pairs, initial_index = None ):
-
- StaticBox.__init__( self, parent, title )
-
- self._indices_to_radio_buttons = {}
- self._radio_buttons_to_data = {}
-
- for ( index, ( text, data ) ) in enumerate( choice_pairs ):
-
- radio_button = QW.QRadioButton( text, self )
-
- self.Add( radio_button, CC.FLAGS_EXPAND_PERPENDICULAR )
-
- self._indices_to_radio_buttons[ index ] = radio_button
- self._radio_buttons_to_data[ radio_button ] = data
-
-
- if initial_index is not None and initial_index in self._indices_to_radio_buttons: self._indices_to_radio_buttons[ initial_index ].setChecked( True )
-
-
- def GetSelectedClientData( self ):
-
- for radio_button in list(self._radio_buttons_to_data.keys()):
-
- if radio_button.isDown(): return self._radio_buttons_to_data[ radio_button]
-
-
-
- def SetSelection( self, index ):
-
- self._indices_to_radio_buttons[ index ].setChecked( True )
-
-
- def SetString( self, index, text ):
-
- self._indices_to_radio_buttons[ index ].setText( text )
-
-
+
class TextCatchEnterEventFilter( QC.QObject ):
def __init__( self, parent, callable, *args, **kwargs ):
diff --git a/hydrus/client/gui/widgets/ClientGUINumberTest.py b/hydrus/client/gui/widgets/ClientGUINumberTest.py
index fb038c94d..e3124d896 100644
--- a/hydrus/client/gui/widgets/ClientGUINumberTest.py
+++ b/hydrus/client/gui/widgets/ClientGUINumberTest.py
@@ -39,7 +39,7 @@ def __init__( self, parent, allowed_operators = None, max = 200000, unit_string
- self._operator = QP.DataRadioBox( self, choice_tuples = choice_tuples )
+ self._operator = ClientGUICommon.BetterRadioBox( self, choice_tuples )
self._value = self._GenerateValueWidget( max )
diff --git a/hydrus/client/importing/ClientImportGallery.py b/hydrus/client/importing/ClientImportGallery.py
index 304a1bf29..4dbe55536 100644
--- a/hydrus/client/importing/ClientImportGallery.py
+++ b/hydrus/client/importing/ClientImportGallery.py
@@ -816,9 +816,11 @@ def SetFileImportOptions( self, file_import_options: FileImportOptions.FileImpor
with self._lock:
- if file_import_options.DumpToString() != self._file_import_options.DumpToString():
-
- self._file_import_options = file_import_options
+ change_made = file_import_options.DumpToString() != self._file_import_options.DumpToString()
+
+ self._file_import_options = file_import_options
+
+ if change_made:
self._SerialisableChangeMade()
@@ -849,9 +851,13 @@ def SetNoteImportOptions( self, note_import_options: NoteImportOptions.NoteImpor
with self._lock:
- if note_import_options.DumpToString() != self._note_import_options.DumpToString():
+ change_made = note_import_options.DumpToString() != self._note_import_options.DumpToString()
+
+ self._note_import_options = note_import_options
+
+ if change_made:
- self._note_import_options = note_import_options
+ self._SerialisableChangeMade()
@@ -860,9 +866,13 @@ def SetTagImportOptions( self, tag_import_options: TagImportOptions.TagImportOpt
with self._lock:
- if tag_import_options.DumpToString() != self._tag_import_options.DumpToString():
+ change_made = tag_import_options.DumpToString() != self._tag_import_options.DumpToString()
+
+ self._tag_import_options = tag_import_options
+
+ if change_made:
- self._tag_import_options = tag_import_options
+ self._SerialisableChangeMade()
diff --git a/hydrus/client/importing/ClientImportWatchers.py b/hydrus/client/importing/ClientImportWatchers.py
index fd42e8583..2ddd8d9d8 100644
--- a/hydrus/client/importing/ClientImportWatchers.py
+++ b/hydrus/client/importing/ClientImportWatchers.py
@@ -554,9 +554,11 @@ def SetFileImportOptions( self, file_import_options ):
with self._lock:
- if file_import_options.DumpToString() != self._file_import_options.DumpToString():
-
- self._file_import_options = file_import_options
+ changes_made = file_import_options.DumpToString() != self._file_import_options.DumpToString()
+
+ self._file_import_options = file_import_options
+
+ if changes_made:
self._SerialisableChangeMade()
@@ -582,9 +584,11 @@ def SetNoteImportOptions( self, note_import_options ):
with self._lock:
- if note_import_options.DumpToString() != self._note_import_options.DumpToString():
-
- self._note_import_options = note_import_options
+ changes_made = note_import_options.DumpToString() != self._note_import_options.DumpToString()
+
+ self._note_import_options = note_import_options
+
+ if changes_made:
self._SerialisableChangeMade()
@@ -595,9 +599,11 @@ def SetTagImportOptions( self, tag_import_options ):
with self._lock:
- if tag_import_options.DumpToString() != self._tag_import_options.DumpToString():
-
- self._tag_import_options = tag_import_options
+ changes_made = tag_import_options.DumpToString() != self._tag_import_options.DumpToString()
+
+ self._tag_import_options = tag_import_options
+
+ if changes_made:
self._SerialisableChangeMade()
@@ -1688,9 +1694,11 @@ def SetCheckerOptions( self, checker_options: ClientImportOptions.CheckerOptions
with self._lock:
- if checker_options.DumpToString() != self._checker_options.DumpToString():
-
- self._checker_options = checker_options
+ change_made = checker_options.DumpToString() != self._checker_options.DumpToString()
+
+ self._checker_options = checker_options
+
+ if change_made:
self._UpdateNextCheckTime()
diff --git a/hydrus/client/networking/ClientNetworkingJobs.py b/hydrus/client/networking/ClientNetworkingJobs.py
index ef58663c5..d5a8be5fd 100644
--- a/hydrus/client/networking/ClientNetworkingJobs.py
+++ b/hydrus/client/networking/ClientNetworkingJobs.py
@@ -153,7 +153,7 @@ class NetworkJob( object ):
IS_HYDRUS_SERVICE = False
IS_IPFS_SERVICE = False
- def __init__( self, method: str, url: str, body = None, referral_url = None, temp_path = None ):
+ def __init__( self, method: str, url: str, body = None, referral_url = None, temp_path = None, file_body_path = None ):
if body is not None and isinstance( body, str ):
@@ -179,6 +179,7 @@ def __init__( self, method: str, url: str, body = None, referral_url = None, tem
self._referral_url = referral_url
self._actual_fetched_url = self._url
self._temp_path = temp_path
+ self._file_body_path = file_body_path
self._response_server_header = None
self._response_last_modified = None
@@ -798,11 +799,25 @@ def _SendRequestAndGetResponse( self ) -> requests.Response:
( connect_timeout, read_timeout ) = self._GetTimeouts()
- response = session.request( method, url, data = data, files = files, headers = headers, stream = True, timeout = ( connect_timeout, read_timeout ) )
+ if self._file_body_path is not None:
+
+ with open( self._file_body_path, 'rb' ) as f:
+
+ response = session.request( method, url, data = f, headers = headers, stream = True, timeout = ( connect_timeout, read_timeout ) )
+
+
+ else:
+
+ response = session.request( method, url, data = data, files = files, headers = headers, stream = True, timeout = ( connect_timeout, read_timeout ) )
+
with self._lock:
- if self._body is not None:
+ if self._file_body_path is not None:
+
+ self._ReportDataUsed( os.path.getsize( self._file_body_path ) )
+
+ elif self._body is not None:
self._ReportDataUsed( len( self._body ) )
@@ -2098,11 +2113,11 @@ class NetworkJobHydrus( NetworkJob ):
WILLING_TO_WAIT_ON_INVALID_LOGIN = False
IS_HYDRUS_SERVICE = True
- def __init__( self, service_key, method, url, body = None, referral_url = None, temp_path = None ):
+ def __init__( self, service_key, method, url, body = None, referral_url = None, temp_path = None, file_body_path = None ):
self._service_key = service_key
- NetworkJob.__init__( self, method, url, body = body, referral_url = referral_url, temp_path = temp_path )
+ NetworkJob.__init__( self, method, url, body = body, referral_url = referral_url, temp_path = temp_path, file_body_path = file_body_path )
def _GenerateNetworkContexts( self ):
diff --git a/hydrus/client/networking/ClientNetworkingURLClass.py b/hydrus/client/networking/ClientNetworkingURLClass.py
index 2ad6d3898..38007d30f 100644
--- a/hydrus/client/networking/ClientNetworkingURLClass.py
+++ b/hydrus/client/networking/ClientNetworkingURLClass.py
@@ -1253,6 +1253,10 @@ def GetSortingComplexityKey( self ):
# I used to do gallery first, then post, then file, but it ultimately was unhelpful in some situations and better handled by strict component/parameter matching
+ # note, we have added a bunch of extra params and stuff here, and here's another one, 2024-05:
+ # adding domain length so that api.vxtwitter.com will match before vxtwitter.com. subdomains before domains!
+
+ len_domain = len( self._netloc )
num_required_path_components = len( [ 1 for ( string_match, default ) in self._path_components if default is None ] )
num_total_path_components = len( self._path_components )
num_required_parameters = len( [ 1 for parameter in self._parameters if not parameter.HasDefaultValue() ] )
@@ -1267,7 +1271,7 @@ def GetSortingComplexityKey( self ):
len_example_url = len( self._example_url )
- return ( num_required_path_components, num_total_path_components, num_required_parameters, num_total_parameters, len_example_url )
+ return ( len_domain, num_required_path_components, num_total_path_components, num_required_parameters, num_total_parameters, len_example_url )
def GetURLBooleans( self ):
diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py
index 561c292a1..5cc233361 100644
--- a/hydrus/core/HydrusConstants.py
+++ b/hydrus/core/HydrusConstants.py
@@ -105,7 +105,7 @@
# Misc
NETWORK_VERSION = 20
-SOFTWARE_VERSION = 575
+SOFTWARE_VERSION = 576
CLIENT_API_VERSION = 64
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
diff --git a/hydrus/test/TestHydrusServer.py b/hydrus/test/TestHydrusServer.py
index c991ea861..9f6b28297 100644
--- a/hydrus/test/TestHydrusServer.py
+++ b/hydrus/test/TestHydrusServer.py
@@ -168,21 +168,16 @@ def _test_file_repo( self, service ):
self.assertEqual( response, EXAMPLE_FILE )
- #
-
try: os.remove( path )
except: pass
- path = os.path.join( HC.STATIC_DIR, 'hydrus.png' )
+ #
- with open( path, 'rb' ) as f:
-
- file_bytes = f.read()
-
+ path = os.path.join( HC.STATIC_DIR, 'hydrus.png' )
HG.test_controller.ClearWrites( 'file' )
- service.Request( HC.POST, 'file', { 'file' : file_bytes } )
+ service.Request( HC.POST, 'file', file_body_path = path )
written = HG.test_controller.GetWrite( 'file' )
diff --git a/static/default/parsers/danbooru file page parser - get webm ugoira.png b/static/default/parsers/danbooru file page parser - get webm ugoira.png
index 31bd0de3a..c0675e7b2 100644
Binary files a/static/default/parsers/danbooru file page parser - get webm ugoira.png and b/static/default/parsers/danbooru file page parser - get webm ugoira.png differ
diff --git a/static/default/parsers/danbooru file page parser.png b/static/default/parsers/danbooru file page parser.png
index 2d055c668..d04510e14 100644
Binary files a/static/default/parsers/danbooru file page parser.png and b/static/default/parsers/danbooru file page parser.png differ