Version 564

closes #1468
This commit is contained in:
Hydrus Network Developer 2024-02-28 15:36:43 -06:00
parent 7444f20834
commit 9984879c68
No known key found for this signature in database
GPG Key ID: 76249F053212133C
29 changed files with 1114 additions and 702 deletions

View File

@ -72,7 +72,11 @@ Swapiness is a setting you might have seen, but it only determines Linux's desir
## Why does my Linux system studder or become unresponsive when hydrus has been running a while?
You are running out of pages because Linux releases I/O buffer pages only when a file is closed. Thus the OS is waiting for you to hit the watermark(as described in "why is hydrus crashing") to start freeing pages, which causes the chug. When contents is written from memory to disk the page is retained so that if you reread that part of the disk the OS does not need to access disk it just pulls it from the much faster memory. This is usually a good thing, but Hydrus does not close database files so it eats up pages over time. This is really good for hydrus but sucks for the responsiveness of other apps, and will cause hydrus to consume pages after doing a lengthy operation in anticipation of needing them again, even when it is thereafter idle. You need to set `vm.dirtytime_expire_seconds` to a lower value.
You are running out of pages because Linux releases I/O buffer pages only when a file is closed, OR memory fragmentation in Hydrus is high because you have a big session weight or had a big I/O spike. Thus the OS is waiting for you to hit the watermark(as described in "why is hydrus crashing") to start freeing pages, which causes the chug.
When contents is written from memory to disk the page is retained so that if you reread that part of the disk the OS does not need to access disk it just pulls it from the much faster memory. This is usually a good thing, but Hydrus makes many small writes to files you probably wont be asking for again soon it eats up pages over time.
Hydrus also holds the database open and red/wrires new areas to it often even if it will not acess those parts again for ages. It tends to accumulate lots of I/O cache for these small pages it will not be interested in. This is really good for hydrus (because it will over time have the root of the most important indexes in memory) but sucks for the responsiveness of other apps, and will cause hydrus to consume pages after doing a lengthy operation in anticipation of needing them again, even when it is thereafter idle. You need to set `vm.dirtytime_expire_seconds` to a lower value.
> `vm.dirtytime_expire_seconds`
> When a lazytime inode is constantly having its pages dirtied, the inode with
@ -89,24 +93,30 @@ https://www.kernel.org/doc/Documentation/sysctl/vm.txt
## Why does everything become clunky for a bit if I have tuned all of the above settings?
## Why does everything become clunky for a bit if I have tuned all of the above settings? (especially if I try to do something on the system that isn't hydrus)
The kernel launches a process called `kswapd` to swap and reclaim memory pages, its behaviour is goverened by the following two values
The kernel launches a process called `kswapd` to swap and reclaim memory pages, after hydrus has used pages they need to be returned to the OS (unless fragmentation is preventing this). The OS needs to scan for pages allocated to programs which are not in use, it doens't do this all the time because holding the required locks would have a serious performance impact. The behaviour of `kswapd` is goverened by several important values. If you are using a classic system with a reasonably sized amount of memoery and a swapfile you should tune these. If you are using memory compression (or should be using memory compression because you have a cheap system) read this whole document for info specific to that configuration.
- `vm.vfs_cache_pressure` The tendancy for the kernel to reclaim I/O cache for files and directories. Default=100, set to 110 to bias the kernel into reclaiming I/O pages over keeping them at a "fair rate" compared to other pages. Hydrus tends to write a lot of files and then ignore them for a long time, so its a good idea to prefer freeing pages for infrequent I/O.
**Note**: Increasing `vfs_cache_pressure` significantly beyond 100 may have negative performance impact. Reclaim code needs to take various locks to find freeable directory and inode objects. With `vfs_cache_pressure=1000`, it will look for ten times more freeable objects than there are.
- `watermark_scale_factor`
- `vm.watermark_scale_factor`
This factor controls the aggressiveness of kswapd. It defines the amount of memory left in a node/system before kswapd is woken up and how much memory needs to be free before kswapd goes back to sleep. The unit is in fractions of 10,000. The default value of 10 means the distances between watermarks are 0.1% of the available memory in the node/system. The maximum value is 1000, or 10% of memory. A high rate of threads entering direct reclaim (allocstall) or kswapd going to sleep prematurely (kswapd_low_wmark_hit_quickly) can indicate that the number of free pages kswapd maintains for latency reasons is too small for the allocation bursts occurring in the system. This knob can then be used to tune kswapd aggressiveness accordingly.
- `vm.watermark_boost_factor`: If memory fragmentation is high raise the scale factor to look for reclaimable/swappable pages more agressively.
I like to keep `watermark_scale_factor` at 70 (70/10,000)=0.7%, so kswapd will run until at least 0.7% of system memory has been reclaimed.
i.e. If 32GiB (real and virt) of memory, it will try to keep at least 0.224 GiB immediately available.
- `vm.dirty_ratio`: The absolute maximum number of un-synced memory(as a percentage of available memory) that the system will buffer before blocking writing processes. This **protects you against OOM, but does not keep your system responsive**.
- **Note**: A default installation of Ubuntu sets this way too high (60%) as it does not expect your workload to just be hammering possibly slow disks with written pages. **Even with memory overcomitting this can make you OOM**, because you will run out of real memory before the system pauses the program that is writing so hard. A more reasonable value is 10 (10%)
- `vm.dirty_background_ratio`: How many the number of unsynced pages that can exist before the system starts comitting them in the background. If this is set too low the system will constantly spend cycles trying to write out dirty pages. If it is set too high it will be way to lazy. I like to set it to 8.
- `vm.vfs_cache_pressure` The tendancy for the kernel to reclaim I/O cache for files and directories. This is less important than the other values, but hydrus opens and closes lots of file handles so you may want to boost it a bit higher than default. Default=100, set to 110 to bias the kernel into reclaiming I/O pages over keeping them at a "fair rate" compared to other pages. Hydrus tends to write a lot of files and then ignore them for a long time, so its a good idea to prefer freeing pages for infrequent I/O.
**Note**: Increasing `vfs_cache_pressure` significantly beyond 100 may have negative performance impact. Reclaim code needs to take various locks to find freeable directory and inode objects. With `vfs_cache_pressure=1000`, it will look for ten times more freeable objects than there are.
### Virtual Memory Under Linux 4: Unleash the memory
An example /etc/sysctl.conf section for virtual memory settings.
An example `/etc/sysctl.conf` section for virtual memory settings.
```ini
########
@ -134,3 +144,63 @@ vm.watermark_scale_factor=70
#Don't set this value much over 100 or the kernel will spend all its time reclaiming I/O pages
vm.vfs_cache_pressure=110
```
## Virtual Memory Under Linux 5: Phenomenal Cosmic Power; Itty bitty living space
Are you trying to __run hydrus on a 200 dollar miniPC__, this is suprisingly doable, but you will need to really understand what you are tuning.
To start lets explain memory tiers. As memory moves further away from the CPU it becomes slower. Memory close to the CPU is volatile, which means if you remove power from it, it disappears forever. Conversely disk is called non-volatile memory, or persistant storage. We want to get files written to non-volatile storage, and we don't want to have to compete to read non-volatile storage, we would also prefer to not have to compete for writing, butthis is harder.
The most straight forward way of doing this is to seperate where hydrus writes its SQLITE database(index) files, from where it writes the imported files. But we can make a more flexible setup that will also keep our system responsive, we just need to make sure that the system writes to the fastest possible place first. So let's illustrate the options.
```mermaid
graph
direction LR
CPU-->RAM;
RAM-->ZRAM;
ZRAM-->SSD;
SSD-->HDD;
subgraph Non-Volatile
SSD;
HDD;
end
```
1. **RAM**: Information must be in RAM for it to be operated on
2. **ZRAM**: A compressed area of RAM that cannot be directly accessed. Like a zip file but in memory. Or for the more technical, like a compressed ramdisk.
3. **SSD**: Fast non-volatile storage,good for random access about 100-1000x slower than RAM.
4. **HDD**: Slow non-volatile storage good for random access. About 10000x slowee than RAM.
5. **Tape**(Not shown): Slow archival storage or backup. surprisingly fast actually but can only be accessed sequentially.
The objective is to make the most of our limited hardware so we definitely want to go through zram first. Depending on your configuration you might have a bulk storage (NAS) downstream that you can write the files to, if all of your storage is in the same tower as you are running hydrus, then make sure the SQLITE .db files are on an SSD volume.
Next you should enable [ZRAM](https://wiki.archlinux.org/title/Zram) devices (__**Not to be confused with ZWAP**__). A ZRAM device is a compressed swapfile that lives in RAM.
ZRAM can drastically improve performance, and RAM capacity. Experimentally, a 1.7GB partition usually shrinks to around 740MiB. Depending on your system ZRAM may generate several partitions. The author asked for 4x2GB=8GIB partitions, hence the cited ratio.
**ZRAM must be created every boot as RAM-disks are lost when power is removed**
Install a zram generator as part of your startup process. If you still do not have enough swap, you can still create a swapfile. RAM can be configured to use a partition as fallback, but not a file. However you can enable a standard swapfile as described in the prior section. ZRAM generators usually create ZRAM partitions with the highest priority (lowest priority number) so ZRAM will fill up first, before normal disk swaping.
To check your swap configuration
```sh
swapon #no argument
cat /proc/swapinfo
```
To make maximum use of your swap make sure to **__SET THE FOLLOWING VM SETTINGS__**
```sh
#disable IO clustering we are writing to memroy which is super fast
#IF YOU DO NOT DO THIS YOUR SYSTEM WILL HITCH as it tries to lock multiple RAM pages. This would be desirable on non-volatile storages but is actually bad on RAM.
vm.page-cluster=0
#Tell the system that it costs almost as much to swap as to write out dirty pages. But bias it very slightly to completing writes. This is ideal since hydrus tends to hammer the system with writes, and we want to use ZRAM to eat spikes, but also want the system to slightly prefer writing dirty pages
vm.swappiness=99
```
----
The above is good for most users. If however you also need to speed up your storage due to a high number of applications on your network using it you may wish to install cache, provided you have at least one or two avialable SSD slots, and the writing pattern is many small random writes.
You should never create a write cache without knowing what you are doing. You need two SSDs to crosscheck eachother, and ideally ressilant server SSDs with large capacitors that ensure all content is always written. If you go with a commercial storage solution they will probably check this already, and give you a nice interface for just inserting and assigning SSD cache.
You can also create a cach manually wit the **Logical Volume** using the LVM. If you do this you can group together storage volumes. In particular you can put a [read or write cache](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/logical_volume_manager_administration/lvm_cache_volume_creation) with an SSD in front of slower HDD.

View File

@ -7,6 +7,50 @@ title: Changelog
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
## [Version 564](https://github.com/hydrusnetwork/hydrus/releases/tag/v564)
### more macOS work
### thanks to a user, we have more macOS features
* macOS users get a new shortcut action, default Space, that uses Quick Look to preview a thumbnail like you can in Finder. **all existing users will get the new shortcut!**
* the hydrus .app now has the version number in Get Info
* **macOS users who run from source should rebuild their venvs this week!** if you don't, then trying this new Quick Look feature will just give you an error notification
### new fuzzy operator math in system predicates
* the system predicates for width, height, num_notes, num_words, num_urls, num_frames, duration, and framerate now support two different kinds of approximate equals, ≈: absolute (±x), and percentage (±x%). previously, the ≈ secretly just did ±15% in all cases (issue #1468)
* all `system:framerate=x` searches are now converted to `±5%`, which is what they were behind the scenes. `!=` framerate stuff is no longer supported, so if you happened to use it, it is now converted to `<` just as a valid fallback
* `system:duration` gets the same thing, `±5%`. it wasn't doing this behind the scenes before, but it should have been!
* `system:duration` also now allows hours and minutes input, if you need longer!
* for now, the parsing system is not updated to specify the % or absolute ± values. it will remain the same as the old system, with ±15% as the default for a `~=` input
* there's still a little borked logic in these combined types. if you search `< 3 URLs`, that will return files with 0 URLs, and same for `num_notes`, but if you search `< 200px width` or any of the others I changed this week, that won't return a PDF that has no width (although it will return a damaged file that reports 0 width specifically). I am going to think about this, since there isn't an easy one-size-fits-all-solution to marry what is technically correct with what is actually convenient. I'll probably add a checkbox that says whether to include 'Null' values or not and default that True/False depending on the situation; let me know what you think!
### misc
* I have taken out Space as the default for archive/delete filter 'keep' and duplicate filter 'this is better, delete other'. Space is now exclusively, by default, media pause/play. **I am going to set this to existing users too, deleting/overwriting what Space does for you, if you are still set to the defaults**
* integer percentages are now rendered without the trailing `.0`. `15%`, not `15.0%`
* when you 'open externally', 'open in web browser', or 'open path' from a thumbnail, the preview viewer now pauses rather than clears completely
* fixed the edit shortcut panel ALWAYS showing the new (home/end/left/right/to focus) dropdown for thumbnail dropdown, arrgh
* I fixed a stupid typo that was breaking file repository file deletes
* `help->about` now shows the Qt platformName
* added a note about bad Wayland support to the Linux 'installing' help document
* the guy who wrote the `Fixing_Hydrus_Random_Crashes_Under_Linux` document has updated it with new information, particularly related to running hydrus fast using virtual memory on small, underpowered computers
### client api
* thanks to a user, the undocumented API call that returns info on importer pages now includes the sha256 file hash in each import object Object
* although it is a tiny change, let's nonetheless update the Client API version to 61
### boring predicate overhaul work
* updated the `NumberTest` object to hold specific percentage and absolute ± values
* updated the `NumberTest` object to render itself to any number format, for instance pixels vs kilobytes vs a time delta
* updated the `Predicate` object for system preds width, height, num_notes, num_words, num_urls, num_frames, duration, and framerate to store their operator and value as a `NumberTest`, and updated predicate string rendering, parsing, editing, database-level predicate handling
* wrote new widgets to edit `NumberTest`s of various sorts and spammed them to these (operator, value) system predicate UI panels. we are finally clearing out some 8+-year-old jank here
* rewrote the `num_notes` database search logic to use `NumberTest`s
* the system preds for height, width, and framerate now say 'has x' and 'no x' when set to `>0` or `=0`, although what these really mean is not perfectly defined
## [Version 563](https://github.com/hydrusnetwork/hydrus/releases/tag/v563)
### macOS improvements
@ -418,68 +462,3 @@ title: Changelog
* slimmed down some of the watcher/subscription fixed-checking-time code
* misc formatting cleanup and surplus import clearout
* fixed the discord link in the PTR help document
## [Version 554](https://github.com/hydrusnetwork/hydrus/releases/tag/v554)
### checker options fixes
* **sorry for any jank 'static check interval' watcher or subscription timings you saw last week! I screwed something up and it slipped through testing**
* the 'static check interval' logic is much much simpler. rather than try to always keep to the same check period, even if the actual check is delayed, it just works off 'last check time + period', every time. the clever stuff was generally confusing and failing in a variety of ways
* fixed a bug in the new static check time code that was stopping certain in-limbo watchers from calculating their correct next check time on program load
* fixed a bug in the new static check time code that was causing too many checks in long-paused-and-now-unpaused downloaders
* some new unit tests will make sure these errors do not happen again
* in the checker options UI, if you uncheck 'just check at a static, regular interval', and leave the faster/slower values as the same when you OK, then the dialog now asks you if that is what you want
* in the checker options UI, the 'slower than' value will now automatically update itself to be no smaller than the 'faster than' value
### job status fixes and cleanup (mostly boring)
* **sorry for any 'Cancel/IsCancellable' related errors you saw last week! I screwed something else up**
* fixed a dumb infinite recursion error in the new job status cancellable 'safety' checks that was happening when it was time to auto-dismiss a cancellable job due to program/thread shutdown or a maintenance mode change. this also fixes some non-dismissing popup messages (usually subscriptions) that weren't setting their cancel status correctly
* this happened because the code here was ancient and ugly. I have renamed, simplified, and reworked the logical dangerzone variables and methods in the job status object so we don't run into this problem again. 'Cancel' and 'Finish' no longer take a seconds parameter, 'Delete' is now 'FinishAndDismiss', 'IsDeleted' is now 'IsDismissed', 'IsDeletable' is now merged into a cleaner 'IsDone', 'IsWorking' is removed, 'SetCancellable' and 'SetPausable' are removed (these will always be in the init, and will determine what type of job we have), and the various new Client API calls and help are updated for this
* also, the job status methods now check their backstop 'cancel' tests far less often, and there's a throttle to make sure they can only run once a second anyway
* also ditched the needless threading events for simple bools
* also cleared up about 40 pointless Finish/FinishAndDismiss duplicate calls across the program
* also fixed up the job status object to do its various yield pauses more sanely
### cbz and ugoira detection and thumbnails
* CBZ files are now detected! there is no very strict standard of what is or isn't a CBZ (it is basically just a zip of images and maybe some metadata files), but I threw together a 'yeah that looks like a cbz' test that now runs on every zip. there will probably be several false positives, but with luck fewer false negatives, which I think is the way we want to lean here. if you have just some zip of similarly named images, it'll now be counted as a CBZ, but I think we'll nonetheless want to give those all the upcoming CBZ tech anyway, even if they aren't technically intended to be 'CBZ', whatever that actually means here other than the different file extension
* the client looks for the cover image in your CBZ and uses that for the thumbnail! it also uses this file's resolution as the CBZ resolution
* Ugoira files are now detected! there is a firmer standard of what an Ugoira is, but it is still tricky as we are just talking about a different list of zipped image files here. I expect zero false negatives and some false positives (unfortunately, it'll be CBZs with zero-padded numerical-only filenames). as all ugoiras are valid CBZs but few CBZs are valid ugoiras, the Ugoira test runs first
* the client now gets a thumbnail for Ugoiras. It'll also use the x%-in setting that other animations and videos use! it also fetches resolution and 'num frames'. duration can't be inferred just yet, but we hope to have some options (and actual rendering) happening in the medium-term future
* this is all an experiment. let me know how it goes, and send in any examples of it failing awfully. there is lots more to do. if things don't explode with this much, I'll see about .cbr and cb7, which seems totally doable, and then I can seriously plan out UI for actual view and internal navigation. I can't promise proper reader features like bookmarks or anything, but I'll keep on pushing
* all your existing zips will be scheduled for a filetype re-scan on update
### animations
* the native FFMPEG renderer pipeline is now capable of transparency. APNGs rendered in the native viewer now have correct transparency and can pass 'has transparency' checks
* all your apngs will be scheduled for the 'has transparency' check, just like pngs and gifs and stuff a couple weeks ago. thanks to the user who submitted some transparency-having apngs to test with!
* the thumbnails for animated gifs are now taken using the FFMPEG renderer, which puts them x% in, just like APNG and other video. transparency in these thumbnails also seems to be good! am not going to regen everyone's animated gif thumbs yet--I'll do some more IRL testing--but this will probably come in a few weeks. let me know if you see a bevy of newly imported gifs with crazy thumbs
* I also overhauled the native GIF renderer. what used to be a cobbled-together RGB OpenCV solution with a fallback to bad PIL code is now a proper only-PIL RGBA solution, and the transparency seems to be great now (the OpenCV code had no transparency, and the PIL fallback tried but generally drew the last frame on top of the previous, giving a noclip effect). the new renderer also skips to an unrendered area faster
* given the file maintenance I/O Error problems we had the past couple weeks, I also made this cleaner GIF renderer much more robust; it will generally just rewind itself or give a black frame if it runs into truncation problems, no worries, and for gifs that just have one weird frame that doesn't break seek, it should be able to skip past those now, repeating the last good frame until it hits something valid
* as a side thing, the FFMPEG GIF renderer seems capable of doing almost everything the PIL renderer can now. I can flip the code to using the FFMPEG pipeline and gifs come through fine, transparency included. I prefer the PIL for now, but depending on how things go, I may add options to use the FFMPEG bridge as a testbed/fallback in future
* added some PIL animated gif rendering tech to handle a gif that out of nowhere produces a giga 85171x53524 frame, eating up multiple GB of memory and taking twenty seconds to failrender
* fixed yet another potential source of the false positive I/O Errors caused by the recent 'has transparency' checking, this time not just in malformed animated gif frames, but some busted static images too
* improved the PIL loading code a little more, converting more possible I/O Errors and other weird damaged file states to the correct hydrus-internal exception types with nicer error texts
* the 'disable CV for gifs' option is removed
### file pre-import checks
* the 'is this file free to work on' test that runs before files are added to the manual or import folder file list now has an additional file-open check. this improves reliability over NAS connections, where the file may be used by a remote process, and also improves detection for files where the current user only has read permissions
* import folders now have a 'recent modified time skip period' setting, defaulting to 60 seconds. any file that has a modified date newer than that many seconds ago will not be imported on the current check. this helps to avoid importing files that are currently being downloaded/copied into the folder when the import folder runs (when that folder/download process is otherwise immune to the existing 'already in use' checks)
* import folders now repeat-check folders that have many previously-seen files much faster
### misc
* the 'max gif size' setting in the quiet and loud file import options now defaults to 'no limit'. it used to be 32MB, to catch various trash webm re-encodes, but these days it catches more false positives than it is worth, and 32MB is less of a deal these days too
* the test on boot to see if the given database location is writeable-to should now give an error when that location is on a non--existing location (e.g. a removable usb drive that is not currently plugged in). previously, it could, depending on the situation, either proceed and go crazy later or wait indefinitely on a CPU-heavy busy-wait for the drive to be plugged back in. unfortunately, because at this stage there is no logfile location and no UI, if your custom db dir does not and cannot exist, the program terminates instantly and silently writes a crash log to your desktop. I have made a plan to improve this in future
* also cleaned up all the db_dir boot code generally. the various validity tests should now only happen once per potential location
* the function that converts an export phrase into a filename will now elide long unicode filenames correctly. filenames with complex unicode characters will take more than one byte per character (and most OSes have ~255 byte filename limit), which requires a trickier check. also, on Windows, where there is a 260-character total path limit, the combined directory+filename length is checked better, and just checked on Windows. all errors raised here are better
* added some unit tests to check the new path eliding tech
* brushed up the 'getting started with ratings' help a little
### client api
* thanks to a user, the Client API now has the ability to see and interact with the current popup messages in the popup toaster!
* fixed a stupid typo that I made in the new Client API options call. added a unit test to catch this in future, too
* the client api version is now 57

View File

@ -70,7 +70,7 @@ There are three ways a file can be related to another in the current duplicates
You can customise the shortcuts under _file->shortcuts->duplicate_filter_. The defaults are:
* Left-click or space: **this is better, delete the other**.
* Left-click: **this is better, delete the other**.
* Right-click: **they are related alternates**.

View File

@ -97,7 +97,7 @@ Lets say you just downloaded a good thread, or perhaps you just imported an old
Select some thumbnails, and either choose _filter->archive/delete_ from the right-click menu or hit F12. You will see them in a special version of the media viewer, with the following default controls:
* ++left-button++, ++space++, or ++f7++: **keep and archive the file, move on**
* ++left-button++ or ++f7++: **keep and archive the file, move on**
* ++right-button++ or ++delete++: **delete the file, move on**
* ++up++: **Skip this file, move on**
* ++middle-button++ or ++backspace++: **I didn't mean that, go back one**

View File

@ -37,6 +37,9 @@ I try to release a new version every Wednesday by 8pm EST and write an accompany
* _This release has always been a little buggy. Many macOS users are having better success [running from source](running_from_source.md)._
=== "Linux"
!!! warning "Wayland"
Unfortunately, hydrus has several bad bugs in Wayland. The mpv window will often not embed properly into the media viewer, menus and windows may position on the wrong screen, and the taskbar icon may not work at all. [Running from source](running_from_source.md) may improve the situation, but some of these issues seem to be intractable for now. X11 is much happier with hydrus.
* Get the .tag.gz. Extract it somewhere useful and create shortcuts to 'client' and 'server' as you like. The build is made on Ubuntu, so if you run something else, compatibility is hit and miss.
* If you have problems running the Ubuntu build, users with some python experience generally find running from source works well.

View File

@ -34,6 +34,42 @@
<div class="content">
<h1 id="changelog"><a href="#changelog">changelog</a></h1>
<ul>
<li>
<h2 id="version_564"><a href="#version_564">version 564</a></h2>
<ul>
<li><h3>more macOS work</h3></li>
<li><h3>thanks to a user, we have more macOS features</h3></li>
<li>macOS users get a new shortcut action, default Space, that uses Quick Look to preview a thumbnail like you can in Finder. **all existing users will get the new shortcut!**</li>
<li>the hydrus .app now has the version number in Get Info</li>
<li>**macOS users who run from source should rebuild their venvs this week!** if you don't, then trying this new Quick Look feature will just give you an error notification</li>
<li><h3>new fuzzy operator math in system predicates</h3></li>
<li>the system predicates for width, height, num_notes, num_words, num_urls, num_frames, duration, and framerate now support two different kinds of approximate equals, ≈: absolute (±x), and percentage (±x%). previously, the ≈ secretly just did ±15% in all cases (issue #1468)</li>
<li>all `system:framerate=x` searches are now converted to `±5%`, which is what they were behind the scenes. `!=` framerate stuff is no longer supported, so if you happened to use it, it is now converted to `<` just as a valid fallback</li>
<li>`system:duration` gets the same thing, `±5%`. it wasn't doing this behind the scenes before, but it should have been!</li>
<li>`system:duration` also now allows hours and minutes input, if you need longer!</li>
<li>for now, the parsing system is not updated to specify the % or absolute ± values. it will remain the same as the old system, with ±15% as the default for a `~=` input</li>
<li>there's still a little borked logic in these combined types. if you search `< 3 URLs`, that will return files with 0 URLs, and same for `num_notes`, but if you search `< 200px width` or any of the others I changed this week, that won't return a PDF that has no width (although it will return a damaged file that reports 0 width specifically). I am going to think about this, since there isn't an easy one-size-fits-all-solution to marry what is technically correct with what is actually convenient. I'll probably add a checkbox that says whether to include 'Null' values or not and default that True/False depending on the situation; let me know what you think!</li>
<li><h3>misc</h3></li>
<li>I have taken out Space as the default for archive/delete filter 'keep' and duplicate filter 'this is better, delete other'. Space is now exclusively, by default, media pause/play. **I am going to set this to existing users too, deleting/overwriting what Space does for you, if you are still set to the defaults**</li>
<li>integer percentages are now rendered without the trailing `.0`. `15%`, not `15.0%`</li>
<li>when you 'open externally', 'open in web browser', or 'open path' from a thumbnail, the preview viewer now pauses rather than clears completely</li>
<li>fixed the edit shortcut panel ALWAYS showing the new (home/end/left/right/to focus) dropdown for thumbnail dropdown, arrgh</li>
<li>I fixed a stupid typo that was breaking file repository file deletes</li>
<li>`help->about` now shows the Qt platformName</li>
<li>added a note about bad Wayland support to the Linux 'installing' help document</li>
<li>the guy who wrote the `Fixing_Hydrus_Random_Crashes_Under_Linux` document has updated it with new information, particularly related to running hydrus fast using virtual memory on small, underpowered computers</li>
<li><h3>client api</h3></li>
<li>thanks to a user, the undocumented API call that returns info on importer pages now includes the sha256 file hash in each import object Object</li>
<li>although it is a tiny change, let's nonetheless update the Client API version to 61</li>
<li><h3>boring predicate overhaul work</h3></li>
<li>updated the `NumberTest` object to hold specific percentage and absolute ± values</li>
<li>updated the `NumberTest` object to render itself to any number format, for instance pixels vs kilobytes vs a time delta</li>
<li>updated the `Predicate` object for system preds width, height, num_notes, num_words, num_urls, num_frames, duration, and framerate to store their operator and value as a `NumberTest`, and updated predicate string rendering, parsing, editing, database-level predicate handling</li>
<li>wrote new widgets to edit `NumberTest`s of various sorts and spammed them to these (operator, value) system predicate UI panels. we are finally clearing out some 8+-year-old jank here</li>
<li>rewrote the `num_notes` database search logic to use `NumberTest`s</li>
<li>the system preds for height, width, and framerate now say 'has x' and 'no x' when set to `>0` or `=0`, although what these really mean is not perfectly defined</li>
</ul>
</li>
<li>
<h2 id="version_563"><a href="#version_563">version 563</a></h2>
<ul>

View File

@ -207,10 +207,6 @@ def GetDefaultShortcuts():
CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_ARCHIVE_DELETE_FILTER_BACK )
)
archive_delete_filter.SetCommand(
ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, ClientGUIShortcuts.SHORTCUT_KEY_SPECIAL_SPACE, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [] ),
CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_ARCHIVE_DELETE_FILTER_KEEP )
)
archive_delete_filter.SetCommand(
ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, ClientGUIShortcuts.SHORTCUT_KEY_SPECIAL_F7, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [] ),
CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_ARCHIVE_DELETE_FILTER_KEEP )
@ -254,10 +250,6 @@ def GetDefaultShortcuts():
CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_DUPLICATE_FILTER_BACK )
)
duplicate_filter.SetCommand(
ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, ClientGUIShortcuts.SHORTCUT_KEY_SPECIAL_SPACE, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [] ),
CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_DUPLICATE_FILTER_THIS_IS_BETTER_AND_DELETE_OTHER )
)
duplicate_filter.SetCommand(
ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, ClientGUIShortcuts.SHORTCUT_KEY_SPECIAL_UP, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [] ),
CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_DUPLICATE_FILTER_SKIP )
@ -496,7 +488,7 @@ def GetDefaultShortcuts():
media_viewer.SetCommand(
ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, ClientGUIShortcuts.SHORTCUT_KEY_SPECIAL_SPACE, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [] ),
CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_PAUSE_PLAY_MEDIA )
CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_PAUSE_PLAY_MEDIA )
)
media_viewer.SetCommand(
@ -718,6 +710,14 @@ def GetDefaultShortcuts():
CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_SELECT_FILES, simple_data = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_NONE ) )
)
if HC.PLATFORM_MACOS:
thumbnails.SetCommand(
ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, ClientGUIShortcuts.SHORTCUT_KEY_SPECIAL_SPACE, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [] ),
CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_MAC_QUICKLOOK )
)
shortcuts.append( thumbnails )
return shortcuts

