Version 399

This commit is contained in:
Hydrus Network Developer 2020-05-27 16:27:52 -05:00
parent 54b7ebf02e
commit 8d7fd30334
48 changed files with 926 additions and 346 deletions

View File

@ -58,12 +58,9 @@
<h3 id="finding_duplicates">finding duplicates</h3>
<p><i>system:similar_to</i> lets you run the duplicates processing page's searches manually. You can either insert the hash and hamming distance manually, or you can launch these searches automatically from the thumbnail <i>right-click->find similar files</i> menu. For example:</p>
<p><img src="similar_gununu.png" /></p>
<h3 id="pil_errors">PIL errors</h3>
<p>At some point, you will probably encounter a PIL error when importing a file. PIL is the Python Image Library, the code I use to manipulate image files. Some files are kooky, and just won't load with it. I can't fix these errors, since PIL is not mine. Just gotta deal with it.</p>
<p>If the PIL error'ing file is one you particularly care about, I suggest you import it into photoshop or similar and save it again. Photoshop should be clever enough to parse the file's weirdness, and then it'll hopefully save again to a simpler format that PIL, and hence the client, will be able to understand.</p>
<h3 id="busted_gifs">busted up gifs</h3>
<p>Animated gifs are a real pain in the neck. The standard permits odd palettes and colourspaces, and PIL has a hard time parsing it all. I try my best to compensate, but some still break for reasons I can't fathom. I have fixed most of this on Windows by moving to OpenCV for gif rendering, but it still affects Linux and macOS.</p>
<p>So, some gifs will have a coloured first frame but grey frames thereafter; or they will have odd washy noise all over; or they will just be black. The file isn't broken, the client is just looking at it wrong.</p>
<h3 id="file_import_errors">truncated/malformed file import errors</h3>
<p>Some files, even though they seem ok in another program, will not import to hydrus. This is usually because they file has some 'truncated' or broken data, probably due to a bad upload or storage at some point in its internet history. While sophisticated external programs can usually patch the error (often rendering the bottom lines of a jpeg as grey, for instance), hydrus is not so clever. Please feel free to send or link me, hydrus developer, to these files, so I can check them out on my end and try to fix support.</p>
<p>If the file is one you particularly care about, the easiest solution is to open it in photoshop or gimp and save it again. Those programs should be clever enough to parse the file's weirdness, and then make a nice clean saved file when it exports. That new file should be importable to hydrus.</p>
<h3 id="password">setting a password</h3>
<p>the client offers a very simple password system, enough to keep out noobs. You can set it at <i>database->set a password</i>. It will thereafter ask for the password every time you start the program, and will not open without it. However none of the database is encrypted, and someone with enough enthusiasm or a tool and access to your computer can still very easily see what files you have. The password is mainly to stop idle snoops checking your images if you are away from your machine.</p>
</div>

View File

@ -8,6 +8,57 @@
<div class="content">
<h3>changelog</h3>
<ul>
<li><h3>version 399</h3></li>
<ul>
<li>improvements:</li>
<li>the media viewer and thumbnail _right-click->manage_ menus now have a _viewing stats->clear_ action, which does a straight-up delete of all viewing stats record for the selected files. 'edit' will be added to this menu in future</li>
<li>extended the tag autocomplete options with a checkbox to allow 'namespace:' to match all tags, without the explicit asterisk</li>
<li>tag autocomplete options now permit namespace searches if the 'search namespaces into full tags' option is set</li>
<li>the tag autocomplete options panel now disables and checks the namespace checkboxes when one option overrules another</li>
<li>cleaned up some tag search logic to recognise and deal with 'namespace:' as a query</li>
<li>added some more unit tests for tag autocomplete options</li>
<li>the html and json parsing formulae now support negative indexing, to select the nth last item from a list</li>
<li>extended the '1 -> "1st"' ordinal string conversion code to deal with negative indices</li>
<li>the 'hide tag' taglist menu actions are now wrapped in yes/no dialogs</li>
<li>reduced the activation-to-click-accept time that the shortcuts handler uses to ignore activating clicks from 100ms to 17ms</li>
<li>clicking the media viewer's top hover window's zoom buttons now forces the 'media viewer center' zoom centerpoint, so if you have the mouse centerpoint set, it won't zoom around the button where you are clicking!</li>
<li>added a simple 8chan.moe watcher to the defaults, all users will get it on update</li>
<li>the default bandwidth rules for download pages, subs, and watchers are now more liberal. only new users will get these. various improvements to db and ui update pipeline mean the enforced breaks are less needed</li>
<li>when a manage tags dialog moves to another media, if it has a 'recent tags' suggestion list with a selection, the selection now resets to the top item in the list</li>
<li>the mpv player now tracks when a video is fully loaded and only reports seek bar info and allows seeks when this is so (this should fix some seekbar errors on broken/slow-loading vids)</li>
<li>added 'undelete_file' to media shortcut commands</li>
<li>file delete and undelete are no longer hardcoded in the media viewer and media thumbnail grid. these actions are now handled entirely in the media shortcut set, and added to all clients by default (this defaults to (shift +) delete key, and also backspace on macos, so likely no changes)</li>
<li>ctrl+mouse wheel is no longer hardcoded to zoom in the media browser. these actions are now handled entirely in the 'all' media viewer shortcut set (this defaults to ctrl+wheel or +/-, so likely no changes)</li>
<li>deleted some old shortcut processing code</li>
<li>tightened up some update timers to better halt work while the client is minimised to system tray. this _may_ improve some users' restore hanging issues</li>
<li>as Qt is happier than wx about making pages on a non-visible client, subscriptions and various url import operations are now permitted to create pages while the client is minimised to taskbar or system tray. if this applies to your situation, please let me know how you get on here, as this may relieve some restore hanging as the pending new-file jobs are no longer queued up</li>
<li>.</li>
<li>fixes:</li>
<li>clicks on hover window greyspace should no longer propagate up to the media viewer. this was causing weird archive/delete filter actions</li>
<li>mouse scroll on hover window taglist should no longer propagate up to the media viewer when the taglist has no more to scroll in that direction</li>
<li>fixed an issue that meant preview windows were initialising about twenty pixels too short for the first page loaded in a session, and also pages created within nested page of pages. also cleaned up some logic for unusual situations like hidden preview windows. one more cycle of closing and reopening the client will fix the option value here</li>
<li>cleaned and unified some page sash setting code, also improving the 'hide preview window' option reliability for advanced actions</li>
<li>fixed a bug that meant file viewtime was still being recorded on the duplicate filter when the special exception option was off</li>
<li>reduced some file viewtime manager overhead</li>
<li>fixed an issue with database repair code when local_tags_cache is missing</li>
<li>fixed an issue updating a very old db not recognising that local_tags_cache does not yet exist for proper reason and then trying to repair it before update code runs</li>
<li>fixed the annoying issue introduced in the recent string match overhaul where a 'fixed character' string match edit panel would not want to ok if the (now hidden) example string input did not have the same fixed char data. it now validates no matter what is in the hidden input</li>
<li>potentially important parsing fix: JSON parsing, when set to get strings, no longer converts a 'null' value to 'None'</li>
<li>the JSON parsing formula now allows you to select the nth indexed item of an Object (a JSON key->value dictionary). due to technical limitations, it alphabetises the keys, not selecting them as-is in the JSON itself</li>
<li>images that do not load in PIL no longer cause mime exceptions if they are run through the decompression bomb check</li>
<li>.</li>
<li>misc:</li>
<li>boosted the values of the decompression bomb check anyway, to reduce false positives. it generally now has a problem with images with a bmp > 1GB memory</li>
<li>by default, new file import options now start with decompression bombs allowed. this option is being reduced to a stopgap for users with less memory</li>
<li>'MimeException' is renamed to 'UnsupportedFileException'</li>
<li>added 'DamagedOrUnusualFileException' to handle normally supported files that cannot be parsed or loaded</li>
<li>'SizeException' is split into 'TagSizeException' and 'FileSizeException'</li>
<li>improved some file exception inheritance</li>
<li>removed the 'experimental' label from sub-gallery page url type in parsing system</li>
<li>updated some advanced help regarding bad files</li>
<li>misc help updates</li>
<li>updated cloudscraper to 1.2.40</li>
</ul>
<li><h3>version 398</h3></li>
<ul>
<li>new tag search options:</li>

View File

@ -10,7 +10,7 @@
<p>If any of this is confusing, some users are building video guides for hydrus to help new users <a href="https://github.com/CuddleBear92/Hydrus-guides">here</a>!</p>
<h3 id="downloading">downloading</h3>
<p>You can get the latest release at <a href="https://github.com/hydrusnetwork/hydrus/releases">my github releases page</a>.</p>
<p>I try to release a new version every Wednesday by 8pm EST and write an accompanying post on <a href="http://hydrus.tumblr.com/">my tumblr</a> and a sticky on <a href="https://8ch.net/hydrus/index.html">my 8chan board</a>.</p>
<p>I try to release a new version every Wednesday by 8pm EST and write an accompanying post on <a href="http://hydrus.tumblr.com/">my tumblr</a> and a sticky on <a href="https://8kun.top/hydrus/index.html">my 8kun board</a>.</p>
<h3 id="installing">installing</h3>
<p class="warning">The hydrus releases are 64-bit only. If you are a python expert, there is the slimmest chance you'll be able to get it running from source on a 32-bit machine, but it would be easier just to find a newer computer to run it on.</p>
<p>for Windows:</p>
@ -27,12 +27,13 @@
<p>for Linux:</p>
<ul>
<li>Get the .tag.gz. Extract it somewhere useful and create shortcuts to 'client' and 'server' as you like. I build on Ubuntu, so if you run something else, compatibility is hit and miss.</li>
<li>Or try <a href="wine.html">running the Windows version in wine</a>.</li>
<li>If you use Arch Linux, you can check out the AUR package a user maintains <a href="https://aur.archlinux.org/packages/hydrus/">here</a>.</li>
<li>If you have problems running the Ubuntu build, users with some python experience generally find running from source works well.</li>
<li>You can also try <a href="wine.html">running the Windows version in wine</a>.</li>
</ul>
<p>from source:</p>
<ul>
<li>If you know Python, you can <a href="running_from_source.html">run from source</a>.</li>
<li>If you have some python experience, you can <a href="running_from_source.html">run from source</a>.</li>
</ul>
<p>Hydrus stores all its data&#x2014;options, files, subscriptions, <i>everything</i>&#x2014;entirely inside its own directory. You can extract it to a usb stick, move it from one place to another, have multiple installs for multiple purposes, wrap it all up inside a truecrypt volume, whatever you like. The .exe installer writes some unavoidable uninstall registry stuff to Windows, but the 'installed' client itself will run fine if you manually move it.</p>
<p><span class="warning">However, for macOS users:</span> the Hydrus App is <b>non-portable</b> and puts your database in ~/Library/Hydrus (i.e. /Users/[You]/Library/Hydrus). You can update simply by replacing the old App with the new, but if you wish to backup, you should be looking at ~/Library/Hydrus, not the App itself.</p>

View File

@ -53,7 +53,7 @@
<p>Subsequent syncs are more complicated. It ideally 'stops' searching when it reaches files it saw in a previous sync, but if it comes across new files mixed in with the old, it will search a bit deeper. It is not foolproof, and if a file gets tagged very late and ends up a hundred deep in the search, it will probably be missed. There is no good and computationally cheap way at present to resolve this problem, but thankfully it is very rare.</p>
<p>If the sub keeps finding apparently new URLs on a regular sync, it will stop upon hitting its 'periodic file limit', which is also usually 100. This is a safety stopgap, and usually happens when the site's URL format itself has changed, which may or may not require attention from you to figure out. If a user just went nuts and uploaded 500 new files to that tag in one day, you'll have a 'gap' in your sub sync, which you'll want to fill in with a manual download. If a sub hits its periodic file limit and thinks something like this happened, it will give you a popup explaining the situation.</p>
<p>Please note that subscriptions only keep up with new content. They cannot search backwards in time in order to 'fill out' a search, nor can they fill in gaps. <span class="warning">Do not change the file limits or check times to try to make this happen.</span> If you want to ensure complete sync with all existing content for a particular search, please use the manual downloader.</p>
<p>In practise, most subs only need to check the first page of a gallery since only the first two or three urls are new.</p>
<p>In practice, most subs only need to check the first page of a gallery since only the first two or three urls are new.</p>
<h3 id="merging_and_splitting">I put character queries in my artist sub, and now things are all mixed up</h3>
<p>On the main subscription dialog, there are 'merge' and 'split' buttons. These are powerful, but they will walk you through the process of pulling queries out of a sub and merging them back into a different one. Only subs that use the same download source can be merged. Give them a go, and if it all goes wrong, just hit the cancel button.</p>

View File

@ -17,7 +17,7 @@
<a class="screenshot" href="screenshot_booru.png" title="You can run your own (simple!) booru"><img src="screenshot_booru_thumb.png" /></a>
<a class="screenshot" href="screenshot_advanced_autocomplete.png" title="The client can get complicated if you want it to. This screenshot shows a tag sibling, where one tag is immediately swapped with another, and a non-local search, where results that are known but not on the computer are shown."><img src="screenshot_advanced_autocomplete_thumb.png" /></a>
<h3>hydrus help</h3>
<p>Although I try to make hydrus's interface simple, some of the things it does are quite complicated. Please read the introduction and skim the simple getting started guides at the least. If you like, you can revisit the more complicated topics later, once you are experienced in the basics.</p>
<p>Although I try to make hydrus's interface simple, some of the things it does are quite complicated. Please read how to update/backup and skim the first getting started guides at the least. If you like, you can revisit the more complicated topics later, once you are experienced in the basics.</p>
<p>Keeping the help up to date is a constant battle. If you discover something really does not match the program, or is otherwise confusing or not well worded, please <a href="contact.html">let me know</a>.</p>
<ul>
<li><h3>starting out</h3></li>

View File