View File

@ -340,7 +340,7 @@ def GetDuplicateComparisonStatements( shown_media, comparison_media ):
if s_has_audio:
audio_statement = 'has audio, the other does not'
audio_statement = 'this has audio, the other does not'
score = duplicate_comparison_score_has_audio
else:
@ -506,7 +506,7 @@ def GetDuplicateComparisonStatements( shown_media, comparison_media ):
if s_has_transparency:
transparency_statement = 'has transparency, the other is opaque'
transparency_statement = 'this has transparency, the other is opaque'
else:
@ -523,7 +523,7 @@ def GetDuplicateComparisonStatements( shown_media, comparison_media ):
if s_has_exif:
exif_statement = 'has exif data, the other does not'
exif_statement = 'this has exif data, the other does not'
else:
@ -540,7 +540,7 @@ def GetDuplicateComparisonStatements( shown_media, comparison_media ):
if s_has_human_readable_embedded_metadata:
embedded_metadata_statement = 'has embedded metadata, the other does not'
embedded_metadata_statement = 'this has embedded metadata, the other does not'
else:
@ -557,7 +557,7 @@ def GetDuplicateComparisonStatements( shown_media, comparison_media ):
if s_has_icc:
icc_statement = 'has icc profile, the other does not'
icc_statement = 'this has icc profile, the other does not'
else:

View File

@ -1,5 +1,8 @@
# noinspection PyUnresolvedReferences
import objc
# noinspection PyUnresolvedReferences
from Foundation import NSObject, NSURL
# noinspection PyUnresolvedReferences
from Quartz import QLPreviewPanel
QLPreviewPanelDataSource = objc.protocolNamed('QLPreviewPanelDataSource')

View File

@ -143,6 +143,8 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'booleans' ][ 'show_related_tags' ] = True
self._dictionary[ 'booleans' ][ 'show_file_lookup_script_tags' ] = False
self._dictionary[ 'booleans' ][ 'use_native_menubar' ] = HC.PLATFORM_MACOS
self._dictionary[ 'booleans' ][ 'shortcuts_merge_non_number_numpad' ] = True
self._dictionary[ 'booleans' ][ 'disable_get_safe_position_test' ] = False

View File

@ -10140,6 +10140,126 @@ class DB( HydrusDB.HydrusDB ):
if version == 563:
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client import ClientApplicationCommand as CAC
try:
space_shortcut = ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, ClientGUIShortcuts.SHORTCUT_KEY_SPECIAL_SPACE, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [] )
pause_play_command = CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_PAUSE_PLAY_MEDIA )
archive_keep = CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_ARCHIVE_DELETE_FILTER_KEEP )
this_is_better = CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_DUPLICATE_FILTER_THIS_IS_BETTER_AND_DELETE_OTHER )
media_viewer_shortcuts_set: ClientGUIShortcuts.ShortcutSet = self.modules_serialisable.GetJSONDumpNamed( HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUT_SET, 'media_viewer' )
we_mapped_space = False
we_undid_filter = False
if not media_viewer_shortcuts_set.HasCommand( space_shortcut ):
media_viewer_shortcuts_set.SetCommand( space_shortcut, pause_play_command )
self.modules_serialisable.SetJSONDump( media_viewer_shortcuts_set )
we_mapped_space = True
if media_viewer_shortcuts_set.GetCommand( space_shortcut ) == pause_play_command:
# ok, user has not set something different by themselves, let's set them up
archive_delete_filter_shortcuts_set: ClientGUIShortcuts.ShortcutSet = self.modules_serialisable.GetJSONDumpNamed( HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUT_SET, 'archive_delete_filter' )
if archive_delete_filter_shortcuts_set.GetCommand( space_shortcut ) == archive_keep:
archive_delete_filter_shortcuts_set.DeleteShortcut( space_shortcut )
self.modules_serialisable.SetJSONDump( archive_delete_filter_shortcuts_set )
we_undid_filter = True
duplicate_filter_shortcuts_set: ClientGUIShortcuts.ShortcutSet = self.modules_serialisable.GetJSONDumpNamed( HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUT_SET, 'duplicate_filter' )
if duplicate_filter_shortcuts_set.GetCommand( space_shortcut ) == this_is_better:
duplicate_filter_shortcuts_set.DeleteShortcut( space_shortcut )
self.modules_serialisable.SetJSONDump( duplicate_filter_shortcuts_set )
we_undid_filter = True
if we_mapped_space or we_undid_filter:
message = 'Hey, I shuffled around what Space does in the media viewer.'
if we_undid_filter:
message += ' It looks like you had default mappings, so I have removed Space from the archive/delete and duplicate filters.'
if we_mapped_space:
message += ' Space now does pause/play media for you.'
else:
message += ' Space now only does pause/play media for you.'
message += ' If you actually liked how it was before, sorry, please hit up _file->shortcuts_ to fix it back!'
self.pub_initial_message( message )
except Exception as e:
HydrusData.PrintException( e )
message = 'Trying to update some shortcuts failed! Please let hydrus dev know!'
self.pub_initial_message( message )
if HC.PLATFORM_MACOS:
try:
thumbnails_shortcuts_set: ClientGUIShortcuts.ShortcutSet = self.modules_serialisable.GetJSONDumpNamed( HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUT_SET, 'thumbnails' )
jobs = [
(
ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, ClientGUIShortcuts.SHORTCUT_KEY_SPECIAL_SPACE, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [] ),
CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_MAC_QUICKLOOK )
)
]
for ( shortcut, command ) in jobs:
if not thumbnails_shortcuts_set.HasCommand( shortcut ):
thumbnails_shortcuts_set.SetCommand( shortcut, command )
self.modules_serialisable.SetJSONDump( thumbnails_shortcuts_set )
except Exception as e:
HydrusData.PrintException( e )
message = 'Trying to update some shortcuts failed! Please let hydrus dev know!'
self.pub_initial_message( message )
self._controller.frame_splash_status.SetTitleText( 'updated db to v{}'.format( HydrusData.ToHumanInt( version + 1 ) ) )
self._Execute( 'UPDATE version SET version = ?;', ( version + 1, ) )

View File