@ -9,7 +9,7 @@
<h3>hydrus is cpu and hdd hungry</h3>
<p>The hydrus client manages a lot of complicated data and gives you a lot of power over it. To add millions of files and tags to its database, and then to perform difficult searches over that information, it needs to use a lot of CPU time and hard drive time--sometimes in small laggy blips, and occasionally in big 100% CPU chunks. I don't put training wheels or limiters on the software either, so if you search for 300,000 files, the client will try to fetch that many.</p>
<p>In general, the client works best on snappy computers with low-latency hard drives where it does not have to constantly compete with other CPU- or HDD- heavy programs. Running hydrus on your games computer is no problem at all, but you should have it set to not start a big job while your CPU is otherwise busy so your games can run freely. Similarly, if you run two clients on the same computer, you should have them set to work at different times, because if they both try to process 500,000 tags at once on the same hard drive, they will each slow to a crawl.</p>
<p>Keeping your HDDs defragged is very important, and good practise for all your programs anyway. Make sure you know what this is and that you do it. I use PerfectDisk. O&O Defrag is also good.</p>
<p>Keeping your HDDs defragged is very important, and good practice for all your programs anyway. Make sure you know what this is and that you do it. I use PerfectDisk. O&O Defrag is also good.</p>
<h3>maintenance and processing</h3>
<p>I have attempted to offload most of the background maintenance of the client (which typically means repository processing and internal database defragging) to time when you are not using the client. This can either be 'idle time' or 'shutdown time'. The calculations for what these exactly mean are customisable in <i>file->options->maintenance and processing</i>.</p>
<p>If you run a quick computer, you likely don't have to change any of these options. Repositories will synchronise and the database will stay fairly optimal without you even noticing the work that is going on. This is especially true if you leave your client on all the time.</p>
@ -36,4 +36,4 @@
<p>There are several ways to <a href="contact.html">contact me</a>.</p>
</div>
</body>
</html>
</html>

View File

@ -10718,6 +10718,14 @@ class DB( HydrusDB.HydrusDB ):
self._c.execute( 'UPDATE file_viewing_stats SET preview_views = preview_views + ?, preview_viewtime = preview_viewtime + ?, media_views = media_views + ?, media_viewtime = media_viewtime + ? WHERE hash_id = ?;', ( preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta, hash_id ) )
elif action == HC.CONTENT_UPDATE_DELETE:
hashes = row
hash_ids = self._GetHashIds( hashes )
self._c.executemany( 'DELETE FROM file_viewing_stats WHERE hash_id = ?;', ( ( hash_id, ) for hash_id in hash_ids ) )
elif service_type in HC.REAL_TAG_SERVICES:
@ -10730,7 +10738,7 @@ class DB( HydrusDB.HydrusDB ):
tag_id = self._GetTagId( tag )
except HydrusExceptions.SizeException:
except HydrusExceptions.TagSizeException:
continue
@ -10805,7 +10813,7 @@ class DB( HydrusDB.HydrusDB ):
parent_tag_id = self._GetTagId( parent_tag )
except HydrusExceptions.SizeException:
except HydrusExceptions.TagSizeException:
continue
@ -10840,7 +10848,7 @@ class DB( HydrusDB.HydrusDB ):
parent_tag_id = self._GetTagId( parent_tag )
except HydrusExceptions.SizeException:
except HydrusExceptions.TagSizeException:
continue
@ -10874,7 +10882,7 @@ class DB( HydrusDB.HydrusDB ):
parent_tag_id = self._GetTagId( parent_tag )
except HydrusExceptions.SizeException:
except HydrusExceptions.TagSizeException:
continue
@ -10898,7 +10906,7 @@ class DB( HydrusDB.HydrusDB ):
good_tag_id = self._GetTagId( good_tag )
except HydrusExceptions.SizeException:
except HydrusExceptions.TagSizeException:
continue
@ -10933,7 +10941,7 @@ class DB( HydrusDB.HydrusDB ):
good_tag_id = self._GetTagId( good_tag )
except HydrusExceptions.SizeException:
except HydrusExceptions.TagSizeException:
continue
@ -10967,7 +10975,7 @@ class DB( HydrusDB.HydrusDB ):
bad_tag_id = self._GetTagId( bad_tag )
except HydrusExceptions.SizeException:
except HydrusExceptions.TagSizeException:
continue
@ -11552,6 +11560,10 @@ class DB( HydrusDB.HydrusDB ):
self._controller.pub( 'modal_message', job_key )
# need this here to ensure that local_tags_cache exists, as the mappings cache regens use it
# we can't move it up, as it relies on them for its own regen. just make an empty table here to get repopulated
self._CreateDBCaches()
tag_service_ids = self._GetServiceIds( HC.REAL_TAG_SERVICES )
file_service_ids = self._GetServiceIds( HC.AUTOCOMPLETE_CACHE_SPECIFIC_FILE_SERVICES )
@ -11898,7 +11910,10 @@ class DB( HydrusDB.HydrusDB ):
mappings_cache_tables.add( GenerateCombinedFilesMappingsCacheTableName( tag_service_id ).split( '.' )[1] )
mappings_cache_tables.add( 'local_tags_cache' )
if version >= 351:
mappings_cache_tables.add( 'local_tags_cache' )
missing_main_tables = sorted( mappings_cache_tables.difference( existing_cache_tables ) )
@ -12406,7 +12421,7 @@ class DB( HydrusDB.HydrusDB ):
HydrusTags.CheckTagNotEmpty( subtag )
except HydrusExceptions.SizeException:
except HydrusExceptions.TagSizeException:
return False
@ -12431,7 +12446,7 @@ class DB( HydrusDB.HydrusDB ):
HydrusTags.CheckTagNotEmpty( tag )
except HydrusExceptions.SizeException:
except HydrusExceptions.TagSizeException:
return False
@ -14799,6 +14814,85 @@ class DB( HydrusDB.HydrusDB ):
if version == 398:
existing_shortcut_names = self._GetJSONDumpNames( HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUT_SET )
if 'media' in existing_shortcut_names:
try:
media_shortcuts = self._GetJSONDumpNamed( HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUT_SET, dump_name = 'media' )
from hydrus.client.gui import ClientGUIShortcuts
delete_command = ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'delete_file' )
undelete_command = ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'undelete_file' )
for delete_key in ClientGUIShortcuts.DELETE_KEYS_HYDRUS:
shortcut = ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, delete_key, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [] )
if media_shortcuts.GetCommand( shortcut ) is None:
media_shortcuts.SetCommand( shortcut, delete_command )
shortcut = ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, delete_key, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [ ClientGUIShortcuts.SHORTCUT_MODIFIER_SHIFT ] )
if media_shortcuts.GetCommand( shortcut ) is None:
media_shortcuts.SetCommand( shortcut, undelete_command )
self._SetJSONDump( media_shortcuts )
except:
HydrusData.PrintException( e )
message = 'Trying to update the media shortcuts failed! Please let hydrus dev know!'
self.pub_initial_message( message )
if version == 398:
try:
domain_manager = self._GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
domain_manager.Initialise()
#
domain_manager.OverwriteDefaultURLClasses( [ '8chan.moe thread', '8chan.moe thread json api' ] )
#
domain_manager.OverwriteDefaultParsers( [ '8chan.moe thread api parser' ] )
#
domain_manager.TryToLinkURLClassesAndParsers()
#
self._SetJSONDump( domain_manager )
except Exception as e:
HydrusData.PrintException( e )
message = 'Trying to update some parsers failed! Please let hydrus dev know!'
self.pub_initial_message( message )
self._controller.pub( 'splash_set_title_text', 'updated db to v{}'.format( HydrusData.ToHumanInt( version + 1 ) ) )
self._c.execute( 'UPDATE version SET version = ?;', ( version + 1, ) )

View File

@ -343,6 +343,26 @@ def GetDefaultShortcuts():
media = ClientGUIShortcuts.ShortcutSet( 'media' )
delete_command = ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'delete_file' )
undelete_command = ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'undelete_file' )
for delete_key in ClientGUIShortcuts.DELETE_KEYS_HYDRUS:
shortcut = ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, delete_key, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [] )
if media.GetCommand( shortcut ) is None:
media.SetCommand( shortcut, delete_command )
shortcut = ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, delete_key, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [ ClientGUIShortcuts.SHORTCUT_MODIFIER_SHIFT ] )
if media.GetCommand( shortcut ) is None:
media.SetCommand( shortcut, undelete_command )
media.SetCommand( ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, ClientGUIShortcuts.SHORTCUT_KEY_SPECIAL_F4, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'manage_file_ratings' ) )
media.SetCommand( ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, ClientGUIShortcuts.SHORTCUT_KEY_SPECIAL_F3, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'manage_file_tags' ) )
@ -549,10 +569,7 @@ def SetDefaultBandwidthManagerRules( bandwidth_manager ):
rules = HydrusNetworking.BandwidthRules()
# most gallery downloaders need two rqs per file (page and file), remember
rules.AddRule( HC.BANDWIDTH_TYPE_REQUESTS, 300, 200 ) # after that first sample of small files, take it easy
rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 300, 128 * MB ) # after that first sample of big files, take it easy
rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 300, 512 * MB ) # just a careful stopgap
bandwidth_manager.SetRules( ClientNetworkingContexts.NetworkContext( CC.NETWORK_CONTEXT_DOWNLOADER_PAGE ), rules )
@ -563,7 +580,7 @@ def SetDefaultBandwidthManagerRules( bandwidth_manager ):
# most gallery downloaders need two rqs per file (page and file), remember
rules.AddRule( HC.BANDWIDTH_TYPE_REQUESTS, 86400, 800 ) # catch up on a big sub in little chunks every day
rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 86400, 512 * MB ) # catch up on a big sub in little chunks every day
rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 86400, 768 * MB ) # catch up on a big sub in little chunks every day
bandwidth_manager.SetRules( ClientNetworkingContexts.NetworkContext( CC.NETWORK_CONTEXT_SUBSCRIPTION ), rules )
@ -571,10 +588,6 @@ def SetDefaultBandwidthManagerRules( bandwidth_manager ):
rules = HydrusNetworking.BandwidthRules()
rules.AddRule( HC.BANDWIDTH_TYPE_REQUESTS, 300, 100 ) # after that first sample of small files, take it easy
rules.AddRule( HC.BANDWIDTH_TYPE_DATA, 300, 128 * MB ) # after that first sample of big files, take it easy
bandwidth_manager.SetRules( ClientNetworkingContexts.NetworkContext( CC.NETWORK_CONTEXT_WATCHER_PAGE ), rules )
#

View File

@ -1602,7 +1602,7 @@ class FilesMaintenanceManager( object ):
return additional_data
except HydrusExceptions.MimeException:
except HydrusExceptions.UnsupportedFileException:
self._CheckFileIntegrity( media_result, REGENERATE_FILE_DATA_JOB_FILE_INTEGRITY_DATA_URL )

View File

@ -367,7 +367,7 @@ class FileViewingStatsManager( object ):
do_it = True
if viewtime_delta == 'media_duplicates_filter' and not new_options.GetBoolean( 'file_viewing_statistics_active_on_dupe_filter' ):
if viewtype == 'media_duplicates_filter' and not new_options.GetBoolean( 'file_viewing_statistics_active_on_dupe_filter' ):
do_it = False
@ -393,6 +393,16 @@ class FileViewingStatsManager( object ):
return ( preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta )
def _RowMakesChanges( self, row ):
( preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) = row
preview_change = preview_views_delta != 0 or preview_viewtime_delta != 0
media_change = media_views_delta != 0 or media_viewtime_delta != 0
return preview_change or media_change
def _PubSubRow( self, hash, row ):
( preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) = row
@ -450,6 +460,11 @@ class FileViewingStatsManager( object ):
row = self._GenerateViewsRow( viewtype, viewtime_delta )
if not self._RowMakesChanges( row ):
return
if hash not in self._pending_updates:
self._pending_updates[ hash ] = row

View File

@ -507,7 +507,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
exclude_deleted = True
do_not_check_known_urls_before_importing = False
do_not_check_hashes_before_importing = False
allow_decompression_bombs = False
allow_decompression_bombs = True
min_size = None
max_size = None
max_gif_size = 32 * 1048576

View File

@ -481,9 +481,7 @@ def RenderJSONParseRule( rule ):
index = parse_rule
num = index + 1
s = 'get the ' + HydrusData.ConvertIntToPrettyOrdinalString( num ) + ' item'
s = 'get the ' + HydrusData.ConvertIndexToPrettyOrdinalString( index ) + ' item (for Objects, keys sorted)'
elif parse_rule_type == JSON_PARSE_RULE_TYPE_DICT_KEY:
@ -1308,15 +1306,17 @@ class ParseRuleHTML( HydrusSerialisable.SerialisableBase ):
if self._tag_index is not None:
if len( found_nodes ) < self._tag_index + 1:
try:
found_nodes = []
indexed_node = found_nodes[ self._tag_index ]
else:
except IndexError:
found_nodes = [ found_nodes[ self._tag_index ] ]
continue
found_nodes = [ indexed_node ]
elif self._rule_type == HTML_RULE_TYPE_ASCENDING:
@ -1388,9 +1388,7 @@ class ParseRuleHTML( HydrusSerialisable.SerialisableBase ):
else:
num = self._tag_index + 1
s += ' the ' + HydrusData.ConvertIntToPrettyOrdinalString( num )
s += ' the ' + HydrusData.ConvertIndexToPrettyOrdinalString( self._tag_index )
if self._tag_name is not None:
@ -1511,20 +1509,44 @@ class ParseFormulaJSON( ParseFormula ):
elif parse_rule_type == JSON_PARSE_RULE_TYPE_INDEXED_ITEM:
if not isinstance( root, list ):
continue
index = parse_rule
if len( root ) < index + 1:
if isinstance( root, ( list, dict ) ):
if isinstance( root, list ):
list_to_index = root
elif isinstance( root, dict ):
list_to_index = list( root.keys() )
HydrusData.HumanTextSort( list_to_index )
try:
indexed_item = list_to_index[ index ]
except IndexError:
continue
if isinstance( root, list ):
next_roots.append( indexed_item )
elif isinstance( root, dict ):
next_roots.append( root[ indexed_item ] )
else:
continue
next_roots.append( root[ index ] )
elif parse_rule_type == JSON_PARSE_RULE_TYPE_DICT_KEY:
if not isinstance( root, dict ):
@ -1560,9 +1582,12 @@ class ParseFormulaJSON( ParseFormula ):
continue
raw_text = str( root )
raw_texts.append( raw_text )
if root is not None:
raw_text = str( root )
raw_texts.append( raw_text )
elif self._content_to_fetch == JSON_CONTENT_JSON:
@ -3326,6 +3351,11 @@ class StringMatch( StringProcessingStep ):
( self._match_type, self._match_value, self._min_chars, self._max_chars, self._example_string ) = serialisable_info
def GetExampleString( self ):
return self._example_string
def MakesChanges( self ) -> bool:
if self._min_chars is not None or self._max_chars is not None:

View File

@ -2272,11 +2272,22 @@ def SearchTextIsFetchAll( search_text: str ):
return False
def SearchTextIsNamespaceBareFetchAll( search_text: str ):
( namespace, subtag ) = HydrusTags.SplitTag( search_text )
if namespace not in ( '', '*' ) and subtag == '':
return True
return False
def SearchTextIsNamespaceFetchAll( search_text: str ):
( namespace, subtag ) = HydrusTags.SplitTag( search_text )
if namespace not in ( '', '*' ) and subtag in ( '', '*' ):
if namespace not in ( '', '*' ) and subtag == '*':
return True
@ -2335,7 +2346,9 @@ class ParsedAutocompleteText( object ):
( namespace, subtag ) = HydrusTags.SplitTag( text )
if len( subtag ) > 0 and not subtag.endswith( '*' ):
should_have_it = len( namespace ) > 0 or len( subtag ) > 0
if should_have_it and not subtag.endswith( '*' ):
text = '{}*'.format( text )
@ -2406,19 +2419,30 @@ class ParsedAutocompleteText( object ):
search_text = self._GetSearchText( False )
if SubtagIsEmpty( search_text ):
if search_text == '':
return False
( namespace, subtag ) = HydrusTags.SplitTag( search_text )
bnfa = SearchTextIsNamespaceBareFetchAll( search_text )
nfa = SearchTextIsNamespaceFetchAll( search_text )
fa = SearchTextIsFetchAll( search_text )
if not self._tag_autocomplete_options.NamespaceFetchAllAllowed() and SearchTextIsNamespaceFetchAll( search_text ):
bare_ok = self._tag_autocomplete_options.NamespaceBareFetchAllAllowed() or self._tag_autocomplete_options.SearchNamespacesIntoFullTags()
namespace_ok = self._tag_autocomplete_options.NamespaceBareFetchAllAllowed() or self._tag_autocomplete_options.NamespaceFetchAllAllowed() or self._tag_autocomplete_options.SearchNamespacesIntoFullTags()
fa_ok = self._tag_autocomplete_options.FetchAllAllowed()
if bnfa and not bare_ok:
return False
if not self._tag_autocomplete_options.FetchAllAllowed() and SearchTextIsFetchAll( search_text ):
if nfa and not namespace_ok:
return False
if fa and not fa_ok:
return False
@ -2463,7 +2487,7 @@ class ParsedAutocompleteText( object ):
search_text = self._GetSearchText( False )
return SearchTextIsNamespaceFetchAll( search_text )
return SearchTextIsNamespaceFetchAll( search_text ) or SearchTextIsNamespaceBareFetchAll( search_text )
def IsTagSearch( self ):

View File

@ -235,7 +235,7 @@ class TagAutocompleteOptions( HydrusSerialisable.SerialisableBase ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_TAG_AUTOCOMPLETE_OPTIONS
SERIALISABLE_NAME = 'Tag Autocomplete Options'
SERIALISABLE_VERSION = 1
SERIALISABLE_VERSION = 2
def __init__( self, service_key: typing.Optional[ bytes ] = None ):
@ -262,6 +262,7 @@ class TagAutocompleteOptions( HydrusSerialisable.SerialisableBase ):
self._search_namespaces_into_full_tags = False
self._namespace_bare_fetch_all_allowed = False
self._namespace_fetch_all_allowed = False
self._fetch_all_allowed = False
@ -279,6 +280,7 @@ class TagAutocompleteOptions( HydrusSerialisable.SerialisableBase ):
self._override_write_autocomplete_file_domain,
serialisable_write_autocomplete_file_domain,
self._search_namespaces_into_full_tags,
self._namespace_bare_fetch_all_allowed,
self._namespace_fetch_all_allowed,
self._fetch_all_allowed
]
@ -294,6 +296,7 @@ class TagAutocompleteOptions( HydrusSerialisable.SerialisableBase ):
self._override_write_autocomplete_file_domain,
serialisable_write_autocomplete_file_domain,
self._search_namespaces_into_full_tags,
self._namespace_bare_fetch_all_allowed,
self._namespace_fetch_all_allowed,
self._fetch_all_allowed
] = serialisable_info
@ -303,6 +306,37 @@ class TagAutocompleteOptions( HydrusSerialisable.SerialisableBase ):
self._write_autocomplete_file_domain = bytes.fromhex( serialisable_write_autocomplete_file_domain )
def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
if version == 1:
[
serialisable_service_key,
serialisable_write_autocomplete_tag_domain,
override_write_autocomplete_file_domain,
serialisable_write_autocomplete_file_domain,
search_namespaces_into_full_tags,
namespace_fetch_all_allowed,
fetch_all_allowed
] = old_serialisable_info
namespace_bare_fetch_all_allowed = False
new_serialisable_info = [
serialisable_service_key,
serialisable_write_autocomplete_tag_domain,
override_write_autocomplete_file_domain,
serialisable_write_autocomplete_file_domain,
search_namespaces_into_full_tags,
namespace_bare_fetch_all_allowed,
namespace_fetch_all_allowed,
fetch_all_allowed
]
return ( 2, new_serialisable_info )
def FetchAllAllowed( self ):
return self._fetch_all_allowed
@ -345,6 +379,11 @@ class TagAutocompleteOptions( HydrusSerialisable.SerialisableBase ):
return self._write_autocomplete_tag_domain
def NamespaceBareFetchAllAllowed( self ):
return self._namespace_bare_fetch_all_allowed
def NamespaceFetchAllAllowed( self ):
return self._namespace_fetch_all_allowed
@ -365,6 +404,7 @@ class TagAutocompleteOptions( HydrusSerialisable.SerialisableBase ):
override_write_autocomplete_file_domain: bool,
write_autocomplete_file_domain: bytes,
search_namespaces_into_full_tags: bool,
namespace_bare_fetch_all_allowed: bool,
namespace_fetch_all_allowed: bool,
fetch_all_allowed: bool
):
@ -373,6 +413,7 @@ class TagAutocompleteOptions( HydrusSerialisable.SerialisableBase ):
self._override_write_autocomplete_file_domain = override_write_autocomplete_file_domain
self._write_autocomplete_file_domain = write_autocomplete_file_domain
self._search_namespaces_into_full_tags = search_namespaces_into_full_tags
self._namespace_bare_fetch_all_allowed = namespace_bare_fetch_all_allowed
self._namespace_fetch_all_allowed = namespace_fetch_all_allowed
self._fetch_all_allowed = fetch_all_allowed

View File

@ -1923,13 +1923,6 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
url_caught = True
if not self._notebook.HasURLImportPage() and self._CurrentlyMinimisedOrHidden():
self._controller.CallLaterQtSafe(self, 10, self._ImportURL, url, service_keys_to_tags = service_keys_to_tags, destination_page_name = destination_page_name, destination_page_key = destination_page_key, show_destination_page = show_destination_page, allow_watchers = allow_watchers, allow_other_recognised_urls = allow_other_recognised_urls, allow_unrecognised_urls = allow_unrecognised_urls)
return ( url, '"{}" URL was accepted, but it needed a new page and the client is currently minimized or hidden. It is queued to be added once the client is restored.' )
page = self._notebook.GetOrMakeURLImportPage( desired_page_name = destination_page_name, desired_page_key = destination_page_key, select_page = show_destination_page )
if page is not None:
@ -1950,13 +1943,6 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
url_caught = True
if not self._notebook.HasMultipleWatcherPage() and self._CurrentlyMinimisedOrHidden():
self._controller.CallLaterQtSafe(self, 10, self._ImportURL, url, service_keys_to_tags = service_keys_to_tags, destination_page_name = destination_page_name, destination_page_key = destination_page_key, show_destination_page = show_destination_page, allow_watchers = allow_watchers, allow_other_recognised_urls = allow_other_recognised_urls, allow_unrecognised_urls = allow_unrecognised_urls)
return ( url, '"{}" URL was accepted, but it needed a new page and the client is current minimized or hidden. It is queued to be added once the client is restored.' )
page = self._notebook.GetOrMakeMultipleWatcherPage( desired_page_name = destination_page_name, desired_page_key = destination_page_key, select_page = show_destination_page )
if page is not None:
@ -4061,6 +4047,11 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
def TIMEREventAnimationUpdate( self ):
if self._currently_minimised_to_system_tray:
return
try:
windows = list( self._animation_update_windows )
@ -5408,13 +5399,6 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
def PresentImportedFilesToPage( self, hashes, page_name ):
if self._CurrentlyMinimisedOrHidden() and not self._notebook.HasMediaPageName( page_name ):
self._controller.CallLaterQtSafe( self, 10.0, self.PresentImportedFilesToPage, hashes, page_name )
return
self._notebook.PresentImportedFilesToPage( hashes, page_name )
@ -5838,6 +5822,11 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
def REPEATINGUIUpdate( self ):
if self._currently_minimised_to_system_tray:
return
for window in list( self._ui_update_windows ):
if not QP.isValid( window ):

View File