@ -114,38 +114,24 @@ def GetFilesInfoPredicates( system_predicates: ClientSearch.FileSystemPredicates
files_info_predicates.append( 'has_audio = {}'.format( int( has_audio ) ) )
if 'min_width' in simple_preds:
if ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH in simple_preds:
files_info_predicates.append( 'width > ' + str( simple_preds[ 'min_width' ] ) )
number_tests: typing.List[ ClientSearch.NumberTest ] = simple_preds[ ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH ]
if 'width' in simple_preds:
files_info_predicates.append( 'width = ' + str( simple_preds[ 'width' ] ) )
if 'not_width' in simple_preds:
files_info_predicates.append( 'width != ' + str( simple_preds[ 'not_width' ] ) )
if 'max_width' in simple_preds:
files_info_predicates.append( 'width < ' + str( simple_preds[ 'max_width' ] ) )
for number_test in number_tests:
files_info_predicates.extend( number_test.GetSQLitePredicates( 'width' ) )
if 'min_height' in simple_preds:
if ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT in simple_preds:
files_info_predicates.append( 'height > ' + str( simple_preds[ 'min_height' ] ) )
number_tests: typing.List[ ClientSearch.NumberTest ] = simple_preds[ ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT ]
if 'height' in simple_preds:
files_info_predicates.append( 'height = ' + str( simple_preds[ 'height' ] ) )
if 'not_height' in simple_preds:
files_info_predicates.append( 'height != ' + str( simple_preds[ 'not_height' ] ) )
if 'max_height' in simple_preds:
files_info_predicates.append( 'height < ' + str( simple_preds[ 'max_height' ] ) )
for number_test in number_tests:
files_info_predicates.extend( number_test.GetSQLitePredicates( 'height' ) )
if 'min_num_pixels' in simple_preds:
@ -190,125 +176,44 @@ def GetFilesInfoPredicates( system_predicates: ClientSearch.FileSystemPredicates
files_info_predicates.append( '( width * 1.0 ) / height < ' + str( float( ratio_width ) ) + ' / ' + str( ratio_height ) )
if 'min_num_words' in simple_preds: files_info_predicates.append( 'num_words > ' + str( simple_preds[ 'min_num_words' ] ) )
if 'num_words' in simple_preds:
if ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS in simple_preds:
num_words = simple_preds[ 'num_words' ]
number_tests: typing.List[ ClientSearch.NumberTest ] = simple_preds[ ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS ]
if num_words == 0: files_info_predicates.append( '( num_words IS NULL OR num_words = 0 )' )
else: files_info_predicates.append( 'num_words = ' + str( num_words ) )
if 'not_num_words' in simple_preds:
num_words = simple_preds[ 'not_num_words' ]
files_info_predicates.append( '( num_words IS NULL OR num_words != {} )'.format( num_words ) )
if 'max_num_words' in simple_preds:
max_num_words = simple_preds[ 'max_num_words' ]
if max_num_words == 0: files_info_predicates.append( 'num_words < ' + str( max_num_words ) )
else: files_info_predicates.append( '( num_words < ' + str( max_num_words ) + ' OR num_words IS NULL )' )
for number_test in number_tests:
files_info_predicates.extend( number_test.GetSQLitePredicates( 'num_words' ) )
if 'min_duration' in simple_preds: files_info_predicates.append( 'duration > ' + str( simple_preds[ 'min_duration' ] ) )
if 'duration' in simple_preds:
if ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION in simple_preds:
duration = simple_preds[ 'duration' ]
number_tests: typing.List[ ClientSearch.NumberTest ] = simple_preds[ ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION ]
if duration == 0:
for number_test in number_tests:
files_info_predicates.append( '( duration = 0 OR duration IS NULL )' )
files_info_predicates.extend( number_test.GetSQLitePredicates( 'duration' ) )
else:
files_info_predicates.append( 'duration = ' + str( duration ) )
if 'not_duration' in simple_preds:
duration = simple_preds[ 'not_duration' ]
files_info_predicates.append( '( duration IS NULL OR duration != {} )'.format( duration ) )
if 'max_duration' in simple_preds:
max_duration = simple_preds[ 'max_duration' ]
if max_duration == 0: files_info_predicates.append( 'duration < ' + str( max_duration ) )
else: files_info_predicates.append( '( duration < ' + str( max_duration ) + ' OR duration IS NULL )' )
if 'min_framerate' in simple_preds or 'framerate' in simple_preds or 'max_framerate' in simple_preds or 'not_framerate' in simple_preds:
if ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE in simple_preds:
if 'not_framerate' in simple_preds:
pred = '( duration IS NULL OR num_frames = 0 OR ( duration IS NOT NULL AND duration != 0 AND num_frames != 0 AND num_frames IS NOT NULL AND {} ) )'
min_framerate_sql = simple_preds[ 'not_framerate' ] * 0.95
max_framerate_sql = simple_preds[ 'not_framerate' ] * 1.05
pred = pred.format( '( num_frames * 1.0 ) / ( duration / 1000.0 ) NOT BETWEEN {} AND {}'.format( min_framerate_sql, max_framerate_sql ) )
else:
min_framerate_sql = None
max_framerate_sql = None
pred = '( duration IS NOT NULL AND duration != 0 AND num_frames != 0 AND num_frames IS NOT NULL AND {} )'
if 'min_framerate' in simple_preds:
min_framerate_sql = simple_preds[ 'min_framerate' ] * 1.05
if 'framerate' in simple_preds:
min_framerate_sql = simple_preds[ 'framerate' ] * 0.95
max_framerate_sql = simple_preds[ 'framerate' ] * 1.05
if 'max_framerate' in simple_preds:
max_framerate_sql = simple_preds[ 'max_framerate' ] * 0.95
if min_framerate_sql is None:
pred = pred.format( '( num_frames * 1.0 ) / ( duration / 1000.0 ) < {}'.format( max_framerate_sql ) )
elif max_framerate_sql is None:
pred = pred.format( '( num_frames * 1.0 ) / ( duration / 1000.0 ) > {}'.format( min_framerate_sql ) )
else:
pred = pred.format( '( num_frames * 1.0 ) / ( duration / 1000.0 ) BETWEEN {} AND {}'.format( min_framerate_sql, max_framerate_sql ) )
number_tests: typing.List[ ClientSearch.NumberTest ] = simple_preds[ ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE ]
files_info_predicates.append( pred )
for number_test in number_tests:
files_info_predicates.extend( number_test.GetSQLitePredicates( '( num_frames * 1.0 ) / ( duration / 1000.0 )' ) )
if 'min_num_frames' in simple_preds: files_info_predicates.append( 'num_frames > ' + str( simple_preds[ 'min_num_frames' ] ) )
if 'num_frames' in simple_preds:
if ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_FRAMES in simple_preds:
num_frames = simple_preds[ 'num_frames' ]
number_tests: typing.List[ ClientSearch.NumberTest ] = simple_preds[ ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_FRAMES ]
if num_frames == 0: files_info_predicates.append( '( num_frames IS NULL OR num_frames = 0 )' )
else: files_info_predicates.append( 'num_frames = ' + str( num_frames ) )
if 'not_num_frames' in simple_preds:
num_frames = simple_preds[ 'not_num_frames' ]
files_info_predicates.append( '( num_frames IS NULL OR num_frames != {} )'.format( num_frames ) )
if 'max_num_frames' in simple_preds:
max_num_frames = simple_preds[ 'max_num_frames' ]
if max_num_frames == 0: files_info_predicates.append( 'num_frames < ' + str( max_num_frames ) )
else: files_info_predicates.append( '( num_frames < ' + str( max_num_frames ) + ' OR num_frames IS NULL )' )
for number_test in number_tests:
files_info_predicates.extend( number_test.GetSQLitePredicates( 'num_frames' ) )
return files_info_predicates
@ -926,33 +831,15 @@ class ClientDBFilesQuery( ClientDBModule.ClientDBModule ):
simple_preds = system_predicates.GetSimpleInfo()
min_num_notes = None
max_num_notes = None
number_tests = simple_preds.get( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, [] )
if 'num_notes' in simple_preds:
min_num_notes = simple_preds[ 'num_notes' ]
max_num_notes = min_num_notes
else:
if 'min_num_notes' in simple_preds:
min_num_notes = simple_preds[ 'min_num_notes' ] + 1
if 'max_num_notes' in simple_preds:
max_num_notes = simple_preds[ 'max_num_notes' ] - 1
if min_num_notes is not None or max_num_notes is not None:
if len( number_tests ) > 0:
with self._MakeTemporaryIntegerTable( query_hash_ids, 'hash_id' ) as temp_table_name:
self._AnalyzeTempTable( temp_table_name )
num_notes_hash_ids = self.modules_notes_map.GetHashIdsFromNumNotes( min_num_notes, max_num_notes, temp_table_name, job_status = job_status )
num_notes_hash_ids = self.modules_notes_map.GetHashIdsFromNumNotes( number_tests, query_hash_ids, temp_table_name, job_status = job_status )
query_hash_ids = intersection_update_qhi( query_hash_ids, num_notes_hash_ids )
@ -2175,7 +2062,7 @@ class ClientDBFilesQuery( ClientDBModule.ClientDBModule ):
#
num_urls_tests = system_predicates.GetNumURLsNumberTests()
num_urls_tests = simple_preds.get( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, [] )
if len( num_urls_tests ) > 0:

View File

@ -8,6 +8,7 @@ from hydrus.core import HydrusDB
from hydrus.client import ClientThreading
from hydrus.client.db import ClientDBMaster
from hydrus.client.db import ClientDBModule
from hydrus.client.search import ClientSearch
class ClientDBNotesMap( ClientDBModule.ClientDBModule ):
@ -59,7 +60,17 @@ class ClientDBNotesMap( ClientDBModule.ClientDBModule ):
return self._STS( self._ExecuteCancellable( 'SELECT hash_id FROM file_notes CROSS JOIN {} USING ( hash_id ) WHERE name_id = ?;'.format( hash_ids_table_name ), ( label_id, ), cancelled_hook ) )
def GetHashIdsFromNumNotes( self, min_num_notes: typing.Optional[ int ], max_num_notes: typing.Optional[ int ], hash_ids_table_name: str, job_status: typing.Optional[ ClientThreading.JobStatus ] = None ):
def GetHashIdsFromNumNotes( self, number_tests: typing.List[ ClientSearch.NumberTest ], hash_ids: typing.Collection[ int ], hash_ids_table_name: str, job_status: typing.Optional[ ClientThreading.JobStatus ] = None ):
result_hash_ids = set( hash_ids )
specific_number_tests = [ number_test for number_test in number_tests if not ( number_test.IsZero() or number_test.IsAnythingButZero() ) ]
megalambda = ClientSearch.NumberTest.STATICCreateMegaLambda( specific_number_tests )
is_zero = True in ( number_test.IsZero() for number_test in number_tests )
is_anything_but_zero = True in ( number_test.IsAnythingButZero() for number_test in number_tests )
wants_zero = True in ( number_test.WantsZero() for number_test in number_tests )
cancelled_hook = None
@ -68,50 +79,41 @@ class ClientDBNotesMap( ClientDBModule.ClientDBModule ):
cancelled_hook = job_status.IsCancelled
has_notes = max_num_notes is None and min_num_notes == 1
not_has_notes = ( min_num_notes is None or min_num_notes == 0 ) and max_num_notes is not None and max_num_notes == 0
nonzero_hash_ids = set()
if has_notes:
if is_zero or is_anything_but_zero or wants_zero:
hash_ids = self.GetHashIdsThatHaveNotes( hash_ids_table_name, job_status = job_status )
nonzero_hash_ids = self.GetHashIdsThatHaveNotes( hash_ids_table_name, job_status = job_status )
elif not_has_notes:
if is_zero:
result_hash_ids.difference_update( nonzero_hash_ids )
hash_ids = self.GetHashIdsThatDoNotHaveNotes( hash_ids_table_name, job_status = job_status )
if is_anything_but_zero:
result_hash_ids.intersection_update( nonzero_hash_ids )
else:
include_zero_count_hash_ids = False
if min_num_notes is None:
filt = lambda c: c <= max_num_notes
include_zero_count_hash_ids = True
elif max_num_notes is None:
filt = lambda c: min_num_notes <= c
else:
filt = lambda c: min_num_notes <= c <= max_num_notes
if len( specific_number_tests ) > 0:
# temp hashes to notes
query = 'SELECT hash_id, COUNT( * ) FROM {} CROSS JOIN file_notes USING ( hash_id ) GROUP BY hash_id;'.format( hash_ids_table_name )
hash_ids = { hash_id for ( hash_id, count ) in self._ExecuteCancellable( query, (), cancelled_hook ) if filt( count ) }
good_url_count_hash_ids = { hash_id for ( hash_id, count ) in self._ExecuteCancellable( query, (), cancelled_hook ) if megalambda( count ) }
if include_zero_count_hash_ids:
if wants_zero:
zero_hash_ids = self.GetHashIdsThatDoNotHaveNotes( hash_ids_table_name, job_status = job_status )
zero_hash_ids = result_hash_ids.difference( nonzero_hash_ids )
hash_ids.update( zero_hash_ids )
good_url_count_hash_ids.update( zero_hash_ids )
result_hash_ids.intersection_update( good_url_count_hash_ids )
return hash_ids
return result_hash_ids
def GetHashIdsThatDoNotHaveNotes( self, hash_ids_table_name: str, job_status: typing.Optional[ ClientThreading.JobStatus ] = None ):

View File

@ -57,7 +57,7 @@ class ClientDBURLMap( ClientDBModule.ClientDBModule ):
return hash_ids
def GetHashIdsFromCountTests( self, num_urls_tests: typing.List[ ClientSearch.NumberTest ], hash_ids: typing.Collection[ int ], hash_ids_table_name: str ):
def GetHashIdsFromCountTests( self, number_tests: typing.List[ ClientSearch.NumberTest ], hash_ids: typing.Collection[ int ], hash_ids_table_name: str ):
# we'll have to natural join 'urls' or 'urls-class-map-cache' or whatever when we add a proper filter to this guy
@ -72,13 +72,13 @@ class ClientDBURLMap( ClientDBModule.ClientDBModule ):
result_hash_ids = set( hash_ids )
specific_num_urls_tests = [ number_test for number_test in num_urls_tests if not ( number_test.IsZero() or number_test.IsAnythingButZero() ) ]
specific_number_tests = [ number_test for number_test in number_tests if not ( number_test.IsZero() or number_test.IsAnythingButZero() ) ]
megalambda = ClientSearch.NumberTest.STATICCreateMegaLambda( specific_num_urls_tests )
megalambda = ClientSearch.NumberTest.STATICCreateMegaLambda( specific_number_tests )
is_zero = True in ( number_test.IsZero() for number_test in num_urls_tests )
is_anything_but_zero = True in ( number_test.IsAnythingButZero() for number_test in num_urls_tests )
wants_zero = True in ( number_test.WantsZero() for number_test in num_urls_tests )
is_zero = True in ( number_test.IsZero() for number_test in number_tests )
is_anything_but_zero = True in ( number_test.IsAnythingButZero() for number_test in number_tests )
wants_zero = True in ( number_test.WantsZero() for number_test in number_tests )
if is_zero or is_anything_but_zero or wants_zero:
@ -97,7 +97,7 @@ class ClientDBURLMap( ClientDBModule.ClientDBModule ):
if len( specific_num_urls_tests ) > 0:
if len( specific_number_tests ) > 0:
select = f'SELECT hash_id, COUNT( url_id ) FROM {table_join} GROUP BY hash_id;'

View File

@ -749,19 +749,21 @@ class FrameGUI( CAC.ApplicationCommandProcessorMixin, ClientGUITopLevelWindows.M
library_version_lines.append( 'Pillow: {}'.format( PIL.__version__ ) )
library_version_lines.append( 'Pillow-HEIF: {}'.format( HydrusImageHandling.HEIF_OK ) )
qt_string = 'Qt: Unknown'
if QtInit.WE_ARE_QT5:
if QtInit.WE_ARE_PYSIDE:
import PySide2
library_version_lines.append( 'Qt: PySide2 {}'.format( PySide2.__version__ ) )
qt_string = 'Qt: PySide2 {}'.format( PySide2.__version__ )
elif QtInit.WE_ARE_PYQT:
from PyQt5.Qt import PYQT_VERSION_STR # pylint: disable=E0401,E0611
library_version_lines.append( 'Qt: PyQt5 {}'.format( PYQT_VERSION_STR ) )
qt_string = 'Qt: PyQt5 {}'.format( PYQT_VERSION_STR )
elif QtInit.WE_ARE_QT6:
@ -770,16 +772,28 @@ class FrameGUI( CAC.ApplicationCommandProcessorMixin, ClientGUITopLevelWindows.M
import PySide6
library_version_lines.append( 'Qt: PySide6 {}'.format( PySide6.__version__ ) )
qt_string = 'Qt: PySide6 {}'.format( PySide6.__version__ )
elif QtInit.WE_ARE_PYQT:
from PyQt6.QtCore import PYQT_VERSION_STR # pylint: disable=E0401,E0611
library_version_lines.append( 'Qt: PyQt6 {}'.format( PYQT_VERSION_STR ) )
qt_string = 'Qt: PyQt6 {}'.format( PYQT_VERSION_STR )
try:
# note this is the actual platformName. if you call this on the App instance(), I think you get what might have been changed by a launch parameter
qt_string += f' ({QG.QGuiApplication.platformName()})'
except:
qt_string += f' (unknown platform)'
library_version_lines.append( qt_string )
library_version_lines.append( 'QtCharts ok: {}'.format( ClientGUICharts.QT_CHARTS_OK ) )
if QtInit.WE_ARE_QT5:
@ -2254,12 +2268,17 @@ class FrameGUI( CAC.ApplicationCommandProcessorMixin, ClientGUITopLevelWindows.M
def _InitialiseMenubar( self ):
self._menubar = QW.QMenuBar( )
self._menubar = QW.QMenuBar()
use_native_menubar = CG.client_controller.new_options.GetBoolean( 'use_native_menubar' )
self._menubar.setNativeMenuBar( use_native_menubar )
if not self._menubar.isNativeMenuBar():
self._menubar.setParent( self )
self._menu_updater_file = self._InitialiseMenubarGetMenuUpdaterFile()
self._menu_updater_database = self._InitialiseMenubarGetMenuUpdaterDatabase()
self._menu_updater_network = self._InitialiseMenubarGetMenuUpdaterNetwork()

View File

@ -1331,6 +1331,10 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
tt = 'Many buttons that produce menus when clicked are also "scrollable", so if you wheel your mouse over them, the selection will scroll through the underlying menu. If this is annoying for you, turn it off here!'
self._menu_choice_buttons_can_mouse_scroll.setToolTip( tt )
self._use_native_menubar = QW.QCheckBox( self._misc_panel )
tt = 'macOS and some Linux allows to embed the main GUI menubar into the OS. This can be buggy! Requires restart.'
self._use_native_menubar.setToolTip( tt )
self._human_bytes_sig_figs = ClientGUICommon.BetterSpinBox( self._misc_panel, min = 1, max = 6 )
self._human_bytes_sig_figs.setToolTip( 'When the program presents a bytes size above 1KB, like 21.3KB or 4.11GB, how many total digits do we want in the number? 2 or 3 is best.')
@ -1375,6 +1379,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._menu_choice_buttons_can_mouse_scroll.setChecked( self._new_options.GetBoolean( 'menu_choice_buttons_can_mouse_scroll' ) )
self._use_native_menubar.setChecked( self._new_options.GetBoolean( 'use_native_menubar' ) )
self._human_bytes_sig_figs.setValue( self._new_options.GetInteger( 'human_bytes_sig_figs' ) )
self._discord_dnd_fix.setChecked( self._new_options.GetBoolean( 'discord_dnd_fix' ) )
@ -1419,6 +1425,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
rows.append( ( 'Copy temp files for drag-and-drop (works for <=25, <200MB file DnDs--fixes Discord!): ', self._discord_dnd_fix ) )
rows.append( ( 'Drag-and-drop export filename pattern: ', self._discord_dnd_filename_pattern ) )
rows.append( ( '', self._export_pattern_button ) )
rows.append( ( 'Use Native MenuBar (if available): ', self._use_native_menubar ) )
rows.append( ( 'EXPERIMENTAL: Bytes strings >1KB pseudo significant figures: ', self._human_bytes_sig_figs ) )
rows.append( ( 'EXPERIMENTAL BUGFIX: Secret discord file drag-and-drop fix: ', self._secret_discord_dnd_fix ) )
rows.append( ( 'BUGFIX: If on macOS, show dialog menus in a debug menu: ', self._do_macos_debug_dialog_menus ) )
@ -1508,6 +1515,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._new_options.SetBoolean( 'always_show_iso_time', self._always_show_iso_time.isChecked() )
self._new_options.SetBoolean( 'menu_choice_buttons_can_mouse_scroll', self._menu_choice_buttons_can_mouse_scroll.isChecked() )
self._new_options.SetBoolean( 'use_native_menubar', self._use_native_menubar.isChecked() )
self._new_options.SetInteger( 'human_bytes_sig_figs', self._human_bytes_sig_figs.value() )

View File

@ -20,6 +20,7 @@ from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.gui.widgets import ClientGUIControls
from hydrus.client.importing.options import ClientImportOptions
def QDateTimeToPrettyString( dt: typing.Optional[ QC.QDateTime ], include_milliseconds = False ):
@ -1485,6 +1486,39 @@ class TimestampDataStubCtrl( QW.QWidget ):
class NumberTestWidgetDuration( ClientGUIControls.NumberTestWidget ):
def _GenerateAbsoluteValueWidget( self, max: int ):
return TimeDeltaCtrl( self, min = 0, minutes = True, seconds = True, milliseconds = True )
def _GenerateValueWidget( self, max: int ):
return TimeDeltaCtrl( self, min = 0, days = False, hours = True, minutes = True, seconds = True, milliseconds = True )
def _GetAbsoluteValue( self ):
return HydrusTime.MillisecondiseS( self._absolute_plus_or_minus.GetValue() )
def _SetAbsoluteValue( self, value ):
return self._absolute_plus_or_minus.SetValue( HydrusTime.SecondiseMS( value ) )
def _GetSubValue( self ) -> int:
return HydrusTime.MillisecondiseS( self._value.GetValue() )
def _SetSubValue( self, value ):
return self._value.SetValue( HydrusTime.SecondiseMS( value ) )
class VelocityCtrl( QW.QWidget ):
velocityChanged = QC.Signal()

View File

@ -54,9 +54,18 @@ from hydrus.client.metadata import ClientContentUpdates
from hydrus.client.metadata import ClientTags
from hydrus.client.search import ClientSearch
MAC_QUARTZ_OK = True
if HC.PLATFORM_MACOS:
from hydrus.client import ClientMacIntegration
try:
from hydrus.client import ClientMacIntegration
except:
MAC_QUARTZ_OK = False
FRAME_DURATION_60FPS = 1.0 / 60
@ -1247,7 +1256,7 @@ class MediaPanel( CAC.ApplicationCommandProcessorMixin, ClientMedia.ListeningMed
if media.GetLocationsManager().IsLocal():
self.SetFocusedMedia( None )
self.focusMediaPaused.emit()
hash = media.GetHash()
mime = media.GetMime()
@ -1280,7 +1289,7 @@ class MediaPanel( CAC.ApplicationCommandProcessorMixin, ClientMedia.ListeningMed
path = client_files_manager.GetFilePath( hash, mime )
self._SetFocusedMedia( None )
self.focusMediaPaused.emit()
ClientPaths.LaunchPathInWebBrowser( path )
@ -1302,7 +1311,7 @@ class MediaPanel( CAC.ApplicationCommandProcessorMixin, ClientMedia.ListeningMed
path = client_files_manager.GetFilePath( hash, mime )
self._SetFocusedMedia( None )
self.focusMediaPaused.emit()
HydrusPaths.OpenFileLocation( path )
@ -1323,10 +1332,17 @@ class MediaPanel( CAC.ApplicationCommandProcessorMixin, ClientMedia.ListeningMed
path = client_files_manager.GetFilePath( hash, mime )
#self._SetFocusedMedia( None )
self.focusMediaPaused.emit()
if not MAC_QUARTZ_OK:
HydrusData.ShowText( 'Sorry, could not do the Quick Look integration--it looks like your venv does not support it. If you are running from source, try rebuilding it!' )
ClientMacIntegration.show_quicklook_for_path( path )
def _OpenKnownURL( self ):

View File

@ -813,38 +813,29 @@ class PanelPredicateSystemDuration( PanelPredicateSystemSingle ):
PanelPredicateSystemSingle.__init__( self, parent )
choices = [ '<', HC.UNICODE_APPROX_EQUAL, '=', HC.UNICODE_NOT_EQUAL, '>' ]
allowed_operators = [
ClientSearch.NUMBER_TEST_OPERATOR_LESS_THAN,
ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE,
ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT,
ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
]
self._sign = QP.RadioBox( self, choices = choices )
self._duration_s = ClientGUICommon.BetterSpinBox( self, max=3599, width = 60 )
self._duration_ms = ClientGUICommon.BetterSpinBox( self, max=999, width = 60 )
self._number_test = ClientGUITime.NumberTestWidgetDuration( self, allowed_operators = allowed_operators, appropriate_absolute_plus_or_minus_default = 30000, appropriate_percentage_plus_or_minus_default = 5 )
#
predicate = self._GetPredicateToInitialisePanelWith( predicate )
( sign, ms ) = predicate.GetValue()
number_test = predicate.GetValue()
s = ms // 1000
ms = ms % 1000
self._sign.SetStringSelection( sign )
self._duration_s.setValue( s )
self._duration_ms.setValue( ms )
self._number_test.SetValue( number_test )
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:duration'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._sign, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._duration_s, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'s'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._duration_ms, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'ms'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._number_test, CC.FLAGS_CENTER_PERPENDICULAR )
hbox.addStretch( 1 )
@ -853,19 +844,21 @@ class PanelPredicateSystemDuration( PanelPredicateSystemSingle ):
def GetDefaultPredicate( self ):
sign = '>'
duration = 0
number_test = ClientSearch.NumberTest( operator = ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN, value = 0 )
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( sign, duration ) )
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, number_test )
def GetPredicates( self ):
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( self._sign.GetStringSelection(), self._duration_s.value() * 1000 + self._duration_ms.value() ) ), )
number_test = self._number_test.GetValue()
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, number_test ), )
return predicates
class PanelPredicateSystemFileService( PanelPredicateSystemSingle ):
def __init__( self, parent, predicate ):
@ -1079,51 +1072,47 @@ class PanelPredicateSystemFramerate( PanelPredicateSystemSingle ):
PanelPredicateSystemSingle.__init__( self, parent )
choices = [ '<', '=', HC.UNICODE_NOT_EQUAL, '>' ]
allowed_operators = [
ClientSearch.NUMBER_TEST_OPERATOR_LESS_THAN,
ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE,
ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT,
ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
]
self._sign = QP.RadioBox( self, choices = choices )
self._framerate = ClientGUICommon.BetterSpinBox( self, min = 1, max = 3600, width = 60 )
self._number_test = ClientGUIControls.NumberTestWidget( self, allowed_operators = allowed_operators, max = 1000000, unit_string = 'fps', appropriate_absolute_plus_or_minus_default = 1, appropriate_percentage_plus_or_minus_default = 5 )
#
predicate = self._GetPredicateToInitialisePanelWith( predicate )
( sign, framerate ) = predicate.GetValue()
number_test = predicate.GetValue()
self._sign.SetStringSelection( sign )
self._framerate.setValue( framerate )
self._number_test.SetValue( number_test )
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self, 'system:framerate' ), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._sign, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._framerate, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self, 'fps' ), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._number_test, CC.FLAGS_CENTER_PERPENDICULAR )
hbox.addStretch( 1 )
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, ClientGUICommon.BetterStaticText( 'All framerate searches are +/- 5%. Exactly searching for 29.97 is not currently possible.' ), CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
self.setLayout( vbox )
self.setLayout( hbox )
def GetDefaultPredicate( self ):
sign = '='
framerate = 60
number_test = ClientSearch.NumberTest( operator = ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT, value = 60, extra_value = 0.05 )
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE, ( sign, framerate ) )
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE, number_test )
def GetPredicates( self ):
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE, ( self._sign.GetStringSelection(), self._framerate.value() ) ), )
number_test = self._number_test.GetValue()
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE, number_test ), )
return predicates
@ -1262,27 +1251,31 @@ class PanelPredicateSystemHeight( PanelPredicateSystemSingle ):
PanelPredicateSystemSingle.__init__( self, parent )
self._sign = QP.RadioBox( self, choices=['<',HC.UNICODE_APPROX_EQUAL,'=',HC.UNICODE_NOT_EQUAL,'>'] )
allowed_operators = [
ClientSearch.NUMBER_TEST_OPERATOR_LESS_THAN,
ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE,
ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT,
ClientSearch.NUMBER_TEST_OPERATOR_EQUAL,
ClientSearch.NUMBER_TEST_OPERATOR_NOT_EQUAL,
ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
]
self._height = ClientGUICommon.BetterSpinBox( self, max=200000, width = 60 )
self._number_test = ClientGUIControls.NumberTestWidget( self, allowed_operators = allowed_operators, unit_string = 'px', appropriate_absolute_plus_or_minus_default = 200 )
#
predicate = self._GetPredicateToInitialisePanelWith( predicate )
( sign, height ) = predicate.GetValue()
number_test = predicate.GetValue()
self._sign.SetStringSelection( sign )
self._height.setValue( height )
self._number_test.SetValue( number_test )
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:height'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._sign, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._height, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._number_test, CC.FLAGS_CENTER_PERPENDICULAR )
hbox.addStretch( 1 )
@ -1291,15 +1284,16 @@ class PanelPredicateSystemHeight( PanelPredicateSystemSingle ):
def GetDefaultPredicate( self ):
sign = '='
height = 1080
number_test = ClientSearch.NumberTest( operator = ClientSearch.NUMBER_TEST_OPERATOR_EQUAL, value = 1080 )
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( sign, height ) )
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, number_test )
def GetPredicates( self ):
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( self._sign.GetStringSelection(), self._height.value() ) ), )
number_test = self._number_test.GetValue()
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, number_test ), )
return predicates
@ -1761,28 +1755,31 @@ class PanelPredicateSystemNumFrames( PanelPredicateSystemSingle ):
PanelPredicateSystemSingle.__init__( self, parent )
choices = [ '<', HC.UNICODE_APPROX_EQUAL, '=', HC.UNICODE_NOT_EQUAL, '>' ]
allowed_operators = [
ClientSearch.NUMBER_TEST_OPERATOR_LESS_THAN,
ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE,
ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT,
ClientSearch.NUMBER_TEST_OPERATOR_EQUAL,
ClientSearch.NUMBER_TEST_OPERATOR_NOT_EQUAL,
ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
]
self._sign = QP.RadioBox( self, choices = choices )
self._num_frames = ClientGUICommon.BetterSpinBox( self, min = 0, max = 1000000, width = 80 )
self._number_test = ClientGUIControls.NumberTestWidget( self, allowed_operators = allowed_operators, max = 1000000, appropriate_absolute_plus_or_minus_default = 300 )
#
predicate = self._GetPredicateToInitialisePanelWith( predicate )
( sign, num_frames ) = predicate.GetValue()
number_test = predicate.GetValue()
self._sign.SetStringSelection( sign )
self._num_frames.setValue( num_frames )
self._number_test.SetValue( number_test )
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self, 'system:number of frames' ), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._sign, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._num_frames, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._number_test, CC.FLAGS_CENTER_PERPENDICULAR )
hbox.addStretch( 1 )
@ -1791,15 +1788,16 @@ class PanelPredicateSystemNumFrames( PanelPredicateSystemSingle ):
def GetDefaultPredicate( self ):
sign = '>'
num_frames = 600
number_test = ClientSearch.NumberTest( operator = ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN, value = 600 )
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_FRAMES, ( sign, num_frames ) )
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_FRAMES, number_test )
def GetPredicates( self ):
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_FRAMES, ( self._sign.GetStringSelection(), self._num_frames.value() ) ), )
number_test = self._number_test.GetValue()
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_FRAMES, number_test ), )
return predicates
@ -1895,27 +1893,29 @@ class PanelPredicateSystemNumNotes( PanelPredicateSystemSingle ):
PanelPredicateSystemSingle.__init__( self, parent )
self._sign = QP.RadioBox( self, choices = [ '<', '=', '>' ] )
allowed_operators = [
ClientSearch.NUMBER_TEST_OPERATOR_LESS_THAN,
ClientSearch.NUMBER_TEST_OPERATOR_EQUAL,
ClientSearch.NUMBER_TEST_OPERATOR_NOT_EQUAL,
ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
]
self._num_notes = ClientGUICommon.BetterSpinBox( self, max = 256, width = 60 )
self._number_test = ClientGUIControls.NumberTestWidget( self, allowed_operators = allowed_operators )
#
predicate = self._GetPredicateToInitialisePanelWith( predicate )
( sign, num_notes ) = predicate.GetValue()
number_test = predicate.GetValue()
self._sign.SetStringSelection( sign )
self._num_notes.setValue( num_notes )
self._number_test.SetValue( number_test )
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:number of notes'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._sign, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._num_notes, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self,'system:number of notes' ), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._number_test, CC.FLAGS_CENTER_PERPENDICULAR )
hbox.addStretch( 1 )
@ -1924,15 +1924,16 @@ class PanelPredicateSystemNumNotes( PanelPredicateSystemSingle ):
def GetDefaultPredicate( self ):
sign = '='
num_notes = 1
number_test = ClientSearch.NumberTest( operator = ClientSearch.NUMBER_TEST_OPERATOR_EQUAL, value = 2 )
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ( sign, num_notes ) )
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, number_test )
def GetPredicates( self ):
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ( self._sign.GetStringSelection(), self._num_notes.value() ) ), )
number_test = self._number_test.GetValue()
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, number_test ), )
return predicates
@ -1944,27 +1945,29 @@ class PanelPredicateSystemNumURLs( PanelPredicateSystemSingle ):
PanelPredicateSystemSingle.__init__( self, parent )
self._sign = QP.RadioBox( self, choices=['<','=',HC.UNICODE_NOT_EQUAL,'>'] )
allowed_operators = [
ClientSearch.NUMBER_TEST_OPERATOR_LESS_THAN,
ClientSearch.NUMBER_TEST_OPERATOR_EQUAL,
ClientSearch.NUMBER_TEST_OPERATOR_NOT_EQUAL,
ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
]
self._num_urls = ClientGUICommon.BetterSpinBox( self, max=1000000, width = 60 )
self._number_test = ClientGUIControls.NumberTestWidget( self, allowed_operators = allowed_operators )
#
predicate = self._GetPredicateToInitialisePanelWith( predicate )
( sign, num_urls ) = predicate.GetValue()
number_test = predicate.GetValue()
self._sign.SetStringSelection( sign )
self._num_urls.setValue( num_urls )
self._number_test.SetValue( number_test )
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self,'system:number of urls' ), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._sign, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._num_urls, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._number_test, CC.FLAGS_CENTER_PERPENDICULAR )
hbox.addStretch( 1 )
@ -1973,15 +1976,16 @@ class PanelPredicateSystemNumURLs( PanelPredicateSystemSingle ):
def GetDefaultPredicate( self ):
sign = '>'
num_urls = 0
number_test = ClientSearch.NumberTest( operator = ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN, value = 0 )
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ( sign, num_urls ) )
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, number_test )
def GetPredicates( self ):
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ( self._sign.GetStringSelection(), self._num_urls.value() ) ), )
number_test = self._number_test.GetValue()
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, number_test ), )
return predicates
@ -1993,27 +1997,31 @@ class PanelPredicateSystemNumWords( PanelPredicateSystemSingle ):
PanelPredicateSystemSingle.__init__( self, parent )
self._sign = QP.RadioBox( self, choices=['<',HC.UNICODE_APPROX_EQUAL,'=',HC.UNICODE_NOT_EQUAL,'>'] )
allowed_operators = [
ClientSearch.NUMBER_TEST_OPERATOR_LESS_THAN,
ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE,
ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT,
ClientSearch.NUMBER_TEST_OPERATOR_EQUAL,
ClientSearch.NUMBER_TEST_OPERATOR_NOT_EQUAL,
ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
]
self._num_words = ClientGUICommon.BetterSpinBox( self, max=1000000, width = 60 )
self._number_test = ClientGUIControls.NumberTestWidget( self, allowed_operators = allowed_operators, max = 100000000, appropriate_absolute_plus_or_minus_default = 5000 )
#
predicate = self._GetPredicateToInitialisePanelWith( predicate )
( sign, num_words ) = predicate.GetValue()
number_test = predicate.GetValue()
self._sign.SetStringSelection( sign )
self._num_words.setValue( num_words )
self._number_test.SetValue( number_test )
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:number of words'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._sign, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._num_words, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._number_test, CC.FLAGS_CENTER_PERPENDICULAR )
hbox.addStretch( 1 )
@ -2022,15 +2030,16 @@ class PanelPredicateSystemNumWords( PanelPredicateSystemSingle ):
def GetDefaultPredicate( self ):
sign = '<'
num_words = 30000
number_test = ClientSearch.NumberTest( operator = ClientSearch.NUMBER_TEST_OPERATOR_LESS_THAN, value = 30000 )
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ( sign, num_words ) )
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, number_test )
def GetPredicates( self ):
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ( self._sign.GetStringSelection(), self._num_words.value() ) ), )
number_test = self._number_test.GetValue()
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, number_test ), )
return predicates
@ -2483,27 +2492,31 @@ class PanelPredicateSystemWidth( PanelPredicateSystemSingle ):
PanelPredicateSystemSingle.__init__( self, parent )
self._sign = QP.RadioBox( self, choices=['<',HC.UNICODE_APPROX_EQUAL,'=',HC.UNICODE_NOT_EQUAL,'>'] )
allowed_operators = [
ClientSearch.NUMBER_TEST_OPERATOR_LESS_THAN,
ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE,
ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT,
ClientSearch.NUMBER_TEST_OPERATOR_EQUAL,
ClientSearch.NUMBER_TEST_OPERATOR_NOT_EQUAL,
ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
]
self._width = ClientGUICommon.BetterSpinBox( self, max=200000, width = 60 )
self._number_test = ClientGUIControls.NumberTestWidget( self, allowed_operators = allowed_operators, unit_string = 'px', appropriate_absolute_plus_or_minus_default = 200 )
#
predicate = self._GetPredicateToInitialisePanelWith( predicate )
( sign, width ) = predicate.GetValue()
number_test = predicate.GetValue()
self._sign.SetStringSelection( sign )
self._width.setValue( width )
self._number_test.SetValue( number_test )
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:width'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._sign, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._width, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._number_test, CC.FLAGS_CENTER_PERPENDICULAR )
hbox.addStretch( 1 )
@ -2512,15 +2525,16 @@ class PanelPredicateSystemWidth( PanelPredicateSystemSingle ):
def GetDefaultPredicate( self ):
sign = '='
width = 1920
number_test = ClientSearch.NumberTest( operator = ClientSearch.NUMBER_TEST_OPERATOR_EQUAL, value = 1920 )
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( sign, width ) )
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, number_test )
def GetPredicates( self ):
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( self._sign.GetStringSelection(), self._width.value() ) ), )
number_test = self._number_test.GetValue()
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, number_test ), )
return predicates

View File

@ -6,7 +6,6 @@ from qtpy import QtWidgets as QW
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
@ -187,6 +186,7 @@ def GetEditablePredicates( predicates: typing.Collection[ ClientSearch.Predicate
return ( editable_predicates, only_invertible_predicates, non_editable_predicates )
class EditPredicatesPanel( ClientGUIScrolledPanels.EditPanel ):
def __init__( self, parent, predicates: typing.Collection[ ClientSearch.Predicate ], empty_file_search_context: typing.Optional[ ClientSearch.FileSearchContext ] = None ):
@ -543,9 +543,9 @@ class FleshOutPredicatePanel( ClientGUIScrolledPanels.EditPanel ):
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO, ( '=', 1, 1 ) ), ), show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO, ( 'taller than', 1, 1 ) ), ), show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO, ( 'wider than', 1, 1 ) ), ), show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( '=', 1920 ) ), ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '=', 1080 ) ) ), forced_label = '1080p', show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( '=', 1280 ) ), ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '=', 720 ) ) ), forced_label = '720p', show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( '=', 3840 ) ), ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '=', 2160 ) ) ), forced_label = '4k', show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 1920 ) ), ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 1080 ) ) ), forced_label = '1080p', show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 1280 ) ), ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 720 ) ) ), forced_label = '720p', show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 3840 ) ), ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 2160 ) ) ), forced_label = '4k', show_remove_button = False ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemHeight, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemWidth, predicate ) )
@ -556,10 +556,10 @@ class FleshOutPredicatePanel( ClientGUIScrolledPanels.EditPanel ):
recent_predicate_types = [ ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE, ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_FRAMES ]
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( '>', 0 ) ), ), show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( '=', 0 ) ), ), show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE, ( '=', 30 ) ), ), show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE, ( '=', 60 ) ), ), show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.NumberTest.STATICCreateFromCharacters( '>', 0 ) ), ), show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 0 ) ), ), show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 30 ) ), ), show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 60 ) ), ), show_remove_button = False ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemDuration, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemFramerate, predicate ) )
@ -599,8 +599,8 @@ class FleshOutPredicatePanel( ClientGUIScrolledPanels.EditPanel ):
static_pred_buttons = []
editable_pred_panels = []
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ( '>', 0 ) ), ), show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ( '=', 0 ) ), ), show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ClientSearch.NumberTest.STATICCreateFromCharacters( '>', 0 ) ), ), show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 0 ) ), ), show_remove_button = False ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemNumURLs, predicate ) )
@ -687,8 +687,8 @@ class FleshOutPredicatePanel( ClientGUIScrolledPanels.EditPanel ):
recent_predicate_types = [ ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME ]
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ( '>', 0 ) ), ), show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ( '=', 0 ) ), ), show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ClientSearch.NumberTest.STATICCreateFromCharacters( '>', 0 ) ), ), show_remove_button = False ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 0 ) ), ), show_remove_button = False ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemNumNotes, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemHasNoteName, predicate ) )