@ -163,8 +163,6 @@ def ReadFetch(
if fetch_from_db:
small_exact_match_search = False
is_explicit_wildcard = parsed_autocomplete_text.IsExplicitWildcard()
small_exact_match_search = ShouldDoExactSearch( strict_search_text ) and not is_explicit_wildcard
@ -361,7 +359,7 @@ def ShouldDoExactSearch( entry_text ):
test_text = entry_text
return len( test_text ) <= autocomplete_exact_match_threshold
return 0 < len( test_text ) <= autocomplete_exact_match_threshold
def WriteFetch( win, job_key, results_callable, parsed_autocomplete_text: ClientSearch.ParsedAutocompleteText, file_service_key: bytes, tag_service_key: bytes, expand_parents: bool, results_cache: ClientSearch.PredicateResultsCache ):

View File

@ -714,11 +714,6 @@ class Canvas( QW.QWidget ):
return media_show_action not in ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW )
def _IShouldCatchShortcutEvent( self, event = None ):
return ClientGUIShortcuts.IShouldCatchShortcutEvent( self, event = event, child_tlw_classes_who_can_pass_up = ( ClientGUICanvasHoverFrames.CanvasHoverFrame, ) )
def _MaintainZoom( self, previous_media ):
if previous_media is None:
@ -1049,7 +1044,7 @@ class Canvas( QW.QWidget ):
def _TryToChangeZoom( self, new_zoom ):
def _TryToChangeZoom( self, new_zoom, zoom_center_type_override = None ):
if self._current_media is None:
@ -1073,7 +1068,14 @@ class Canvas( QW.QWidget ):
#
zoom_center_type = HG.client_controller.new_options.GetInteger( 'media_viewer_zoom_center' )
if zoom_center_type_override is None:
zoom_center_type = HG.client_controller.new_options.GetInteger( 'media_viewer_zoom_center' )
else:
zoom_center_type = zoom_center_type_override
# viewer center is the default
zoom_centerpoint = QC.QPoint( my_size.width() // 2, my_size.height() // 2 )
@ -1163,7 +1165,7 @@ class Canvas( QW.QWidget ):
self.update()
def _ZoomIn( self ):
def _ZoomIn( self, zoom_center_type_override = None ):
if self._current_media is not None and self._IsZoomable():
@ -1203,12 +1205,12 @@ class Canvas( QW.QWidget ):
new_zoom = min( bigger_zooms )
self._TryToChangeZoom( new_zoom )
self._TryToChangeZoom( new_zoom, zoom_center_type_override = zoom_center_type_override )
def _ZoomOut( self ):
def _ZoomOut( self, zoom_center_type_override = None ):
if self._current_media is not None and self._IsZoomable():
@ -1248,12 +1250,12 @@ class Canvas( QW.QWidget ):
new_zoom = max( smaller_zooms )
self._TryToChangeZoom( new_zoom )
self._TryToChangeZoom( new_zoom, zoom_center_type_override = zoom_center_type_override )
def _ZoomSwitch( self ):
def _ZoomSwitch( self, zoom_center_type_override = None ):
if self._current_media is not None and self._IsZoomable():
@ -1271,7 +1273,7 @@ class Canvas( QW.QWidget ):
new_zoom = 1.0
self._TryToChangeZoom( new_zoom )
self._TryToChangeZoom( new_zoom, zoom_center_type_override = zoom_center_type_override )
if new_zoom <= self._canvas_zoom:
@ -1473,6 +1475,10 @@ class Canvas( QW.QWidget ):
self._Delete()
elif action == 'undelete_file':
self._Undelete()
elif action == 'inbox_file':
self._Inbox()
@ -1521,14 +1527,26 @@ class Canvas( QW.QWidget ):
self._ZoomIn()
elif action == 'zoom_in_canvas_button':
self._ZoomIn( zoom_center_type_override = ZOOM_CENTERPOINT_VIEWER_CENTER )
elif action == 'zoom_out':
self._ZoomOut()
elif action == 'zoom_out_canvas_button':
self._ZoomOut( zoom_center_type_override = ZOOM_CENTERPOINT_VIEWER_CENTER )
elif action == 'switch_between_100_percent_and_canvas_zoom':
self._ZoomSwitch()
elif action == 'switch_between_100_percent_and_canvas_zoom_canvas_button':
self._ZoomSwitch( zoom_center_type_override = ZOOM_CENTERPOINT_VIEWER_CENTER )
else:
command_processed = False
@ -1816,6 +1834,8 @@ class CanvasPanel( Canvas ):
ClientGUIMenus.AppendMenuItem( manage_menu, notes_str, 'Manage this file\'s notes.', self._ManageNotes )
ClientGUIMedia.AddManageFileViewingStatsMenu( self, manage_menu, [ self._current_media ] )
ClientGUIMenus.AppendMenu( menu, manage_menu, 'manage' )
ClientGUIMedia.AddKnownURLsViewCopyMenu( self, menu, self._current_media )
@ -3098,32 +3118,6 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
def keyPressEvent( self, event ):
if self._IShouldCatchShortcutEvent( event = event ):
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
if modifier == QC.Qt.NoModifier and key in ClientGUIShortcuts.DELETE_KEYS:
self._Delete()
elif modifier == QC.Qt.ShiftModifier and key in ClientGUIShortcuts.DELETE_KEYS:
self._Undelete()
else:
CanvasWithHovers.keyPressEvent( self, event )
else:
event.ignore()
def ProcessApplicationCommand( self, command, canvas_key = None ):
if canvas_key is not None and canvas_key != self._canvas_key:
@ -3571,19 +3565,16 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
def _Back( self ):
if self._IShouldCatchShortcutEvent():
if self._current_media == self._GetFirst():
if self._current_media == self._GetFirst():
return
else:
self._ShowPrevious()
self._kept.discard( self._current_media )
self._deleted.discard( self._current_media )
return
else:
self._ShowPrevious()
self._kept.discard( self._current_media )
self._deleted.discard( self._current_media )
@ -3733,30 +3724,6 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
def EventDelete( self, event ):
if self._IShouldCatchShortcutEvent( event = event ):
self._Delete()
else:
return True # was: event.ignore()
def EventUndelete( self, event ):
if self._IShouldCatchShortcutEvent( event = event ):
self._Undelete()
else:
return True # was: event.ignore()
def ProcessApplicationCommand( self, command, canvas_key = None ):
if canvas_key is not None and canvas_key != self._canvas_key:
@ -4099,25 +4066,6 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
def keyPressEvent( self, event ):
if self._IShouldCatchShortcutEvent( event = event ):
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
if modifier == QC.Qt.NoModifier and key in ClientGUIShortcuts.DELETE_KEYS: self._Delete()
elif modifier == QC.Qt.ShiftModifier and key in ClientGUIShortcuts.DELETE_KEYS: self._Undelete()
else:
CanvasMediaListNavigable.keyPressEvent( self, event )
else:
event.ignore()
def ProcessApplicationCommand( self, command, canvas_key = None ):
if canvas_key is not None and canvas_key != self._canvas_key:
@ -4299,6 +4247,8 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
ClientGUIMenus.AppendMenuItem( manage_menu, notes_str, 'Manage this file\'s notes.', self._ManageNotes )
ClientGUIMedia.AddManageFileViewingStatsMenu( self, manage_menu, [ self._current_media ] )
ClientGUIMenus.AppendMenu( menu, manage_menu, 'manage' )
ClientGUIMedia.AddKnownURLsViewCopyMenu( self, menu, self._current_media )
@ -4355,32 +4305,3 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
def wheelEvent( self, event ):
if self._IShouldCatchShortcutEvent( event = event ):
if event.modifiers() & QC.Qt.ControlModifier:
if event.angleDelta().y() > 0:
self._ZoomIn()
else:
self._ZoomOut()
else:
if event.angleDelta().y() > 0:
self._ShowPrevious()
else:
self._ShowNext()

View File

@ -880,13 +880,13 @@ class CanvasHoverFrameTop( CanvasHoverFrame ):
self._zoom_text = ClientGUICommon.BetterStaticText( self, 'zoom' )
zoom_in = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().zoom_in, HG.client_controller.pub, 'canvas_application_command', ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'zoom_in' ), self._canvas_key )
zoom_in = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().zoom_in, HG.client_controller.pub, 'canvas_application_command', ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'zoom_in_canvas_button' ), self._canvas_key )
zoom_in.SetToolTipWithShortcuts( 'zoom in', 'zoom_in' )
zoom_out = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().zoom_out, HG.client_controller.pub, 'canvas_application_command', ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'zoom_out' ), self._canvas_key )
zoom_out = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().zoom_out, HG.client_controller.pub, 'canvas_application_command', ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'zoom_out_canvas_button' ), self._canvas_key )
zoom_out.SetToolTipWithShortcuts( 'zoom out', 'zoom_out' )
zoom_switch = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().zoom_switch, HG.client_controller.pub, 'canvas_application_command', ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'switch_between_100_percent_and_canvas_zoom' ), self._canvas_key )
zoom_switch = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().zoom_switch, HG.client_controller.pub, 'canvas_application_command', ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'switch_between_100_percent_and_canvas_zoom_canvas_button' ), self._canvas_key )
zoom_switch.SetToolTipWithShortcuts( 'zoom switch', 'switch_between_100_percent_and_canvas_zoom' )
self._volume_control = ClientGUIMediaControls.VolumeControl( self, ClientGUICommon.CANVAS_MEDIA_VIEWER )

View File

@ -422,7 +422,7 @@ class DialogInputNamespaceRegex( Dialog ):
self._shortcuts = ClientGUICommon.RegexButton( self )
self._regex_intro_link = ClientGUICommon.BetterHyperLink( self, 'a good regex introduction', 'http://www.aivosto.com/vbtips/regex.html' )
self._regex_practise_link = ClientGUICommon.BetterHyperLink( self, 'regex practise', 'http://regexr.com/3cvmf' )
self._regex_practise_link = ClientGUICommon.BetterHyperLink( self, 'regex practice', 'http://regexr.com/3cvmf' )
self._ok = QW.QPushButton( 'OK', self )
self._ok.clicked.connect( self.EventOK )

View File

@ -256,7 +256,7 @@ class FilenameTaggingOptionsPanel( QW.QWidget ):
self._regex_shortcuts = ClientGUICommon.RegexButton( self._regexes_panel )
self._regex_intro_link = ClientGUICommon.BetterHyperLink( self._regexes_panel, 'a good regex introduction', 'http://www.aivosto.com/vbtips/regex.html' )
self._regex_practise_link = ClientGUICommon.BetterHyperLink( self._regexes_panel, 'regex practise', 'http://regexr.com/3cvmf' )
self._regex_practise_link = ClientGUICommon.BetterHyperLink( self._regexes_panel, 'regex practice', 'http://regexr.com/3cvmf' )
#

View File

@ -1449,7 +1449,7 @@ class ListBox( QW.QScrollArea ):
key_code = event.key()
if self.hasFocus() and key_code in ClientGUIShortcuts.DELETE_KEYS:
if self.hasFocus() and key_code in ClientGUIShortcuts.DELETE_KEYS_QT:
self._DeleteActivate()
@ -1631,6 +1631,16 @@ class ListBox( QW.QScrollArea ):
def SelectTopItem( self ):
if len( self._ordered_terms ) > 0:
self._selected_terms = set()
self._Hit( False, False, 0 )
def SetMinimumHeightNumChars( self, minimum_height_num_chars ):
self._minimum_height_num_chars = minimum_height_num_chars
@ -1914,12 +1924,43 @@ class ListBoxTags( ListBox ):
if command == 'hide':
message = 'Hide "{}" from here?'.format( tag )
from hydrus.client.gui import ClientGUIDialogsQuick
result = ClientGUIDialogsQuick.GetYesNo( self, message )
if result != QW.QDialog.Accepted:
return
HG.client_controller.tag_display_manager.HideTag( self._tag_display_type, CC.COMBINED_TAG_SERVICE_KEY, tag )
elif command == 'hide_namespace':
( namespace, subtag ) = HydrusTags.SplitTag( tag )
if namespace == '':
insert = 'unnamespaced'
else:
insert = '"{}"'.format( namespace )
message = 'Hide {} tags from here?'.format( insert )
from hydrus.client.gui import ClientGUIDialogsQuick
result = ClientGUIDialogsQuick.GetYesNo( self, message )
if result != QW.QDialog.Accepted:
return
if namespace != '':
namespace += ':'
@ -2936,7 +2977,7 @@ class ListBoxTagsStringsAddRemove( ListBoxTagsStrings ):
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
if key in ClientGUIShortcuts.DELETE_KEYS:
if key in ClientGUIShortcuts.DELETE_KEYS_QT:
self._Activate()

View File

@ -85,6 +85,9 @@ class BetterListCtrl( QW.QTreeWidget ):
#self.setColumnWidth( resize_column - 1, sizing_column_initial_width )
#self.header().setStretchLastSection( True )
# hydev looked at this problem. the real answer I think will be to move to column size memory and let the last section resize
# start with decent values and then we can remember whatever the user ends up liking later. this will be simpler
self.setMinimumWidth( total_width )
self.GrowShrinkColumnsHeight( height_num_chars )
@ -363,7 +366,7 @@ class BetterListCtrl( QW.QTreeWidget ):
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
if key in ClientGUIShortcuts.DELETE_KEYS:
if key in ClientGUIShortcuts.DELETE_KEYS_QT:
self.ProcessDeleteAction()

View File

@ -115,6 +115,8 @@ class mpvWidget( QW.QWidget ):
self._media = None
self._file_is_loaded = False
self._times_to_play_gif = 0
self._current_seek_to_start_count = 0
@ -192,6 +194,16 @@ class mpvWidget( QW.QWidget ):
def _InitialiseMPVCallbacks( self ):
def qt_file_loaded_event():
if not QP.isValid( self ):
return
self._file_is_loaded = True
def qt_seek_event():
if not QP.isValid( self ):
@ -199,6 +211,11 @@ class mpvWidget( QW.QWidget ):
return
if not self._file_is_loaded:
return
if self._media is not None and self._player.time_pos <= 1.0:
self._current_seek_to_start_count += 1
@ -224,12 +241,18 @@ class mpvWidget( QW.QWidget ):
QP.CallAfter( qt_seek_event )
@player.event_callback( mpv.MpvEventID.FILE_LOADED )
def file_loaded_event( event ):
QP.CallAfter( qt_file_loaded_event )
def GetAnimationBarStatus( self ):
buffer_indices = None
if self._media is None:
if self._media is None or not self._file_is_loaded:
current_frame_index = 0
current_timestamp_ms = 0
@ -268,6 +291,11 @@ class mpvWidget( QW.QWidget ):
def GotoPreviousOrNextFrame( self, direction ):
if not self._file_is_loaded:
return
command = 'frame-step'
if direction == 1:
@ -284,6 +312,11 @@ class mpvWidget( QW.QWidget ):
def Seek( self, time_index_ms ):
if not self._file_is_loaded:
return
time_index_s = time_index_ms / 1000
self._player.seek( time_index_s, reference = 'absolute' )
@ -389,6 +422,8 @@ class mpvWidget( QW.QWidget ):
return
self._file_is_loaded = False
self._media = media
self._times_to_play_gif = 0

View File