View File

@ -571,7 +571,7 @@ class SimpleSubPanel( QW.QWidget ):
action = self._simple_actions.GetValue()
self._duplicates_type_panel.setVisible( action == CAC.SIMPLE_REARRANGE_THUMBNAILS )
self._thumbnail_rearrange_panel.setVisible( action == CAC.SIMPLE_REARRANGE_THUMBNAILS )
self._duplicates_type_panel.setVisible( action == CAC.SIMPLE_SHOW_DUPLICATES )
self._seek_panel.setVisible( action == CAC.SIMPLE_MEDIA_SEEK_DELTA )
self._thumbnail_move_panel.setVisible( action == CAC.SIMPLE_MOVE_THUMBNAIL_FOCUS )

View File

@ -22,6 +22,7 @@ from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.search import ClientSearch
class BandwidthRulesCtrl( ClientGUICommon.StaticBox ):
@ -435,6 +436,180 @@ class NoneableBytesControl( QW.QWidget ):
self._UpdateEnabled()
class NumberTestWidget( QW.QWidget ):
def __init__( self, parent, allowed_operators = None, max = 200000, unit_string = None, appropriate_absolute_plus_or_minus_default = 1, appropriate_percentage_plus_or_minus_default = 15 ):
QW.QWidget.__init__( self, parent )
choice_tuples = []
for possible_operator in [
ClientSearch.NUMBER_TEST_OPERATOR_LESS_THAN,
ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE,
ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT,
ClientSearch.NUMBER_TEST_OPERATOR_EQUAL,
ClientSearch.NUMBER_TEST_OPERATOR_NOT_EQUAL,
ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
]:
if possible_operator in allowed_operators:
text = ClientSearch.number_test_operator_to_str_lookup[ possible_operator ]
if possible_operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT:
text += '%'
choice_tuples.append( ( text, possible_operator ) )
self._operator = QP.DataRadioBox( self, choice_tuples = choice_tuples )
self._value = self._GenerateValueWidget( max )
#
self._absolute_plus_or_minus_panel = QW.QWidget( self )
self._absolute_plus_or_minus = self._GenerateAbsoluteValueWidget( max )
self._SetAbsoluteValue( appropriate_absolute_plus_or_minus_default )
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self._absolute_plus_or_minus_panel, label = HC.UNICODE_PLUS_OR_MINUS ), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._absolute_plus_or_minus, CC.FLAGS_CENTER_PERPENDICULAR )
if unit_string is not None:
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self._absolute_plus_or_minus_panel, label = unit_string ), CC.FLAGS_CENTER_PERPENDICULAR )
self._absolute_plus_or_minus_panel.setLayout( hbox )
#
self._percent_plus_or_minus_panel = QW.QWidget( self )
self._percent_plus_or_minus = ClientGUICommon.BetterSpinBox( self._percent_plus_or_minus_panel, min = 0, max = 10000, width = 60 )
self._percent_plus_or_minus.setValue( appropriate_percentage_plus_or_minus_default )
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self._percent_plus_or_minus_panel, label = HC.UNICODE_PLUS_OR_MINUS ), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._percent_plus_or_minus, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self._percent_plus_or_minus_panel, label = '%' ), CC.FLAGS_CENTER_PERPENDICULAR )
self._percent_plus_or_minus_panel.setLayout( hbox )
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, self._operator, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._value, CC.FLAGS_CENTER_PERPENDICULAR )
if unit_string is not None:
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self, label = unit_string ), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._absolute_plus_or_minus_panel, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._percent_plus_or_minus_panel, CC.FLAGS_CENTER_PERPENDICULAR )
self.setLayout( hbox )
self._operator.radioBoxChanged.connect( self._UpdateVisibility )
self._UpdateVisibility()
def _GenerateAbsoluteValueWidget( self, max: int ):
return ClientGUICommon.BetterSpinBox( self._absolute_plus_or_minus_panel, min = 0, max = int( max / 2 ), width = 60 )
def _GenerateValueWidget( self, max: int ):
return ClientGUICommon.BetterSpinBox( self, max = max, width = 60 )
def _GetSubValue( self ):
return self._value.value()
def _SetSubValue( self, value ):
return self._value.setValue( value )
def _GetAbsoluteValue( self ):
return self._absolute_plus_or_minus.value()
def _SetAbsoluteValue( self, value ):
return self._absolute_plus_or_minus.setValue( value )
def _UpdateVisibility( self ):
operator = self._operator.GetValue()
self._absolute_plus_or_minus_panel.setVisible( operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE )
self._percent_plus_or_minus_panel.setVisible( operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT )
def GetValue( self ) -> ClientSearch.NumberTest:
operator = self._operator.GetValue()
value = self._GetSubValue()
if operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE:
extra_value = self._GetAbsoluteValue()
elif operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT:
extra_value = self._percent_plus_or_minus.value() / 100
else:
extra_value = None
return ClientSearch.NumberTest( operator = operator, value = value, extra_value = extra_value )
def SetValue( self, number_test: ClientSearch.NumberTest ):
self._operator.SetValue( number_test.operator )
self._SetSubValue( number_test.value )
if number_test.operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE:
self._SetAbsoluteValue( number_test.extra_value )
elif number_test.operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT:
self._percent_plus_or_minus.setValue( int( number_test.extra_value * 100 ) )
self._UpdateVisibility()
class TextAndPasteCtrl( QW.QWidget ):
def __init__( self, parent, add_callable, allow_empty_input = False ):

View File

@ -759,6 +759,10 @@ class FileSeed( HydrusSerialisable.SerialisableBase ):
d[ 'status' ] = self.status
d[ 'note' ] = self.note
hash = self.GetHash()
d[ 'hash' ] = hash.hex() if hash is not None else hash
return d

View File

@ -289,32 +289,54 @@ def SubtagIsEmpty( search_text: str ):
NUMBER_TEST_OPERATOR_LESS_THAN = 0
NUMBER_TEST_OPERATOR_GREATER_THAN = 1
NUMBER_TEST_OPERATOR_EQUAL = 2
NUMBER_TEST_OPERATOR_APPROXIMATE = 3
NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT = 3
NUMBER_TEST_OPERATOR_NOT_EQUAL = 4
NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE = 5
number_test_operator_to_str_lookup = {
NUMBER_TEST_OPERATOR_LESS_THAN : '<',
NUMBER_TEST_OPERATOR_GREATER_THAN : '>',
NUMBER_TEST_OPERATOR_EQUAL : '=',
NUMBER_TEST_OPERATOR_APPROXIMATE : HC.UNICODE_APPROX_EQUAL,
NUMBER_TEST_OPERATOR_NOT_EQUAL : HC.UNICODE_NOT_EQUAL
NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT : HC.UNICODE_APPROX_EQUAL,
NUMBER_TEST_OPERATOR_NOT_EQUAL : HC.UNICODE_NOT_EQUAL,
NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE : HC.UNICODE_APPROX_EQUAL
}
number_test_str_to_operator_lookup = { value : key for ( key, value ) in number_test_operator_to_str_lookup.items() }
number_test_str_to_operator_lookup = { value : key for ( key, value ) in number_test_operator_to_str_lookup.items() if key != NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE }
class NumberTest( HydrusSerialisable.SerialisableBase ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_NUMBER_TEST
SERIALISABLE_NAME = 'Number Test'
SERIALISABLE_VERSION = 1
SERIALISABLE_VERSION = 2
def __init__( self, operator = NUMBER_TEST_OPERATOR_EQUAL, value = 1 ):
def __init__( self, operator = NUMBER_TEST_OPERATOR_EQUAL, value = 1, extra_value = None ):
HydrusSerialisable.SerialisableBase.__init__( self )
if operator == NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT and value == 0:
operator = NUMBER_TEST_OPERATOR_EQUAL
extra_value = None
self.operator = operator
self.value = value
if extra_value is None:
if self.operator == NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT:
extra_value = 0.15
elif self.operator == NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE:
extra_value = 1
self.extra_value = extra_value
def __eq__( self, other ):
@ -328,51 +350,129 @@ class NumberTest( HydrusSerialisable.SerialisableBase ):
def __hash__( self ):
return ( self.operator, self.value ).__hash__()
return ( self.operator, self.value, self.extra_value ).__hash__()
def __repr__( self ):
return '{} {}'.format( number_test_operator_to_str_lookup[ self.operator ], self.value )
return self.ToString()
def _GetSerialisableInfo( self ):
return ( self.operator, self.value )
return ( self.operator, self.value, self.extra_value )
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
( self.operator, self.value ) = serialisable_info
( self.operator, self.value, self.extra_value ) = serialisable_info
def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
if version == 1:
( operator, value ) = old_serialisable_info
if operator == NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT:
extra_value = 0.15
else:
extra_value = None
new_serialisable_info = ( operator, value, extra_value )
return ( 2, new_serialisable_info )
def GetLambda( self ):
if self.operator == NUMBER_TEST_OPERATOR_LESS_THAN:
return lambda x: x < self.value
return lambda x: x is not None and x < self.value
elif self.operator == NUMBER_TEST_OPERATOR_GREATER_THAN:
return lambda x: x > self.value
return lambda x: x is not None and x > self.value
elif self.operator == NUMBER_TEST_OPERATOR_EQUAL:
return lambda x: x == self.value
if self.value == 0:
return lambda x: x is None or x == self.value
else:
return lambda x: x == self.value
elif self.operator == NUMBER_TEST_OPERATOR_APPROXIMATE:
elif self.operator == NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT:
lower = self.value * 0.85
upper = self.value * 1.15
lower = self.value * ( 1 - self.extra_value )
upper = self.value * ( 1 + self.extra_value )
return lambda x: lower < x < upper
return lambda x: x is not None and lower < x < upper
elif self.operator == NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE:
lower = self.value - self.extra_value
upper = self.value + self.extra_value
return lambda x: x is not None and lower < x < upper
elif self.operator == NUMBER_TEST_OPERATOR_NOT_EQUAL:
return lambda x: x != self.value
return lambda x: x is not None and x != self.value
def GetSQLitePredicates( self, variable_name ):
if self.operator == NUMBER_TEST_OPERATOR_LESS_THAN:
return [ f'{variable_name} < {self.value}' ]
elif self.operator == NUMBER_TEST_OPERATOR_GREATER_THAN:
return [ f'{variable_name} > {self.value}' ]
elif self.operator == NUMBER_TEST_OPERATOR_EQUAL:
if self.value == 0:
return [ f'({variable_name} IS NULL OR {variable_name} = {self.value})' ]
else:
return [ f'{variable_name} = {self.value}' ]
elif self.operator == NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT:
lower = self.value * ( 1 - self.extra_value )
upper = self.value * ( 1 + self.extra_value )
return [ f'{variable_name} > {lower}', f'{variable_name} < {upper}' ]
elif self.operator == NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE:
lower = self.value - self.extra_value
upper = self.value + self.extra_value
return [ f'{variable_name} > {lower}', f'{variable_name} < {upper}' ]
elif self.operator == NUMBER_TEST_OPERATOR_NOT_EQUAL:
return [ f'{variable_name} != {self.value}' ]
return []
def IsAnythingButZero( self ):
return self.operator in ( NUMBER_TEST_OPERATOR_NOT_EQUAL, NUMBER_TEST_OPERATOR_GREATER_THAN ) and self.value == 0
@ -386,6 +486,27 @@ class NumberTest( HydrusSerialisable.SerialisableBase ):
return actually_zero or less_than_one
def ToString( self, absolute_number_renderer: typing.Optional[ typing.Callable ] = None ) -> str:
if absolute_number_renderer is None:
absolute_number_renderer = HydrusData.ToHumanInt
result = f'{number_test_operator_to_str_lookup[ self.operator ]} {absolute_number_renderer( self.value )}'
if self.operator == NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT:
result += f' {HC.UNICODE_PLUS_OR_MINUS}{HydrusData.ConvertFloatToPercentage(self.extra_value)}'
elif self.operator == NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE:
result += f' {HC.UNICODE_PLUS_OR_MINUS}{absolute_number_renderer(self.extra_value)}'
return result
def WantsZero( self ):
return self.GetLambda()( 0 )
@ -407,6 +528,7 @@ class NumberTest( HydrusSerialisable.SerialisableBase ):
return lambda x: False not in ( lamb( x ) for lamb in lambdas )
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_NUMBER_TEST ] = NumberTest
class FileSystemPredicates( object ):
@ -454,6 +576,30 @@ class FileSystemPredicates( object ):
if predicate_type == PREDICATE_TYPE_SYSTEM_LOCAL: self._local = True
if predicate_type == PREDICATE_TYPE_SYSTEM_NOT_LOCAL: self._not_local = True
for number_test_predicate_type in [
PREDICATE_TYPE_SYSTEM_WIDTH,
PREDICATE_TYPE_SYSTEM_HEIGHT,
PREDICATE_TYPE_SYSTEM_NUM_NOTES,
PREDICATE_TYPE_SYSTEM_NUM_WORDS,
PREDICATE_TYPE_SYSTEM_NUM_URLS,
PREDICATE_TYPE_SYSTEM_NUM_FRAMES,
PREDICATE_TYPE_SYSTEM_DURATION,
PREDICATE_TYPE_SYSTEM_FRAMERATE
]:
if predicate_type == number_test_predicate_type:
if number_test_predicate_type not in self._common_info:
self._common_info[ number_test_predicate_type ] = []
number_test = value
self._common_info[ number_test_predicate_type ].append( number_test )
if predicate_type == PREDICATE_TYPE_SYSTEM_KNOWN_URLS:
( operator, rule_type, rule, description ) = value
@ -608,58 +754,9 @@ class FileSystemPredicates( object ):
self._common_info[ 'mimes' ] = ConvertSummaryFiletypesToSpecific( summary_mimes )
if predicate_type == PREDICATE_TYPE_SYSTEM_DURATION:
if predicate_type == PREDICATE_TYPE_SYSTEM_NUM_TAGS:
( operator, duration ) = value
if operator == '<': self._common_info[ 'max_duration' ] = duration
elif operator == '>': self._common_info[ 'min_duration' ] = duration
elif operator == '=': self._common_info[ 'duration' ] = duration
elif operator == HC.UNICODE_NOT_EQUAL: self._common_info[ 'not_duration' ] = duration
elif operator == HC.UNICODE_APPROX_EQUAL:
if duration == 0:
self._common_info[ 'duration' ] = 0
else:
self._common_info[ 'min_duration' ] = int( duration * 0.85 )
self._common_info[ 'max_duration' ] = int( duration * 1.15 )
if predicate_type == PREDICATE_TYPE_SYSTEM_FRAMERATE:
( operator, framerate ) = value
if operator == '<': self._common_info[ 'max_framerate' ] = framerate
elif operator == '>': self._common_info[ 'min_framerate' ] = framerate
elif operator == '=': self._common_info[ 'framerate' ] = framerate
elif operator == HC.UNICODE_NOT_EQUAL: self._common_info[ 'not_framerate' ] = framerate
if predicate_type == PREDICATE_TYPE_SYSTEM_NUM_FRAMES:
( operator, num_frames ) = value
if operator == '<': self._common_info[ 'max_num_frames' ] = num_frames
elif operator == '>': self._common_info[ 'min_num_frames' ] = num_frames
elif operator == '=': self._common_info[ 'num_frames' ] = num_frames
elif operator == HC.UNICODE_NOT_EQUAL: self._common_info[ 'not_num_frames' ] = num_frames
elif operator == HC.UNICODE_APPROX_EQUAL:
if num_frames == 0:
self._common_info[ 'num_frames' ] = 0
else:
self._common_info[ 'min_num_frames' ] = int( num_frames * 0.85 )
self._common_info[ 'max_num_frames' ] = int( num_frames * 1.15 )
self._num_tags_predicates.append( predicate )
if predicate_type == PREDICATE_TYPE_SYSTEM_RATING:
@ -710,16 +807,6 @@ class FileSystemPredicates( object ):
if predicate_type == PREDICATE_TYPE_SYSTEM_NUM_TAGS:
self._num_tags_predicates.append( predicate.Duplicate() )
if predicate_type == PREDICATE_TYPE_SYSTEM_NUM_URLS:
self._num_urls_predicates.append( predicate.Duplicate() )
if predicate_type == PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER:
( namespace, operator, num ) = value
@ -733,25 +820,6 @@ class FileSystemPredicates( object ):
if predicate_type == PREDICATE_TYPE_SYSTEM_WIDTH:
( operator, width ) = value
if operator == '<': self._common_info[ 'max_width' ] = width
elif operator == '>': self._common_info[ 'min_width' ] = width
elif operator == '=': self._common_info[ 'width' ] = width
elif operator == HC.UNICODE_NOT_EQUAL: self._common_info[ 'not_width' ] = width
elif operator == HC.UNICODE_APPROX_EQUAL:
if width == 0: self._common_info[ 'width' ] = 0
else:
self._common_info[ 'min_width' ] = int( width * 0.85 )
self._common_info[ 'max_width' ] = int( width * 1.15 )
if predicate_type == PREDICATE_TYPE_SYSTEM_NUM_PIXELS:
( operator, num_pixels, unit ) = value
@ -769,37 +837,6 @@ class FileSystemPredicates( object ):
if predicate_type == PREDICATE_TYPE_SYSTEM_HEIGHT:
( operator, height ) = value
if operator == '<': self._common_info[ 'max_height' ] = height
elif operator == '>': self._common_info[ 'min_height' ] = height
elif operator == '=': self._common_info[ 'height' ] = height
elif operator == HC.UNICODE_NOT_EQUAL: self._common_info[ 'not_height' ] = height
elif operator == HC.UNICODE_APPROX_EQUAL:
if height == 0:
self._common_info[ 'height' ] = 0
else:
self._common_info[ 'min_height' ] = int( height * 0.85 )
self._common_info[ 'max_height' ] = int( height * 1.15 )
if predicate_type == PREDICATE_TYPE_SYSTEM_NUM_NOTES:
( operator, num_notes ) = value
if operator == '<': self._common_info[ 'max_num_notes' ] = num_notes
elif operator == '>': self._common_info[ 'min_num_notes' ] = num_notes
elif operator == '=': self._common_info[ 'num_notes' ] = num_notes
if predicate_type == PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME:
( operator, name ) = value
@ -821,25 +858,6 @@ class FileSystemPredicates( object ):
self._common_info[ label ].add( name )
if predicate_type == PREDICATE_TYPE_SYSTEM_NUM_WORDS:
( operator, num_words ) = value
if operator == '<': self._common_info[ 'max_num_words' ] = num_words
elif operator == '>': self._common_info[ 'min_num_words' ] = num_words
elif operator == '=': self._common_info[ 'num_words' ] = num_words
elif operator == HC.UNICODE_NOT_EQUAL: self._common_info[ 'not_num_words' ] = num_words
elif operator == HC.UNICODE_APPROX_EQUAL:
if num_words == 0: self._common_info[ 'num_words' ] = 0
else:
self._common_info[ 'min_num_words' ] = int( num_words * 0.85 )
self._common_info[ 'max_num_words' ] = int( num_words * 1.15 )
if predicate_type == PREDICATE_TYPE_SYSTEM_LIMIT:
limit = value
@ -953,22 +971,6 @@ class FileSystemPredicates( object ):
return namespaces_to_tests
def GetNumURLsNumberTests( self ) -> typing.List[ NumberTest ]:
tests = []
for predicate in self._num_urls_predicates:
( operator, value ) = predicate.GetValue()
test = NumberTest.STATICCreateFromCharacters( operator, value )
tests.append( test )
return tests
def GetRatingsPredicates( self ):
return self._ratings_predicates
@ -1695,7 +1697,7 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_PREDICATE
SERIALISABLE_NAME = 'File Search Predicate'
SERIALISABLE_VERSION = 7
SERIALISABLE_VERSION = 8
def __init__(
self,
@ -1845,6 +1847,12 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
serialisable_value = HydrusSerialisable.SerialisableList( or_predicates ).GetSerialisableTuple()
elif self._predicate_type in ( PREDICATE_TYPE_SYSTEM_WIDTH, PREDICATE_TYPE_SYSTEM_HEIGHT, PREDICATE_TYPE_SYSTEM_NUM_NOTES, PREDICATE_TYPE_SYSTEM_NUM_WORDS, PREDICATE_TYPE_SYSTEM_NUM_URLS, PREDICATE_TYPE_SYSTEM_NUM_FRAMES, PREDICATE_TYPE_SYSTEM_DURATION, PREDICATE_TYPE_SYSTEM_FRAMERATE ):
number_test: NumberTest = self._value
serialisable_value = number_test.GetSerialisableTuple()
else:
serialisable_value = self._value
@ -1918,6 +1926,12 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
self._value = tuple( sorted( HydrusSerialisable.CreateFromSerialisableTuple( serialisable_or_predicates ), key = lambda p: HydrusTags.ConvertTagToSortable( p.ToString() ) ) )
elif self._predicate_type in ( PREDICATE_TYPE_SYSTEM_WIDTH, PREDICATE_TYPE_SYSTEM_HEIGHT, PREDICATE_TYPE_SYSTEM_NUM_NOTES, PREDICATE_TYPE_SYSTEM_NUM_WORDS, PREDICATE_TYPE_SYSTEM_NUM_URLS, PREDICATE_TYPE_SYSTEM_NUM_FRAMES, PREDICATE_TYPE_SYSTEM_DURATION, PREDICATE_TYPE_SYSTEM_FRAMERATE ):
serialisable_number_test = serialisable_value
self._value = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_number_test )
else:
self._value = serialisable_value
@ -2077,6 +2091,36 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
return ( 7, new_serialisable_info )
if version == 7:
( predicate_type, serialisable_value, inclusive ) = old_serialisable_info
if predicate_type in ( PREDICATE_TYPE_SYSTEM_WIDTH, PREDICATE_TYPE_SYSTEM_HEIGHT, PREDICATE_TYPE_SYSTEM_NUM_NOTES, PREDICATE_TYPE_SYSTEM_NUM_WORDS, PREDICATE_TYPE_SYSTEM_NUM_URLS, PREDICATE_TYPE_SYSTEM_NUM_FRAMES, PREDICATE_TYPE_SYSTEM_DURATION, PREDICATE_TYPE_SYSTEM_FRAMERATE ):
( operator, value ) = serialisable_value
number_test = NumberTest.STATICCreateFromCharacters( operator, value )
if predicate_type in ( PREDICATE_TYPE_SYSTEM_FRAMERATE, PREDICATE_TYPE_SYSTEM_DURATION ):
if operator == '=':
number_test = NumberTest( operator = NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT, value = value, extra_value = 0.05 )
elif operator == HC.UNICODE_NOT_EQUAL:
number_test = NumberTest( operator = NUMBER_TEST_OPERATOR_LESS_THAN, value = value )
serialisable_value = number_test.GetSerialisableTuple()
new_serialisable_info = ( predicate_type, serialisable_value, inclusive )
return ( 8, new_serialisable_info )
def GetCopy( self ):
@ -2198,17 +2242,15 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
elif self._predicate_type in ( PREDICATE_TYPE_SYSTEM_NUM_NOTES, PREDICATE_TYPE_SYSTEM_NUM_WORDS, PREDICATE_TYPE_SYSTEM_NUM_URLS, PREDICATE_TYPE_SYSTEM_NUM_FRAMES, PREDICATE_TYPE_SYSTEM_DURATION ):
( operator, value ) = self._value
number_test = NumberTest.STATICCreateFromCharacters( operator, value )
number_test: NumberTest = self._value
if number_test.IsZero():
return Predicate( self._predicate_type, ( '>', 0 ) )
return Predicate( self._predicate_type, NumberTest.STATICCreateFromCharacters( '>', 0 ) )
elif number_test.IsAnythingButZero():
return Predicate( self._predicate_type, ( '=', 0 ) )
return Predicate( self._predicate_type, NumberTest.STATICCreateFromCharacters( '=', 0 ) )
else:
@ -2457,18 +2499,31 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_URLS: base = 'urls'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NOTES: base = 'notes'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS: base = 'file relationships'
elif self._predicate_type in ( PREDICATE_TYPE_SYSTEM_WIDTH, PREDICATE_TYPE_SYSTEM_HEIGHT, PREDICATE_TYPE_SYSTEM_NUM_NOTES, PREDICATE_TYPE_SYSTEM_NUM_URLS, PREDICATE_TYPE_SYSTEM_NUM_WORDS, PREDICATE_TYPE_SYSTEM_NUM_FRAMES ):
elif self._predicate_type in ( PREDICATE_TYPE_SYSTEM_WIDTH, PREDICATE_TYPE_SYSTEM_HEIGHT, PREDICATE_TYPE_SYSTEM_NUM_NOTES, PREDICATE_TYPE_SYSTEM_NUM_WORDS, PREDICATE_TYPE_SYSTEM_NUM_URLS, PREDICATE_TYPE_SYSTEM_NUM_FRAMES, PREDICATE_TYPE_SYSTEM_DURATION, PREDICATE_TYPE_SYSTEM_FRAMERATE ):
has_phrase = None
not_has_phrase = None
absolute_number_renderer = None
if self._predicate_type == PREDICATE_TYPE_SYSTEM_WIDTH:
base = 'width'
has_phrase = ': has width'
not_has_phrase = ': no width'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_HEIGHT:
base = 'height'
has_phrase = ': has height'
not_has_phrase = ': no height'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_FRAMERATE:
absolute_number_renderer = lambda s: f'{HydrusData.ToHumanInt(s)}fps'
base = 'framerate'
has_phrase = ': has framerate'
not_has_phrase = ': no framerate'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NUM_NOTES:
@ -2476,76 +2531,51 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
has_phrase = ': has notes'
not_has_phrase = ': no notes'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NUM_URLS:
base = 'number of urls'
has_phrase = ': has urls'
not_has_phrase = ': no urls'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NUM_WORDS:
base = 'number of words'
has_phrase = ': has words'
not_has_phrase = ': no words'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NUM_URLS:
base = 'number of urls'
has_phrase = ': has urls'
not_has_phrase = ': no urls'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NUM_FRAMES:
base = 'number of frames'
has_phrase = ': has frames'
not_has_phrase = ': no frames'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_DURATION:
absolute_number_renderer = HydrusTime.MillisecondsDurationToPrettyTime
base = 'duration'
has_phrase = ': has duration'
not_has_phrase = ': no duration'
if self._value is not None:
( operator, value ) = self._value
number_test: NumberTest = self._value
if operator == '>' and value == 0 and has_phrase is not None:
base += has_phrase
elif ( ( operator == '=' and value == 0 ) or ( operator == '<' and value == 1 ) ) and not_has_phrase is not None:
if number_test.IsZero() and not_has_phrase is not None:
base += not_has_phrase
else:
elif number_test.IsAnythingButZero() and has_phrase is not None:
base += ' {} {}'.format( operator, HydrusData.ToHumanInt( value ) )
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_DURATION:
base = 'duration'
if self._value is not None:
( operator, value ) = self._value
if operator == '>' and value == 0:
base = 'has duration'
elif operator == '=' and value == 0:
base = 'no duration'
base += has_phrase
else:
base += ' {} {}'.format( operator, HydrusTime.MillisecondsDurationToPrettyTime( value ) )
base += f' {number_test.ToString( absolute_number_renderer = absolute_number_renderer )}'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_FRAMERATE:
base = 'framerate'
if self._value is not None:
( operator, value ) = self._value
base += ' {} {}fps'.format( operator, HydrusData.ToHumanInt( value ) )
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME:
base = 'has note'

View File

@ -224,23 +224,23 @@ pred_generators = {
SystemPredicateParser.Predicate.NO_FORCED_FILETYPE : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_FORCED_FILETYPE, False ),
SystemPredicateParser.Predicate.LIMIT : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT, v ),
SystemPredicateParser.Predicate.FILETYPE : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_MIME, tuple( v ) ),
SystemPredicateParser.Predicate.HAS_DURATION : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( '>', 0 ) ),
SystemPredicateParser.Predicate.NO_DURATION : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( '=', 0 ) ),
SystemPredicateParser.Predicate.HAS_DURATION : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.NumberTest.STATICCreateFromCharacters( '>', 0 ) ),
SystemPredicateParser.Predicate.NO_DURATION : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 0 ) ),
SystemPredicateParser.Predicate.HAS_TAGS : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ( '*', '>', 0 ) ),
SystemPredicateParser.Predicate.UNTAGGED : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ( '*', '=', 0 ) ),
SystemPredicateParser.Predicate.NUM_OF_TAGS : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ( '*', o, v ) ),
SystemPredicateParser.Predicate.NUM_OF_TAGS_WITH_NAMESPACE : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, v ),
SystemPredicateParser.Predicate.NUM_OF_URLS : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ( o, v ) ),
SystemPredicateParser.Predicate.NUM_OF_WORDS : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ( o, v ) ),
SystemPredicateParser.Predicate.HEIGHT : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( o, v ) ),
SystemPredicateParser.Predicate.WIDTH : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( o, v ) ),
SystemPredicateParser.Predicate.NUM_OF_URLS : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ClientSearch.NumberTest.STATICCreateFromCharacters( o, v ) ),
SystemPredicateParser.Predicate.NUM_OF_WORDS : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.NumberTest.STATICCreateFromCharacters( o, v ) ),
SystemPredicateParser.Predicate.HEIGHT : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( o, v ) ),
SystemPredicateParser.Predicate.WIDTH : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.NumberTest.STATICCreateFromCharacters( o, v ) ),
SystemPredicateParser.Predicate.FILESIZE : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE, ( o, v, HydrusData.ConvertUnitToInt( u ) ) ),
SystemPredicateParser.Predicate.SIMILAR_TO_FILES : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_SIMILAR_TO_FILES, convert_hex_hashlist_and_other_to_bytes_and_other( v ) ),
SystemPredicateParser.Predicate.SIMILAR_TO_DATA : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_SIMILAR_TO_DATA, convert_double_hex_hashlist_and_other_to_double_bytes_and_other( v ) ),
SystemPredicateParser.Predicate.HASH : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HASH, convert_hex_hashlist_and_other_to_bytes_and_other( v ), inclusive = o == '=' ),
SystemPredicateParser.Predicate.DURATION : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( o, v[0] * 1000 + v[1] ) ),
SystemPredicateParser.Predicate.FRAMERATE : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE, ( o, v ) ),
SystemPredicateParser.Predicate.NUM_OF_FRAMES : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_FRAMES, ( o, v ) ),
SystemPredicateParser.Predicate.DURATION : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.NumberTest.STATICCreateFromCharacters( o, v[0] * 1000 + v[1] ) ),
SystemPredicateParser.Predicate.FRAMERATE : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE, ClientSearch.NumberTest.STATICCreateFromCharacters( o, v ) ),
SystemPredicateParser.Predicate.NUM_OF_FRAMES : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_FRAMES, ClientSearch.NumberTest.STATICCreateFromCharacters( o, v ) ),
SystemPredicateParser.Predicate.NUM_PIXELS : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_PIXELS, ( o, v, HydrusData.ConvertPixelsToInt( u ) ) ),
SystemPredicateParser.Predicate.RATIO : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO, ( o, v[0], v[1] ) ),
SystemPredicateParser.Predicate.RATIO_SPECIAL : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO, ( o, v[0], v[1] ) ),
@ -265,9 +265,9 @@ pred_generators = {
SystemPredicateParser.Predicate.TIME_IMPORTED : lambda o, v, u: date_pred_generator( ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, o, v ),
SystemPredicateParser.Predicate.FILE_SERVICE : file_service_pred_generator,
SystemPredicateParser.Predicate.NUM_FILE_RELS : num_file_relationships_pred_generator,
SystemPredicateParser.Predicate.HAS_NOTES : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ( '>', 0 ) ),
SystemPredicateParser.Predicate.NO_NOTES : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ( '=', 0 ) ),
SystemPredicateParser.Predicate.NUM_NOTES : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ( o, v ) ),
SystemPredicateParser.Predicate.HAS_NOTES : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ClientSearch.NumberTest.STATICCreateFromCharacters( '>', 0 ) ),
SystemPredicateParser.Predicate.NO_NOTES : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 0 ) ),
SystemPredicateParser.Predicate.NUM_NOTES : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ClientSearch.NumberTest.STATICCreateFromCharacters( o, v ) ),
SystemPredicateParser.Predicate.HAS_NOTE_NAME : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME, ( True, strip_quotes( v ) ) ),
SystemPredicateParser.Predicate.NO_NOTE_NAME : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME, ( False, strip_quotes( v ) ) ),
SystemPredicateParser.Predicate.HAS_RATING : lambda o, v, u: rating_service_pred_generator( '=', ( 'rated', v ) ),

View File

@ -105,8 +105,8 @@ options = {}
# Misc
NETWORK_VERSION = 20
SOFTWARE_VERSION = 563
CLIENT_API_VERSION = 60
SOFTWARE_VERSION = 564
CLIENT_API_VERSION = 61
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
@ -1359,6 +1359,7 @@ UNICODE_BYTE_ORDER_MARK = '\uFEFF'
UNICODE_ELLIPSIS = '\u2026'
UNICODE_NOT_EQUAL = '\u2260'
UNICODE_REPLACEMENT_CHARACTER = '\ufffd'
UNICODE_PLUS_OR_MINUS = '\u00B1'
URL_TYPE_POST = 0
URL_TYPE_API = 1

View File

@ -93,7 +93,16 @@ def CleanRunningFile( db_path, instance ):
# TODO: remove all the 'Convert' from these
def ConvertFloatToPercentage( f ):
return '{:.1f}%'.format( f * 100 )
percent = f * 100
if percent == int( percent ):
return f'{int( percent )}%'
else:
return f'{percent:.1f}%'
def ConvertIntToPixels( i ):

View File

@ -388,14 +388,14 @@ class TestClientDB( unittest.TestCase ):
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_ARCHIVE, None, 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( '<', 100, ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( '<', 0, ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( HC.UNICODE_APPROX_EQUAL, 100, ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( HC.UNICODE_APPROX_EQUAL, 0, ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( '=', 100, ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( '=', 0, ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( '>', 100, ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( '>', 0, ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 100, ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 0, ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.NumberTest.STATICCreateFromCharacters( HC.UNICODE_APPROX_EQUAL, 100, ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.NumberTest.STATICCreateFromCharacters( HC.UNICODE_APPROX_EQUAL, 0, ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 100, ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 0, ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.NumberTest.STATICCreateFromCharacters( '>', 100, ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.NumberTest.STATICCreateFromCharacters( '>', 0, ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_EVERYTHING, None, 1 ) )
@ -429,16 +429,16 @@ class TestClientDB( unittest.TestCase ):
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HASH, ( ( hash, ), 'sha256' ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HASH, ( ( bytes.fromhex( '0123456789abcdef' * 4 ), ), 'sha256' ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '<', 201 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '<', 200 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '<', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( HC.UNICODE_APPROX_EQUAL, 200 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( HC.UNICODE_APPROX_EQUAL, 60 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( HC.UNICODE_APPROX_EQUAL, 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '=', 200 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '=', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '>', 200 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '>', 199 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 201 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 200 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( HC.UNICODE_APPROX_EQUAL, 200 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( HC.UNICODE_APPROX_EQUAL, 60 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( HC.UNICODE_APPROX_EQUAL, 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 200 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( '>', 200 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( '>', 199 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_INBOX, None, 1 ) )
@ -472,21 +472,21 @@ class TestClientDB( unittest.TestCase ):
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ( '', '>', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ( '', '>', 1 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ( '<', 1 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ( '<', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ( HC.UNICODE_APPROX_EQUAL, 0 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ( HC.UNICODE_APPROX_EQUAL, 1 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ( '=', 0 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ( '=', 1 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ( '>', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ( '>', 1 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 1 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.NumberTest.STATICCreateFromCharacters( HC.UNICODE_APPROX_EQUAL, 0 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.NumberTest.STATICCreateFromCharacters( HC.UNICODE_APPROX_EQUAL, 1 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 0 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 1 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.NumberTest.STATICCreateFromCharacters( '>', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.NumberTest.STATICCreateFromCharacters( '>', 1 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ( '<', 1 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ( '<', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ( '=', 0 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ( '=', 1 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ( '>', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ( '>', 1 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 1 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 0 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 1 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ClientSearch.NumberTest.STATICCreateFromCharacters( '>', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ClientSearch.NumberTest.STATICCreateFromCharacters( '>', 1 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO, ( '=', 1, 1 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO, ( '=', 4, 3 ), 0 ) )
@ -521,16 +521,16 @@ class TestClientDB( unittest.TestCase ):
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE, ( '>', 0, HydrusData.ConvertUnitToInt( 'MB' ) ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE, ( '>', 0, HydrusData.ConvertUnitToInt( 'GB' ) ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( '<', 201 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( '<', 200 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( '<', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( HC.UNICODE_APPROX_EQUAL, 200 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( HC.UNICODE_APPROX_EQUAL, 60 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( HC.UNICODE_APPROX_EQUAL, 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( '=', 200 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( '=', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( '>', 200 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( '>', 199 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 201 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 200 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.NumberTest.STATICCreateFromCharacters( HC.UNICODE_APPROX_EQUAL, 200 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.NumberTest.STATICCreateFromCharacters( HC.UNICODE_APPROX_EQUAL, 60 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.NumberTest.STATICCreateFromCharacters( HC.UNICODE_APPROX_EQUAL, 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 200 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 0 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.NumberTest.STATICCreateFromCharacters( '>', 200 ), 0 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.NumberTest.STATICCreateFromCharacters( '>', 199 ), 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT, 100, 1 ) )
tests.append( ( ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT, 1, 1 ) )
@ -652,7 +652,7 @@ class TestClientDB( unittest.TestCase ):
preds = []
preds.append( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, 'car' ) )
preds.append( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '<', 201 ) ) )
preds.append( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 201 ) ) )
or_pred = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_OR_CONTAINER, preds )

View File

@ -1734,7 +1734,7 @@ class TestTagObjects( unittest.TestCase ):
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( '<', 200 ) )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 200 ) )
self.assertEqual( p.ToString(), 'system:duration < 200 milliseconds' )
self.assertEqual( p.GetNamespace(), 'system' )
@ -1848,7 +1848,7 @@ class TestTagObjects( unittest.TestCase ):
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '<', 2000 ) )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 2000 ) )
self.assertEqual( p.ToString(), 'system:height < 2,000' )
self.assertEqual( p.GetNamespace(), 'system' )
@ -1914,13 +1914,13 @@ class TestTagObjects( unittest.TestCase ):
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ( '<', 5 ) )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_URLS, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 5 ) )
self.assertEqual( p.ToString(), 'system:number of urls < 5' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ( '<', 5000 ) )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 5000 ) )
self.assertEqual( p.ToString(), 'system:number of words < 5,000' )
self.assertEqual( p.GetNamespace(), 'system' )
@ -1964,7 +1964,7 @@ class TestTagObjects( unittest.TestCase ):
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( '=', 1920 ) )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.NumberTest.STATICCreateFromCharacters( '=', 1920 ) )
self.assertEqual( p.ToString(), 'system:width = 1,920' )
self.assertEqual( p.GetNamespace(), 'system' )
@ -2008,7 +2008,7 @@ class TestTagObjects( unittest.TestCase ):
#
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_OR_CONTAINER, [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '<', 2000 ) ), ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, 'blue eyes' ), ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, 'character:samus aran' ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_OR_CONTAINER, [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.NumberTest.STATICCreateFromCharacters( '<', 2000 ) ), ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, 'blue eyes' ), ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, 'character:samus aran' ) ] )
self.assertEqual( p.ToString(), 'blue eyes OR character:samus aran OR system:height < 2,000' )
self.assertEqual( p.GetNamespace(), '' )
@ -2042,10 +2042,10 @@ class TestTagObjects( unittest.TestCase ):
( 'system:archive', "system:archive " ),
( 'system:archive', "system:archived " ),
( 'system:archive', "system:archived" ),
( 'system:has duration', "system:has duration" ),
( 'system:has duration', "system:has_duration" ),
( 'system:no duration', " system:no_duration" ),
( 'system:no duration', "system:no duration" ),
( 'system:duration: has duration', "system:has duration" ),
( 'system:duration: has duration', "system:has_duration" ),
( 'system:duration: no duration', " system:no_duration" ),
( 'system:duration: no duration', "system:no duration" ),
( 'system:is the best quality file of its duplicate group', "system:is the best quality file of its group" ),
( 'system:is not the best quality file of its duplicate group', "system:isn't the best quality file of its duplicate group" ),
( 'system:is not the best quality file of its duplicate group', 'system:is not the best quality file of its duplicate group' ),
@ -2143,7 +2143,7 @@ class TestTagObjects( unittest.TestCase ):
( 'system:import time: on the day of 2020-01-03', "system:import time: the day of 2020-01-03" ),
( 'system:import time: around 7 days ago', "system:date imported around 7 days ago" ),
( 'system:duration < 5.0 seconds', "system:duration < 5 seconds" ),
( f'system:duration {HC.UNICODE_APPROX_EQUAL} 11.0 seconds', "system:duration ~= 5 sec 6000 msecs" ),
( f'system:duration {HC.UNICODE_APPROX_EQUAL} 11.0 seconds {HC.UNICODE_PLUS_OR_MINUS}15%', "system:duration ~= 5 sec 6000 msecs" ),
( 'system:duration > 3 milliseconds', "system:duration > 3 milliseconds" ),
( 'system:is pending to my files', "system:file service is pending to my files" ),
( 'system:is pending to my files', "system:file service is pending to MY FILES" ),