@ -1,9 +1,11 @@
import os
import random
import time
import typing
from qtpy import QtWidgets as QW
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusPaths
from hydrus.core import HydrusData
@ -55,6 +57,35 @@ def CopyMediaURLClassURLs( medias, url_class ):
HG.client_controller.pub( 'clipboard', 'text', urls_string )
def DoClearFileViewingStats( win: QW.QWidget, flat_medias: typing.Iterable[ ClientMedia.MediaSingleton ] ):
if len( flat_medias ) == 0:
return
if len( flat_medias ) == 1:
insert = 'this file'
else:
insert = 'these {} files'.format( HydrusData.ToHumanInt( len( flat_medias ) ) )
message = 'Clear the file viewing stats for {}?'.format( insert )
result = ClientGUIDialogsQuick.GetYesNo( win, message )
if result == QW.QDialog.Accepted:
hashes = { m.GetHash() for m in flat_medias }
content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILE_VIEWING_STATS, HC.CONTENT_UPDATE_DELETE, hashes )
HG.client_controller.Write( 'content_updates', { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ content_update ] } )
def DoOpenKnownURLFromShortcut( win, media ):
urls = media.GetLocationsManager().GetURLs()
@ -477,6 +508,16 @@ def AddKnownURLsViewCopyMenu( win, menu, focus_media, selected_media = None ):
ClientGUIMenus.AppendMenu( menu, urls_menu, 'known urls' )
def AddManageFileViewingStatsMenu( win: QW.QWidget, menu: QW.QMenu, flat_medias: typing.Iterable[ ClientMedia.MediaSingleton ] ):
# add test here for if media actually has stats, edit them, all that
submenu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( submenu, 'clear', 'Clear all the recorded file viewing stats for the selected files.', DoClearFileViewingStats, win, flat_medias )
ClientGUIMenus.AppendMenu( menu, submenu, 'viewing stats' )
def AddServiceKeyLabelsToMenu( menu, service_keys, phrase ):
services_manager = HG.client_controller.services_manager
@ -505,7 +546,7 @@ def AddServiceKeyLabelsToMenu( menu, service_keys, phrase ):
ClientGUIMenus.AppendMenu( menu, submenu, phrase + '\u2026' )
def AddServiceKeysToMenu( event_handler, menu, service_keys, phrase, description, callable ):
def AddServiceKeysToMenu( event_handler, menu, service_keys, phrase, description, call ):
services_manager = HG.client_controller.services_manager
@ -517,7 +558,7 @@ def AddServiceKeysToMenu( event_handler, menu, service_keys, phrase, description
label = phrase + ' ' + name
ClientGUIMenus.AppendMenuItem( menu, label, description, callable, service_key )
ClientGUIMenus.AppendMenuItem( menu, label, description, call, service_key )
else:
@ -527,7 +568,7 @@ def AddServiceKeysToMenu( event_handler, menu, service_keys, phrase, description
name = services_manager.GetName( service_key )
ClientGUIMenus.AppendMenuItem( submenu, name, description, callable, service_key )
ClientGUIMenus.AppendMenuItem( submenu, name, description, call, service_key )
ClientGUIMenus.AppendMenu( menu, submenu, phrase + '\u2026' )

View File

@ -682,24 +682,31 @@ class Page( QW.QSplitter ):
def GetSashPositions( self ):
if QP.SplitterVisibleCount( self ) > 1:
hpos = HC.options[ 'hpos' ]
sizes = self.sizes()
if len( sizes ) > 1:
x = self.widget( 0 ).width()
else:
x = HC.options[ 'hpos' ]
if sizes[0] != 0:
hpos = sizes[0]
if QP.SplitterVisibleCount( self._search_preview_split ) > 1:
vpos = HC.options[ 'vpos' ]
sizes = self._search_preview_split.sizes()
if len( sizes ) > 1:
y = self._search_preview_split.widget( 0 ).height()
if sizes[1] != 0:
vpos = - sizes[1]
else:
y = HC.options[ 'vpos' ]
return ( x, y )
return ( hpos, vpos )
def GetTotalWeight( self ):
@ -737,12 +744,7 @@ class Page( QW.QSplitter ):
if self.isVisible() and not self._done_split_setups:
self.SetupSplits()
if HC.options[ 'hide_preview' ]:
QP.CallAfter( QP.Unsplit, self._search_preview_split, self._preview_panel )
self.SetSplitterPositions()
self._done_split_setups = True
@ -760,13 +762,6 @@ class Page( QW.QSplitter ):
def SetupSplits( self ):
QP.SplitVertically( self, self._search_preview_split, self._media_panel, HC.options[ 'hpos' ] )
QP.SplitHorizontally( self._search_preview_split, self._management_panel, self._preview_panel, HC.options[ 'vpos' ] )
def ShowHideSplit( self ):
if QP.SplitterVisibleCount( self ) > 1:
@ -777,7 +772,7 @@ class Page( QW.QSplitter ):
else:
self.SetupSplits()
self.SetSplitterPositions()
@ -828,7 +823,17 @@ class Page( QW.QSplitter ):
self._management_panel.SetSearchFocus()
def SetSplitterPositions( self, hpos, vpos ):
def SetSplitterPositions( self, hpos = None, vpos = None ):
if hpos is None:
hpos = HC.options[ 'hpos' ]
if vpos is None:
vpos = HC.options[ 'vpos' ]
QP.SplitHorizontally( self._search_preview_split, self._management_panel, self._preview_panel, vpos )

View File

@ -1038,7 +1038,8 @@ class EditHTMLTagRulePanel( ClientGUIScrolledPanels.EditPanel ):
self._tag_attributes = ClientGUIStringControls.StringToStringDictControl( self, tag_attributes, min_height = 4 )
self._tag_index = ClientGUICommon.NoneableSpinCtrl( self, 'index to fetch', none_phrase = 'get all', min = 0, max = 255 )
self._tag_index = ClientGUICommon.NoneableSpinCtrl( self, 'index to fetch', none_phrase = 'get all', min = -65536, max = 65535 )
self._tag_index.setToolTip( 'You can make this negative to do negative indexing, i.e. "Select the second from last item".' )
self._tag_depth = QP.MakeQSpinBox( self, min=1, max=255 )
@ -1465,13 +1466,14 @@ class EditJSONParsingRulePanel( ClientGUIScrolledPanels.EditPanel ):
self._parse_rule_type.addItem( 'dictionary entry', ClientParsing.JSON_PARSE_RULE_TYPE_DICT_KEY )
self._parse_rule_type.addItem( 'all dictionary/list items', ClientParsing.JSON_PARSE_RULE_TYPE_ALL_ITEMS )
self._parse_rule_type.addItem( 'indexed list item', ClientParsing.JSON_PARSE_RULE_TYPE_INDEXED_ITEM )
self._parse_rule_type.addItem( 'indexed item', ClientParsing.JSON_PARSE_RULE_TYPE_INDEXED_ITEM )
string_match = ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_FIXED, match_value = 'posts', example_string = 'posts' )
self._string_match = ClientGUIStringPanels.EditStringMatchPanel( self, string_match )
self._index = QP.MakeQSpinBox( self, min=0, max=65535 )
self._index = QP.MakeQSpinBox( self, min=-65536, max=65535 )
self._index.setToolTip( 'You can make this negative to do negative indexing, i.e. "Select the second from last item".' )
#
@ -1839,7 +1841,7 @@ class EditContentParserPanel( ClientGUIScrolledPanels.EditPanel ):
self._url_type.addItem( 'url to download/pursue (file/post url)', HC.URL_TYPE_DESIRED )
self._url_type.addItem( 'POST parsers only: url to associate (source url)', HC.URL_TYPE_SOURCE )
self._url_type.addItem( 'GALLERY parsers only: next gallery page (not queued if no post/file urls found)', HC.URL_TYPE_NEXT )
self._url_type.addItem( 'EXPERIMENTAL: GALLERY parsers only: sub-gallery page (is queued even if no post/file urls found)', HC.URL_TYPE_SUB_GALLERY )
self._url_type.addItem( 'GALLERY parsers only: sub-gallery page (is queued even if no post/file urls found--be careful, only use if you know you need it)', HC.URL_TYPE_SUB_GALLERY )
self._file_priority = QP.MakeQSpinBox( self._urls_panel, min=0, max=100 )
self._file_priority.setValue( 50 )

View File

@ -2050,6 +2050,10 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
self._Delete()
elif action == 'undelete_file':
self._Undelete()
elif action == 'inbox_file':
self._Inbox()
@ -2267,6 +2271,7 @@ class MediaPanelThumbnails( MediaPanel ):
self._widget_event_filter.EVT_MIDDLE_DOWN( self.EventMouseFullScreen )
# notice this is on widget, not myself. fails to set up scrollbars if just moved up
# there's a job in qt to-do to sort all this out and fix other scroll issues
self._widget_event_filter.EVT_SIZE( self.EventResize )
self._widget_event_filter.EVT_KEY_DOWN( self.EventKeyDown )
@ -2918,7 +2923,7 @@ class MediaPanelThumbnails( MediaPanel ):
def _UpdateBackgroundColour( self ):
MediaPanel._UpdateBackgroundColour( self )
self._DirtyAllPages()
self._DeleteAllDirtyPages()
@ -3214,8 +3219,10 @@ class MediaPanelThumbnails( MediaPanel ):
services_manager = HG.client_controller.services_manager
flat_selected_medias = ClientMedia.FlattenMedia( self._selected_media )
all_locations_managers = [ media.GetLocationsManager() for media in ClientMedia.FlattenMedia( self._sorted_media ) ]
selected_locations_managers = [ media.GetLocationsManager() for media in ClientMedia.FlattenMedia( self._selected_media ) ]
selected_locations_managers = [ media.GetLocationsManager() for media in flat_selected_medias ]
selection_has_local = True in ( locations_manager.IsLocal() for locations_manager in selected_locations_managers )
selection_has_local_file_domain = True in ( CC.LOCAL_FILE_SERVICE_KEY in locations_manager.GetCurrent() for locations_manager in selected_locations_managers )
@ -3630,6 +3637,8 @@ class MediaPanelThumbnails( MediaPanel ):
ClientGUIMenus.AppendMenuItem( manage_menu, notes_str, 'Manage notes for the focused file.', self._ManageNotes )
ClientGUIMedia.AddManageFileViewingStatsMenu( self, manage_menu, flat_selected_medias )
len_interesting_remote_service_keys = 0
len_interesting_remote_service_keys += len( downloadable_file_service_keys )
@ -3654,10 +3663,12 @@ class MediaPanelThumbnails( MediaPanel ):
remote_action_menu = QW.QMenu( manage_menu )
if len( downloadable_file_service_keys ) > 0:
ClientGUIMenus.AppendMenuItem( remote_action_menu, download_phrase, 'Download all possible selected files.', self._DownloadSelected )
if some_downloading:
ClientGUIMenus.AppendMenuItem( remote_action_menu, rescind_download_phrase, 'Stop downloading any of the selected files.', self._RescindDownloadSelected )
@ -3817,6 +3828,7 @@ class MediaPanelThumbnails( MediaPanel ):
else:
if not focus_is_definitely_king:
ClientGUIMenus.AppendMenuItem( duplicates_action_submenu, 'set this file as the best quality of its group', 'Set the focused media to be the King of its group.', self.ProcessApplicationCommand, ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_media_set_focused_king' ) )
@ -3825,18 +3837,22 @@ class MediaPanelThumbnails( MediaPanel ):
duplicates_single_dissolution_menu = QW.QMenu( duplicates_action_submenu )
if focus_can_be_searched:
ClientGUIMenus.AppendMenuItem( duplicates_single_dissolution_menu, 'schedule this file to be searched for potentials again', 'Queue this file for another potentials search. Will not remove any existing potentials.', self.ProcessApplicationCommand, ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_media_reset_focused_potential_search' ) )
if focus_has_potentials:
ClientGUIMenus.AppendMenuItem( duplicates_single_dissolution_menu, 'remove this file\'s potential relationships', 'Clear out this file\'s potential relationships.', self.ProcessApplicationCommand, ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_media_remove_focused_potentials' ) )
if focus_is_in_duplicate_group:
if not focus_is_definitely_king:
ClientGUIMenus.AppendMenuItem( duplicates_single_dissolution_menu, 'remove this file from its duplicate group', 'Extract this file from its duplicate group and reset its search status.', self.ProcessApplicationCommand, ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_media_remove_focused_from_duplicate_group' ) )
ClientGUIMenus.AppendMenuItem( duplicates_single_dissolution_menu, 'dissolve this file\'s duplicate group completely', 'Completely eliminate this file\'s duplicate group and reset all files\' search status.', self.ProcessApplicationCommand, ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_media_dissolve_focused_duplicate_group' ) )
@ -3848,6 +3864,7 @@ class MediaPanelThumbnails( MediaPanel ):
if focus_has_fps:
ClientGUIMenus.AppendMenuItem( duplicates_single_dissolution_menu, 'delete all false-positive relationships this file\'s alternate group has with other groups', 'Clear out all false-positive relationships this file\'s alternates group has with other groups and resets search status.', self.ProcessApplicationCommand, ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_media_clear_focused_false_positives' ) )
@ -4166,8 +4183,6 @@ class MediaPanelThumbnails( MediaPanel ):
QP.AddShortcut( self, QC.Qt.KeypadModifier, QC.Qt.Key_Home, self._ScrollHome, False )
QP.AddShortcut( self, QC.Qt.NoModifier, QC.Qt.Key_End, self._ScrollEnd, False )
QP.AddShortcut( self, QC.Qt.KeypadModifier, QC.Qt.Key_End, self._ScrollEnd, False )
QP.AddShortcut( self, QC.Qt.NoModifier, QC.Qt.Key_Delete, self._Delete )
QP.AddShortcut( self, QC.Qt.KeypadModifier, QC.Qt.Key_Delete, self._Delete )
QP.AddShortcut( self, QC.Qt.NoModifier, QC.Qt.Key_Return, self._LaunchMediaViewer )
QP.AddShortcut( self, QC.Qt.KeypadModifier, QC.Qt.Key_Enter, self._LaunchMediaViewer )
QP.AddShortcut( self, QC.Qt.NoModifier, QC.Qt.Key_Up, self._MoveFocusedThumbnail, -1, 0, False )
@ -4182,8 +4197,6 @@ class MediaPanelThumbnails( MediaPanel ):
QP.AddShortcut( self, QC.Qt.ShiftModifier | QC.Qt.KeypadModifier, QC.Qt.Key_Home, self._ScrollHome, True )
QP.AddShortcut( self, QC.Qt.ShiftModifier, QC.Qt.Key_End, self._ScrollEnd, True )
QP.AddShortcut( self, QC.Qt.ShiftModifier | QC.Qt.KeypadModifier, QC.Qt.Key_End, self._ScrollEnd, True )
QP.AddShortcut( self, QC.Qt.ShiftModifier, QC.Qt.Key_Delete, self._Undelete )
QP.AddShortcut( self, QC.Qt.ShiftModifier | QC.Qt.KeypadModifier, QC.Qt.Key_Delete, self._Undelete )
QP.AddShortcut( self, QC.Qt.ShiftModifier, QC.Qt.Key_Up, self._MoveFocusedThumbnail, -1, 0, True )
QP.AddShortcut( self, QC.Qt.ShiftModifier | QC.Qt.KeypadModifier, QC.Qt.Key_Up, self._MoveFocusedThumbnail, -1, 0, True )
QP.AddShortcut( self, QC.Qt.ShiftModifier, QC.Qt.Key_Down, self._MoveFocusedThumbnail, 1, 0, True )
@ -4195,13 +4208,6 @@ class MediaPanelThumbnails( MediaPanel ):
QP.AddShortcut( self, QC.Qt.ControlModifier, QC.Qt.Key_A, self._Select, ClientMedia.FileFilter( ClientMedia.FILE_FILTER_ALL ) )
QP.AddShortcut( self, QC.Qt.ControlModifier, QC.Qt.Key_Space, ctrl_space_callback, self )
if HC.PLATFORM_MACOS:
QP.AddShortcut( self, QC.Qt.NoModifier, QC.Qt.Key_Back, self._Delete )
QP.AddShortcut( self, QC.Qt.ShiftModifier, QC.Qt.Key_Back, self._Undelete )
def SetFocusedMedia( self, media ):

View File

@ -5451,7 +5451,7 @@ class ManageURLsPanel( ClientGUIScrolledPanels.ManagePanel ):
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
if key in ClientGUIShortcuts.DELETE_KEYS:
if key in ClientGUIShortcuts.DELETE_KEYS_QT:
urls = [ QP.GetClientData( self._urls_listbox, selection.row() ) for selection in list( self._urls_listbox.selectedIndexes() ) ]

View File

@ -68,11 +68,13 @@ SHORTCUT_KEY_SPECIAL_F12 = 28
if HC.PLATFORM_MACOS:
DELETE_KEYS = ( QC.Qt.Key_Backspace, QC.Qt.Key_Delete )
DELETE_KEYS_QT = ( QC.Qt.Key_Backspace, QC.Qt.Key_Delete )
DELETE_KEYS_HYDRUS = ( SHORTCUT_KEY_SPECIAL_BACKSPACE, SHORTCUT_KEY_SPECIAL_DELETE )
else:
DELETE_KEYS = ( QC.Qt.Key_Delete, )
DELETE_KEYS_QT = ( QC.Qt.Key_Delete, )
DELETE_KEYS_HYDRUS = ( SHORTCUT_KEY_SPECIAL_DELETE, )
special_key_shortcut_enum_lookup = {}
@ -212,7 +214,7 @@ shortcut_names_to_descriptions[ 'preview_media_window' ] = 'Actions for any vide
SHORTCUTS_RESERVED_NAMES = [ 'global', 'archive_delete_filter', 'duplicate_filter', 'media', 'main_gui', 'media_viewer_browser', 'media_viewer', 'media_viewer_media_window', 'preview_media_window' ]
SHORTCUTS_GLOBAL_ACTIONS = [ 'global_audio_mute', 'global_audio_unmute', 'global_audio_mute_flip', 'exit_application', 'exit_application_force_maintenance', 'restart_application', 'hide_to_system_tray' ]
SHORTCUTS_MEDIA_ACTIONS = [ 'manage_file_tags', 'manage_file_ratings', 'manage_file_urls', 'manage_file_notes', 'archive_file', 'inbox_file', 'delete_file', 'export_files', 'export_files_quick_auto_export', 'remove_file_from_view', 'open_file_in_external_program', 'open_selection_in_new_page', 'launch_the_archive_delete_filter', 'copy_bmp', 'copy_file', 'copy_path', 'copy_sha256_hash', 'get_similar_to_exact', 'get_similar_to_very_similar', 'get_similar_to_similar', 'get_similar_to_speculative', 'duplicate_media_set_alternate', 'duplicate_media_set_alternate_collections', 'duplicate_media_set_custom', 'duplicate_media_set_focused_better', 'duplicate_media_set_focused_king', 'duplicate_media_set_same_quality', 'open_known_url' ]
SHORTCUTS_MEDIA_ACTIONS = [ 'manage_file_tags', 'manage_file_ratings', 'manage_file_urls', 'manage_file_notes', 'archive_file', 'inbox_file', 'delete_file', 'undelete_file', 'export_files', 'export_files_quick_auto_export', 'remove_file_from_view', 'open_file_in_external_program', 'open_selection_in_new_page', 'launch_the_archive_delete_filter', 'copy_bmp', 'copy_file', 'copy_path', 'copy_sha256_hash', 'get_similar_to_exact', 'get_similar_to_very_similar', 'get_similar_to_similar', 'get_similar_to_speculative', 'duplicate_media_set_alternate', 'duplicate_media_set_alternate_collections', 'duplicate_media_set_custom', 'duplicate_media_set_focused_better', 'duplicate_media_set_focused_king', 'duplicate_media_set_same_quality', 'open_known_url' ]
SHORTCUTS_MEDIA_VIEWER_ACTIONS = [ 'pause_media', 'pause_play_media', 'move_animation_to_previous_frame', 'move_animation_to_next_frame', 'switch_between_fullscreen_borderless_and_regular_framed_window', 'pan_up', 'pan_down', 'pan_left', 'pan_right', 'pan_top_edge', 'pan_bottom_edge', 'pan_left_edge', 'pan_right_edge', 'pan_vertical_center', 'pan_horizontal_center', 'zoom_in', 'zoom_out', 'switch_between_100_percent_and_canvas_zoom', 'flip_darkmode', 'close_media_viewer' ]
SHORTCUTS_MEDIA_VIEWER_BROWSER_ACTIONS = [ 'view_next', 'view_first', 'view_last', 'view_previous', 'pause_play_slideshow', 'show_menu', 'close_media_viewer' ]
SHORTCUTS_MAIN_GUI_ACTIONS = [ 'refresh', 'refresh_all_pages', 'refresh_page_of_pages_pages', 'new_page', 'new_page_of_pages', 'new_duplicate_filter_page', 'new_gallery_downloader_page', 'new_url_downloader_page', 'new_simple_downloader_page', 'new_watcher_downloader_page', 'synchronised_wait_switch', 'set_media_focus', 'show_hide_splitters', 'set_search_focus', 'unclose_page', 'close_page', 'redo', 'undo', 'flip_darkmode', 'check_all_import_folders', 'flip_debug_force_idle_mode_do_not_set_this', 'show_and_focus_manage_tags_favourite_tags', 'show_and_focus_manage_tags_related_tags', 'show_and_focus_manage_tags_file_lookup_script_tags', 'show_and_focus_manage_tags_recent_tags', 'focus_media_viewer' ]
@ -469,23 +471,54 @@ def AncestorShortcutsHandlers( widget: QW.QWidget ):
return shortcuts_handlers
def IShouldCatchShortcutEvent( evt_handler, event = None, child_tlw_classes_who_can_pass_up = None ):
def IShouldCatchShortcutEvent( event_handler_owner: QC.QObject, event_catcher: QW.QWidget, event: typing.Optional[ QC.QEvent ] = None, child_tlw_classes_who_can_pass_up: typing.Optional[ typing.Iterable[ type ] ] = None ):
do_focus_test = True
if event is not None and isinstance( event, QG.QWheelEvent ):
if event is not None:
do_focus_test = False
# the event happened to somewhere else, most likely a hover window of a media viewer
# should we intercept that event that happened somewhere else?
if event_handler_owner != event_catcher:
# don't pass clicks up
if event.type() in ( QC.QEvent.MouseButtonPress, QC.QEvent.MouseButtonRelease, QC.QEvent.MouseButtonDblClick ):
return False
# don't pass wheels that happen to legit controls that want to eat it, like a list, when the catcher is a window
if event.type() == QC.QEvent.Wheel:
widget_under_mouse = event_catcher.childAt( event_catcher.mapFromGlobal( QG.QCursor.pos() ) )
if widget_under_mouse is not None:
mouse_scroll_over_window_greyspace = widget_under_mouse == event_catcher and event_catcher.isWindow()
if not mouse_scroll_over_window_greyspace:
return False
if event.type() == QC.QEvent.Wheel:
do_focus_test = False
do_focus_test = False
if do_focus_test:
if not ClientGUIFunctions.TLWIsActive( evt_handler ):
if not ClientGUIFunctions.TLWIsActive( event_handler_owner ):
if child_tlw_classes_who_can_pass_up is not None:
child_tlw_has_focus = ClientGUIFunctions.WidgetOrAnyTLWChildHasFocus( evt_handler ) and isinstance( QW.QApplication.activeWindow(), child_tlw_classes_who_can_pass_up )
child_tlw_has_focus = ClientGUIFunctions.WidgetOrAnyTLWChildHasFocus( event_handler_owner ) and isinstance( QW.QApplication.activeWindow(), child_tlw_classes_who_can_pass_up )
if not child_tlw_has_focus:
@ -1050,7 +1083,7 @@ class ShortcutsHandler( QC.QObject ):
if event.type() == QC.QEvent.KeyPress:
i_should_catch_shortcut_event = IShouldCatchShortcutEvent( watched, event = event )
i_should_catch_shortcut_event = IShouldCatchShortcutEvent( self._parent, watched, event = event )
shortcut = ConvertKeyEventToShortcut( event )
@ -1089,7 +1122,7 @@ class ShortcutsHandler( QC.QObject ):
if event.type() in ( QC.QEvent.MouseButtonPress, QC.QEvent.MouseButtonRelease, QC.QEvent.MouseButtonDblClick, QC.QEvent.Wheel ):
if event.type() != QC.QEvent.Wheel and self._ignore_activating_mouse_click and not HydrusData.TimeHasPassedPrecise( self._frame_activated_time + 0.1 ):
if event.type() != QC.QEvent.Wheel and self._ignore_activating_mouse_click and not HydrusData.TimeHasPassedPrecise( self._frame_activated_time + 0.017 ):
if event.type() == QC.QEvent.MouseButtonRelease:
@ -1099,7 +1132,7 @@ class ShortcutsHandler( QC.QObject ):
return False
i_should_catch_shortcut_event = IShouldCatchShortcutEvent( watched, event = event )
i_should_catch_shortcut_event = IShouldCatchShortcutEvent( self._parent, watched, event = event )
shortcut = ConvertMouseEventToShortcut( event )

View File

@ -949,7 +949,7 @@ class EditStringMatchPanel( ClientGUIScrolledPanels.EditPanel ):
try:
string_match.Test( self._example_string.text() )
string_match.Test( string_match.GetExampleString() )
except HydrusExceptions.StringMatchException:

View File

@ -242,10 +242,17 @@ class RecentTagsPanel( QW.QWidget ):
def _UpdateTagDisplay( self ):
had_selection_before = len( self._recent_tags.GetSelectedTags() ) > 0
tags = FilterSuggestedTagsForMedia( self._last_fetched_tags, self._media, self._service_key )
self._recent_tags.SetTags( tags )
if had_selection_before and len( tags ) > 0:
self._recent_tags.SelectTopItem()
def EventClear( self ):

View File

@ -75,6 +75,9 @@ class EditTagAutocompleteOptionsPanel( ClientGUIScrolledPanels.EditPanel ):
self._search_namespaces_into_full_tags = QW.QCheckBox( self )
self._search_namespaces_into_full_tags.setToolTip( 'If on, a search for "ser" will return all "series:" results such as "series:metrod". On large tag services, these searches are extremely slow.' )
self._namespace_bare_fetch_all_allowed = QW.QCheckBox( self )
self._namespace_bare_fetch_all_allowed.setToolTip( 'If on, a search for "series:" will return all "series:" results. On large tag services, these searches are extremely slow.' )
self._namespace_fetch_all_allowed = QW.QCheckBox( self )
self._namespace_fetch_all_allowed.setToolTip( 'If on, a search for "series:*" will return all "series:" results. On large tag services, these searches are extremely slow.' )
@ -87,6 +90,7 @@ class EditTagAutocompleteOptionsPanel( ClientGUIScrolledPanels.EditPanel ):
self._override_write_autocomplete_file_domain.setChecked( tag_autocomplete_options.OverridesWriteAutocompleteFileDomain() )
self._write_autocomplete_file_domain.SetValue( tag_autocomplete_options.GetWriteAutocompleteFileDomain() )
self._search_namespaces_into_full_tags.setChecked( tag_autocomplete_options.SearchNamespacesIntoFullTags() )
self._namespace_bare_fetch_all_allowed.setChecked( tag_autocomplete_options.NamespaceBareFetchAllAllowed() )
self._namespace_fetch_all_allowed.setChecked( tag_autocomplete_options.NamespaceFetchAllAllowed() )
self._fetch_all_allowed.setChecked( tag_autocomplete_options.FetchAllAllowed() )
@ -107,9 +111,10 @@ class EditTagAutocompleteOptionsPanel( ClientGUIScrolledPanels.EditPanel ):
rows.append( ( 'Manage tags default autocomplete tag domain: ', self._write_autocomplete_tag_domain ) )
rows.append( ( 'Search namespaces into full tags: ', self._search_namespaces_into_full_tags ) )
rows.append( ( 'Search "namespace:*": ', self._namespace_fetch_all_allowed ) )
rows.append( ( 'Search "*": ', self._fetch_all_allowed ) )
rows.append( ( 'Search namespaces with normal input: ', self._search_namespaces_into_full_tags ) )
rows.append( ( 'Allow "namespace:": ', self._namespace_bare_fetch_all_allowed ) )
rows.append( ( 'Allow "namespace:*": ', self._namespace_fetch_all_allowed ) )
rows.append( ( 'Allow "*": ', self._fetch_all_allowed ) )
gridbox = ClientGUICommon.WrapInGrid( self, rows )
@ -128,12 +133,45 @@ class EditTagAutocompleteOptionsPanel( ClientGUIScrolledPanels.EditPanel ):
self._UpdateControls()
self._override_write_autocomplete_file_domain.stateChanged.connect( self._UpdateControls )
self._search_namespaces_into_full_tags.stateChanged.connect( self._UpdateControls )
self._namespace_bare_fetch_all_allowed.stateChanged.connect( self._UpdateControls )
def _UpdateControls( self ):
self._write_autocomplete_file_domain.setEnabled( self._override_write_autocomplete_file_domain.isChecked() )
if self._search_namespaces_into_full_tags.isChecked():
self._namespace_bare_fetch_all_allowed.setEnabled( False )
self._namespace_fetch_all_allowed.setEnabled( False )
else:
self._namespace_bare_fetch_all_allowed.setEnabled( True )
if self._namespace_bare_fetch_all_allowed.isChecked():
self._namespace_fetch_all_allowed.setEnabled( False )
else:
self._namespace_fetch_all_allowed.setEnabled( True )
for c in ( self._namespace_bare_fetch_all_allowed, self._namespace_fetch_all_allowed ):
if not c.isEnabled():
c.blockSignals( True )
c.setChecked( True )
c.blockSignals( False )
def GetValue( self ):
@ -143,6 +181,7 @@ class EditTagAutocompleteOptionsPanel( ClientGUIScrolledPanels.EditPanel ):
override_write_autocomplete_file_domain = self._override_write_autocomplete_file_domain.isChecked()
write_autocomplete_file_domain = self._write_autocomplete_file_domain.GetValue()
search_namespaces_into_full_tags = self._search_namespaces_into_full_tags.isChecked()
namespace_bare_fetch_all_allowed = self._namespace_bare_fetch_all_allowed.isChecked()
namespace_fetch_all_allowed = self._namespace_fetch_all_allowed.isChecked()
fetch_all_allowed = self._fetch_all_allowed.isChecked()
@ -151,6 +190,7 @@ class EditTagAutocompleteOptionsPanel( ClientGUIScrolledPanels.EditPanel ):
override_write_autocomplete_file_domain,
write_autocomplete_file_domain,
search_namespaces_into_full_tags,
namespace_bare_fetch_all_allowed,
namespace_fetch_all_allowed,
fetch_all_allowed
)

View File

@ -857,7 +857,7 @@ class FileSeed( HydrusSerialisable.SerialisableBase ):
self.WriteContentUpdates()
except HydrusExceptions.MimeException as e:
except HydrusExceptions.UnsupportedFileException as e:
self.SetStatus( CC.STATUS_ERROR, exception = e )

View File

@ -787,7 +787,7 @@ class FileImportOptions( HydrusSerialisable.SerialisableBase ):
self._exclude_deleted = True
self._do_not_check_known_urls_before_importing = False
self._do_not_check_hashes_before_importing = False
self._allow_decompression_bombs = False
self._allow_decompression_bombs = True
self._min_size = None
self._max_size = None
self._max_gif_size = None
@ -840,7 +840,7 @@ class FileImportOptions( HydrusSerialisable.SerialisableBase ):
max_size = None
max_resolution = None
allow_decompression_bombs = False
allow_decompression_bombs = True
max_gif_size = 32 * 1048576
pre_import_options = ( exclude_deleted, allow_decompression_bombs, min_size, max_size, max_gif_size, min_resolution, max_resolution )
@ -888,17 +888,17 @@ class FileImportOptions( HydrusSerialisable.SerialisableBase ):
if self._min_size is not None and size < self._min_size:
raise HydrusExceptions.SizeException( 'File was ' + HydrusData.ToHumanBytes( size ) + ' but the lower limit is ' + HydrusData.ToHumanBytes( self._min_size ) + '.' )
raise HydrusExceptions.FileSizeException( 'File was ' + HydrusData.ToHumanBytes( size ) + ' but the lower limit is ' + HydrusData.ToHumanBytes( self._min_size ) + '.' )
if self._max_size is not None and size > self._max_size:
raise HydrusExceptions.SizeException( 'File was ' + HydrusData.ToHumanBytes( size ) + ' but the upper limit is ' + HydrusData.ToHumanBytes( self._max_size ) + '.' )
raise HydrusExceptions.FileSizeException( 'File was ' + HydrusData.ToHumanBytes( size ) + ' but the upper limit is ' + HydrusData.ToHumanBytes( self._max_size ) + '.' )
if mime == HC.IMAGE_GIF and self._max_gif_size is not None and size > self._max_gif_size:
raise HydrusExceptions.SizeException( 'File was ' + HydrusData.ToHumanBytes( size ) + ' but the upper limit for gifs is ' + HydrusData.ToHumanBytes( self._max_gif_size ) + '.' )
raise HydrusExceptions.FileSizeException( 'File was ' + HydrusData.ToHumanBytes( size ) + ' but the upper limit for gifs is ' + HydrusData.ToHumanBytes( self._max_gif_size ) + '.' )
if self._min_resolution is not None:
@ -910,7 +910,7 @@ class FileImportOptions( HydrusSerialisable.SerialisableBase ):
if too_thin or too_short:
raise HydrusExceptions.SizeException( 'File had resolution ' + HydrusData.ConvertResolutionToPrettyString( ( width, height ) ) + ' but the lower limit is ' + HydrusData.ConvertResolutionToPrettyString( self._min_resolution ) )
raise HydrusExceptions.FileSizeException( 'File had resolution ' + HydrusData.ConvertResolutionToPrettyString( ( width, height ) ) + ' but the lower limit is ' + HydrusData.ConvertResolutionToPrettyString( self._min_resolution ) )
@ -923,7 +923,7 @@ class FileImportOptions( HydrusSerialisable.SerialisableBase ):
if too_wide or too_tall:
raise HydrusExceptions.SizeException( 'File had resolution ' + HydrusData.ConvertResolutionToPrettyString( ( width, height ) ) + ' but the upper limit is ' + HydrusData.ConvertResolutionToPrettyString( self._max_resolution ) )
raise HydrusExceptions.FileSizeException( 'File had resolution ' + HydrusData.ConvertResolutionToPrettyString( ( width, height ) ) + ' but the upper limit is ' + HydrusData.ConvertResolutionToPrettyString( self._max_resolution ) )
@ -943,20 +943,20 @@ class FileImportOptions( HydrusSerialisable.SerialisableBase ):
if possible_mime == HC.IMAGE_GIF and self._max_gif_size is not None and num_bytes > self._max_gif_size:
raise HydrusExceptions.SizeException( error_prefix + HydrusData.ToHumanBytes( num_bytes ) + ' but the upper limit for gifs is ' + HydrusData.ToHumanBytes( self._max_gif_size ) + '.' )
raise HydrusExceptions.FileSizeException( error_prefix + HydrusData.ToHumanBytes( num_bytes ) + ' but the upper limit for gifs is ' + HydrusData.ToHumanBytes( self._max_gif_size ) + '.' )
if self._max_size is not None and num_bytes > self._max_size:
raise HydrusExceptions.SizeException( error_prefix + HydrusData.ToHumanBytes( num_bytes ) + ' but the upper limit is ' + HydrusData.ToHumanBytes( self._max_size ) + '.' )
raise HydrusExceptions.FileSizeException( error_prefix + HydrusData.ToHumanBytes( num_bytes ) + ' but the upper limit is ' + HydrusData.ToHumanBytes( self._max_size ) + '.' )
if is_complete_file_size:
if self._min_size is not None and num_bytes < self._min_size:
raise HydrusExceptions.SizeException( error_prefix + HydrusData.ToHumanBytes( num_bytes ) + ' but the lower limit is ' + HydrusData.ToHumanBytes( self._min_size ) + '.' )
raise HydrusExceptions.FileSizeException( error_prefix + HydrusData.ToHumanBytes( num_bytes ) + ' but the lower limit is ' + HydrusData.ToHumanBytes( self._min_size ) + '.' )

View File

@ -123,6 +123,13 @@ class FileViewingStatsManager( object ):
self.media_views += media_views_delta
self.media_viewtime += media_viewtime_delta
elif action == HC.CONTENT_UPDATE_DELETE:
self.preview_views = 0
self.preview_viewtime = 0
self.media_views = 0
self.media_viewtime = 0
@staticmethod

View File

@ -1344,7 +1344,7 @@ class NetworkJob( object ):
trace = traceback.format_exc()
if not isinstance( e, ( HydrusExceptions.NetworkInfrastructureException, HydrusExceptions.StreamTimeoutException, HydrusExceptions.SizeException ) ):
if not isinstance( e, ( HydrusExceptions.NetworkInfrastructureException, HydrusExceptions.StreamTimeoutException, HydrusExceptions.FileSizeException ) ):
HydrusData.Print( trace )

View File

@ -73,7 +73,7 @@ options = {}
# Misc
NETWORK_VERSION = 18
SOFTWARE_VERSION = 398
SOFTWARE_VERSION = 399
CLIENT_API_VERSION = 11
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -86,9 +86,25 @@ def ConvertIntToPixels( i ):
elif i == 1000000: return 'megapixels'
else: return 'megapixels'
def ConvertIntToPrettyOrdinalString( num ):
def ConvertIndexToPrettyOrdinalString( index: int ):
remainder = num % 10
if index >= 0:
return ConvertIntToPrettyOrdinalString( index + 1 )
else:
return ConvertIntToPrettyOrdinalString( index )
def ConvertIntToPrettyOrdinalString( num: int ):
if num == 0:
return 'unknown position'
remainder = abs( num ) % 10
if remainder == 1:
@ -107,7 +123,14 @@ def ConvertIntToPrettyOrdinalString( num ):
ordinal = 'th'
return ToHumanInt( num ) + ordinal
s = '{}{}'.format( ToHumanInt( abs( num ) ), ordinal )
if num < 0:
s = '{} from last'.format( s )
return s
def ConvertIntToUnit( unit ):
@ -1603,9 +1626,16 @@ class ContentUpdate( object ):
elif self._data_type == HC.CONTENT_TYPE_FILE_VIEWING_STATS:
( hash, preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) = self._row
hashes = { hash }
if self._action == HC.CONTENT_UPDATE_ADD:
( hash, preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) = self._row
hashes = { hash }
elif self._action == HC.CONTENT_UPDATE_DELETE:
hashes = self._row
if not isinstance( hashes, set ):

View File

@ -5,10 +5,10 @@ class HydrusException( Exception ):
def __str__( self ):
s = []
if isinstance( self.args, collections.abc.Iterable ):
s = []
for arg in self.args:
try:
@ -43,10 +43,15 @@ class ShutdownException( HydrusException ): pass
class QtDeadWindowException(HydrusException): pass
class VetoException( HydrusException ): pass
class CancelledException( VetoException ): pass
class MimeException( VetoException ): pass
class SizeException( VetoException ): pass
class DecompressionBombException( SizeException ): pass
class UnsupportedFileException( VetoException ): pass
class DamagedOrUnusualFileException( UnsupportedFileException ): pass
class FileSizeException( UnsupportedFileException ): pass
class DecompressionBombException( FileSizeException ): pass
class TagSizeException( VetoException ): pass
class ParseException( HydrusException ): pass
class StringConvertException( ParseException ): pass

View File

@ -139,7 +139,7 @@ def GetFileInfo( path, mime = None, ok_to_look_for_hydrus_updates = False ):
if size == 0:
raise HydrusExceptions.SizeException( 'File is of zero length!' )
raise HydrusExceptions.FileSizeException( 'File is of zero length!' )
if mime is None:
@ -151,15 +151,15 @@ def GetFileInfo( path, mime = None, ok_to_look_for_hydrus_updates = False ):
if mime == HC.TEXT_HTML:
raise HydrusExceptions.MimeException( 'Looks like HTML -- maybe the client needs to be taught how to parse this?' )
raise HydrusExceptions.UnsupportedFileException( 'Looks like HTML -- maybe the client needs to be taught how to parse this?' )
elif mime == HC.APPLICATION_UNKNOWN:
raise HydrusExceptions.MimeException( 'Unknown filetype!' )
raise HydrusExceptions.UnsupportedFileException( 'Unknown filetype!' )
else:
raise HydrusExceptions.MimeException( 'Filetype is not permitted!' )
raise HydrusExceptions.UnsupportedFileException( 'Filetype is not permitted!' )
@ -266,7 +266,7 @@ def GetMime( path, ok_to_look_for_hydrus_updates = False ):
if size == 0:
raise HydrusExceptions.SizeException( 'File is of zero length!' )
raise HydrusExceptions.FileSizeException( 'File is of zero length!' )
with open( path, 'rb' ) as f:
@ -316,7 +316,7 @@ def GetMime( path, ok_to_look_for_hydrus_updates = False ):
return mime
except HydrusExceptions.MimeException:
except HydrusExceptions.UnsupportedFileException:
pass

View File

@ -42,6 +42,17 @@ def EnableLoadTruncatedImages():
return False
if not hasattr( PILImage, 'DecompressionBombError' ):
# super old versions don't have this, so let's just make a stub, wew
class DBE_stub( Exception ):
pass
PILImage.DecompressionBombError = DBE_stub
if not hasattr( PILImage, 'DecompressionBombWarning' ):
# super old versions don't have this, so let's just make a stub, wew
@ -53,8 +64,8 @@ if not hasattr( PILImage, 'DecompressionBombWarning' ):
PILImage.DecompressionBombWarning = DBW_stub
warnings.simplefilter( 'ignore', PILImage.DecompressionBombWarning )
warnings.simplefilter( 'ignore', PILImage.DecompressionBombError )
OLD_PIL_MAX_IMAGE_PIXELS = PILImage.MAX_IMAGE_PIXELS
PILImage.MAX_IMAGE_PIXELS = None # this turns off decomp check entirely, wew
@ -246,7 +257,7 @@ def GeneratePILImage( path ):
except Exception as e:
raise HydrusExceptions.MimeException( 'Could not load the image--it was likely malformed!' )
raise HydrusExceptions.DamagedOrUnusualFileException( 'Could not load the image--it was likely malformed!' )
if pil_image.format == 'JPEG' and hasattr( pil_image, '_getexif' ):
@ -690,24 +701,32 @@ def GetTimesToPlayGIFFromPIL( pil_image ):
def IsDecompressionBomb( path ):
# I boosted this up x2 as a temp test
PILImage.MAX_IMAGE_PIXELS = OLD_PIL_MAX_IMAGE_PIXELS * 2
# there are two errors here, the 'Warning' and the 'Error', which atm is just a test vs a test x 2 for number of pixels
# 256MB bmp by default, ( 1024 ** 3 ) // 4 // 3
# we'll set it at 512MB, and now catching error should be about 1GB
warnings.simplefilter( 'error', PILImage.DecompressionBombWarning )
PILImage.MAX_IMAGE_PIXELS = ( 1024 ** 3 ) // 2 // 3
warnings.simplefilter( 'error', PILImage.DecompressionBombError )
try:
GeneratePILImage( path )
except ( PILImage.DecompressionBombWarning, PILImage.DecompressionBombError ):
except ( PILImage.DecompressionBombError ):
return True
except:
# pil was unable to load it, which does not mean it was a decomp bomb
return False
finally:
PILImage.MAX_IMAGE_PIXELS = None
warnings.simplefilter( 'ignore', PILImage.DecompressionBombWarning )
warnings.simplefilter( 'ignore', PILImage.DecompressionBombError )
return False

View File

@ -177,7 +177,7 @@ def CheckTagNotEmpty( tag ):
if subtag == '':
raise HydrusExceptions.SizeException( 'Received a zero-length tag!' )
raise HydrusExceptions.TagSizeException( 'Received a zero-length tag!' )
def CleanTag( tag ):
@ -239,7 +239,7 @@ def CleanTags( tags ):
CheckTagNotEmpty( tag )
except HydrusExceptions.SizeException:
except HydrusExceptions.TagSizeException:
continue

View File

@ -27,7 +27,7 @@ def CheckFFMPEGError( lines ):
if len( lines ) == 0:
raise HydrusExceptions.MimeException( 'Could not parse that file--no FFMPEG output given.' )
raise HydrusExceptions.DamagedOrUnusualFileException( 'Could not parse that file--no FFMPEG output given.' )
if "No such file or directory" in lines[-1]:
@ -37,7 +37,7 @@ def CheckFFMPEGError( lines ):
if 'Invalid data' in lines[-1]:
raise HydrusExceptions.MimeException( 'FFMPEG could not parse.' )
raise HydrusExceptions.DamagedOrUnusualFileException( 'FFMPEG could not parse.' )
def GetFFMPEGVersion():
@ -219,7 +219,7 @@ def GetFFMPEGVideoProperties( path, force_count_frames_manually = False ):
if not has_video:
raise HydrusExceptions.MimeException( 'File did not appear to have a video stream!' )
raise HydrusExceptions.DamagedOrUnusualFileException( 'File did not appear to have a video stream!' )
resolution = ParseFFMPEGVideoResolution( first_second_lines )
@ -298,7 +298,7 @@ def GetMime( path ):
mime_text = ParseFFMPEGMimeText( lines )
except HydrusExceptions.MimeException:
except HydrusExceptions.UnsupportedFileException:
return HC.APPLICATION_UNKNOWN
@ -462,7 +462,7 @@ def ParseFFMPEGDuration( lines ):
except:
raise HydrusExceptions.MimeException( 'Error reading duration!' )
raise HydrusExceptions.DamagedOrUnusualFileException( 'Error reading duration!' )
def ParseFFMPEGFPS( first_second_lines ):
@ -549,7 +549,7 @@ def ParseFFMPEGFPS( first_second_lines ):
except:
raise HydrusExceptions.MimeException( 'Error estimating framerate!' )
raise HydrusExceptions.DamagedOrUnusualFileException( 'Error estimating framerate!' )
def ParseFFMPEGHasVideo( lines ):
@ -558,7 +558,7 @@ def ParseFFMPEGHasVideo( lines ):
video_line = ParseFFMPEGVideoLine( lines )
except HydrusExceptions.MimeException:
except HydrusExceptions.UnsupportedFileException:
return False
@ -581,7 +581,7 @@ def ParseFFMPEGMimeText( lines ):
except:
raise HydrusExceptions.MimeException( 'Error reading mime!' )
raise HydrusExceptions.DamagedOrUnusualFileException( 'Error reading mime!' )
def ParseFFMPEGNumFramesManually( lines ):
@ -590,7 +590,7 @@ def ParseFFMPEGNumFramesManually( lines ):
if len( frame_lines ) == 0:
raise HydrusExceptions.MimeException( 'Video appears to be broken and non-renderable--perhaps a damaged single-frame video?' )
raise HydrusExceptions.DamagedOrUnusualFileException( 'Video appears to be broken and non-renderable--perhaps a damaged single-frame video?' )
final_line = frame_lines[-1] # there will be many progress rows, counting up as the file renders. we hence want the final one
@ -612,7 +612,7 @@ def ParseFFMPEGNumFramesManually( lines ):
except:
raise HydrusExceptions.MimeException( 'Video was unable to render correctly--could not parse ffmpeg output line: "{}"'.format( final_line ) )
raise HydrusExceptions.DamagedOrUnusualFileException( 'Video was unable to render correctly--could not parse ffmpeg output line: "{}"'.format( final_line ) )
return num_frames
@ -623,7 +623,7 @@ def ParseFFMPEGVideoFormat( lines ):
line = ParseFFMPEGVideoLine( lines )
except HydrusExceptions.MimeException:
except HydrusExceptions.UnsupportedFileException:
return ( False, 'unknown' )
@ -649,7 +649,7 @@ def ParseFFMPEGVideoLine( lines ):
if len( lines_video ) == 0:
raise HydrusExceptions.MimeException( 'Could not find video information!' )
raise HydrusExceptions.DamagedOrUnusualFileException( 'Could not find video information!' )
line = lines_video[0]
@ -693,7 +693,7 @@ def ParseFFMPEGVideoResolution( lines ):
except:
raise HydrusExceptions.MimeException( 'Error parsing resolution!' )
raise HydrusExceptions.DamagedOrUnusualFileException( 'Error parsing resolution!' )
# This was built from moviepy's FFMPEG_VideoReader

View File

@ -283,7 +283,7 @@ class TestFileImportOptions( unittest.TestCase ):
file_import_options.CheckFileIsValid( 65536, HC.IMAGE_JPEG, 640, 480 )
with self.assertRaises( HydrusExceptions.SizeException ):
with self.assertRaises( HydrusExceptions.FileSizeException ):
file_import_options.CheckFileIsValid( 512, HC.IMAGE_JPEG, 640, 480 )
@ -297,7 +297,7 @@ class TestFileImportOptions( unittest.TestCase ):
file_import_options.CheckFileIsValid( 1800, HC.IMAGE_JPEG, 640, 480 )
with self.assertRaises( HydrusExceptions.SizeException ):
with self.assertRaises( HydrusExceptions.FileSizeException ):
file_import_options.CheckFileIsValid( 2200, HC.IMAGE_JPEG, 640, 480 )
@ -314,7 +314,7 @@ class TestFileImportOptions( unittest.TestCase ):
file_import_options.CheckFileIsValid( 1800, HC.IMAGE_GIF, 640, 480 )
with self.assertRaises( HydrusExceptions.SizeException ):
with self.assertRaises( HydrusExceptions.FileSizeException ):
file_import_options.CheckFileIsValid( 2200, HC.IMAGE_GIF, 640, 480 )
@ -328,12 +328,12 @@ class TestFileImportOptions( unittest.TestCase ):
file_import_options.CheckFileIsValid( 65536, HC.IMAGE_JPEG, 640, 480 )
with self.assertRaises( HydrusExceptions.SizeException ):
with self.assertRaises( HydrusExceptions.FileSizeException ):
file_import_options.CheckFileIsValid( 65536, HC.IMAGE_JPEG, 180, 480 )
with self.assertRaises( HydrusExceptions.SizeException ):
with self.assertRaises( HydrusExceptions.FileSizeException ):
file_import_options.CheckFileIsValid( 65536, HC.IMAGE_JPEG, 640, 80 )
@ -349,12 +349,12 @@ class TestFileImportOptions( unittest.TestCase ):
file_import_options.CheckFileIsValid( 65536, HC.IMAGE_JPEG, 640, 480 )
with self.assertRaises( HydrusExceptions.SizeException ):
with self.assertRaises( HydrusExceptions.FileSizeException ):
file_import_options.CheckFileIsValid( 65536, HC.IMAGE_JPEG, 3200, 480 )
with self.assertRaises( HydrusExceptions.SizeException ):
with self.assertRaises( HydrusExceptions.FileSizeException ):
file_import_options.CheckFileIsValid( 65536, HC.IMAGE_JPEG, 640, 4200 )

View File

@ -579,14 +579,17 @@ class TestTagObjects( unittest.TestCase ):
tag_autocomplete_options = ClientTags.TagAutocompleteOptions( CC.COMBINED_TAG_SERVICE_KEY )
namespace_fetch_all_allowed = True
search_namespaces_into_full_tags = True
namespace_bare_fetch_all_allowed = False
namespace_fetch_all_allowed = False
fetch_all_allowed = False
tag_autocomplete_options.SetTuple(
tag_autocomplete_options.GetWriteAutocompleteTagDomain(),
tag_autocomplete_options.OverridesWriteAutocompleteFileDomain(),
tag_autocomplete_options.GetWriteAutocompleteFileDomain(),
tag_autocomplete_options.SearchNamespacesIntoFullTags(),
search_namespaces_into_full_tags,
namespace_bare_fetch_all_allowed,
namespace_fetch_all_allowed,
fetch_all_allowed
)
@ -621,6 +624,12 @@ class TestTagObjects( unittest.TestCase ):
#
parsed_autocomplete_text = ClientSearch.ParsedAutocompleteText( 'series:', tag_autocomplete_options, True )
bool_tests( parsed_autocomplete_text, [ True, True, False, False, True, False, True ] )
#
parsed_autocomplete_text = ClientSearch.ParsedAutocompleteText( 'series:*', tag_autocomplete_options, True )
bool_tests( parsed_autocomplete_text, [ True, True, False, True, True, False, True ] )
@ -630,6 +639,128 @@ class TestTagObjects( unittest.TestCase ):
tag_autocomplete_options = ClientTags.TagAutocompleteOptions( CC.COMBINED_TAG_SERVICE_KEY )
search_namespaces_into_full_tags = False
namespace_bare_fetch_all_allowed = True
namespace_fetch_all_allowed = False
fetch_all_allowed = False
tag_autocomplete_options.SetTuple(
tag_autocomplete_options.GetWriteAutocompleteTagDomain(),
tag_autocomplete_options.OverridesWriteAutocompleteFileDomain(),
tag_autocomplete_options.GetWriteAutocompleteFileDomain(),
search_namespaces_into_full_tags,
namespace_bare_fetch_all_allowed,
namespace_fetch_all_allowed,
fetch_all_allowed
)
parsed_autocomplete_text = ClientSearch.ParsedAutocompleteText( '', tag_autocomplete_options, True )
bool_tests( parsed_autocomplete_text, [ False, False, True, False, False, False, True ] )
#
parsed_autocomplete_text = ClientSearch.ParsedAutocompleteText( '-', tag_autocomplete_options, True )
bool_tests( parsed_autocomplete_text, [ False, False, False, False, False, False, False ] )
#
parsed_autocomplete_text = ClientSearch.ParsedAutocompleteText( 'samus', tag_autocomplete_options, True )
bool_tests( parsed_autocomplete_text, [ True, True, False, False, False, True, True ] )
#
parsed_autocomplete_text = ClientSearch.ParsedAutocompleteText( '*', tag_autocomplete_options, True )
bool_tests( parsed_autocomplete_text, [ False, False, False, True, False, False, True ] )
#
parsed_autocomplete_text = ClientSearch.ParsedAutocompleteText( '*:*', tag_autocomplete_options, True )
bool_tests( parsed_autocomplete_text, [ False, False, False, True, False, False, True ] )
#
parsed_autocomplete_text = ClientSearch.ParsedAutocompleteText( 'series:', tag_autocomplete_options, True )
bool_tests( parsed_autocomplete_text, [ True, True, False, False, True, False, True ] )
#
parsed_autocomplete_text = ClientSearch.ParsedAutocompleteText( 'series:*', tag_autocomplete_options, True )
bool_tests( parsed_autocomplete_text, [ True, True, False, True, True, False, True ] )
#
#
tag_autocomplete_options = ClientTags.TagAutocompleteOptions( CC.COMBINED_TAG_SERVICE_KEY )
search_namespaces_into_full_tags = False
namespace_bare_fetch_all_allowed = False
namespace_fetch_all_allowed = True
fetch_all_allowed = False
tag_autocomplete_options.SetTuple(
tag_autocomplete_options.GetWriteAutocompleteTagDomain(),
tag_autocomplete_options.OverridesWriteAutocompleteFileDomain(),
tag_autocomplete_options.GetWriteAutocompleteFileDomain(),
search_namespaces_into_full_tags,
namespace_bare_fetch_all_allowed,
namespace_fetch_all_allowed,
fetch_all_allowed
)
parsed_autocomplete_text = ClientSearch.ParsedAutocompleteText( '', tag_autocomplete_options, True )
bool_tests( parsed_autocomplete_text, [ False, False, True, False, False, False, True ] )
#
parsed_autocomplete_text = ClientSearch.ParsedAutocompleteText( '-', tag_autocomplete_options, True )
bool_tests( parsed_autocomplete_text, [ False, False, False, False, False, False, False ] )
#
parsed_autocomplete_text = ClientSearch.ParsedAutocompleteText( 'samus', tag_autocomplete_options, True )
bool_tests( parsed_autocomplete_text, [ True, True, False, False, False, True, True ] )
#
parsed_autocomplete_text = ClientSearch.ParsedAutocompleteText( '*', tag_autocomplete_options, True )
bool_tests( parsed_autocomplete_text, [ False, False, False, True, False, False, True ] )
#
parsed_autocomplete_text = ClientSearch.ParsedAutocompleteText( '*:*', tag_autocomplete_options, True )
bool_tests( parsed_autocomplete_text, [ False, False, False, True, False, False, True ] )
#
parsed_autocomplete_text = ClientSearch.ParsedAutocompleteText( 'series:', tag_autocomplete_options, True )
bool_tests( parsed_autocomplete_text, [ True, False, False, False, True, False, True ] )
#
parsed_autocomplete_text = ClientSearch.ParsedAutocompleteText( 'series:*', tag_autocomplete_options, True )
bool_tests( parsed_autocomplete_text, [ True, True, False, True, True, False, True ] )
#
#
tag_autocomplete_options = ClientTags.TagAutocompleteOptions( CC.COMBINED_TAG_SERVICE_KEY )
search_namespaces_into_full_tags = False
namespace_bare_fetch_all_allowed = False
namespace_fetch_all_allowed = True
fetch_all_allowed = True
@ -637,7 +768,8 @@ class TestTagObjects( unittest.TestCase ):
tag_autocomplete_options.GetWriteAutocompleteTagDomain(),
tag_autocomplete_options.OverridesWriteAutocompleteFileDomain(),
tag_autocomplete_options.GetWriteAutocompleteFileDomain(),
tag_autocomplete_options.SearchNamespacesIntoFullTags(),
search_namespaces_into_full_tags,
namespace_bare_fetch_all_allowed,
namespace_fetch_all_allowed,
fetch_all_allowed
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB