Version 479

closes #1095, closes #1105
This commit is contained in:
Hydrus Network Developer 2022-03-30 15:28:13 -05:00
parent 5bb46ecdd9
commit 0c55d1b29e
58 changed files with 1124 additions and 479 deletions

View File

@ -1,9 +0,0 @@
.open client.db
.out my_options.sql
.print .open client.db\r\n
.print delete from options;\r\n
.print delete from json_dumps where dump_type = 22;\r\n
.mode insert options
select * from options;
.mode insert json_dumps
select * from json_dumps where dump_type = 22;

View File

@ -1,6 +0,0 @@
.print The subscriptions will lose their tag import options, so make sure to check them once they are imported back in.
.open client.db
.out my_subscriptions.sql
.print .open client.db\r\n
.mode insert json_dumps_named
select * from json_dumps_named where dump_type = 3;

3
db/extract_version.bat Normal file
View File

@ -0,0 +1,3 @@
@ECHO off
sqlite3 < extract_version.sql
SET /P gumpf=Hit Enter to exit!

2
db/extract_version.sql Normal file
View File

@ -0,0 +1,2 @@
.open --readonly client.db
SELECT "This database is version " || version FROM version;

View File

@ -1,9 +0,0 @@
If your db is completely broken and you need to extract some important data, please check out the emergency extract scripts. To use them, put your old database, the sqlite3 executable, and the script in the same folder and feed the script into sqlite3, like so:
sqlite3 < extract_subscriptions.sql
This will connect to the database and copy your subscriptions to the new file my_subscriptions.sql, which you can then move and import to a new db folder in the same way:
sqlite3 < my_subscriptions.sql
Some things are difficult to copy over at this basic level. Your tag options and anything else service-specific will be lost or reset back to default.

View File

@ -3,6 +3,49 @@
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
## [Version 479](https://github.com/hydrusnetwork/hydrus/releases/tag/v479)
### misc
* when shift-selecting some thumbnails, you can now reverse the direction of the select and what you just selected will be deselected, basically a full undo (issue #1105)
* when ctrl-selecting thumbnails, if you add to the selection, the file you click is now focused and always previewed (previously this only happened if there was no focused file already). this is related to the shift-select logic above, but it may be annoying when making a big ctrl-selection of videos etc.. so let me know and I can make this more clever if needed
* added file sort 'file->hash', which sorts pseudorandomly but repeatably. it sounds not super clever, but it will be useful for certain comparison operations across clients
* when you hit 'copy->hash' on a file right-click, it now shows the sha256 hash for quick review
* in the duplicate filter, the zoom locking tech now works better™ when one of the pair is portrait and the other landscape. it now tries to select either width or height to lock both when going AB and BA. it also chooses the 'better' of width or height by choosing the zoom that'll change the size less radically. previously, it could do width on AB and height on BA, which lead to a variety of odd situations. there are probably still some issues here, most likely when one of the files almost exactly fills the whole canvas, so let me know how you get on
* webps with transparency should now load correct! previously they were going crazy in the transparent area. all webps are scheduled a thumbnail regen this week
* when import folders run, the count on their progress bar now ignores previous failed and ignored entries. it should always start 0, like 0/100, rather than 20/120 etc...
* when import folders run, any imports where the status type is set to 'leave the file alone' is now still scanned at the end of a job. if the path does not exist any more, it is removed from the import list
* fixed a typo bug in the recent delete code cleanup that meant 'delete files after export' after a manual export was only working on the last file in the selection. sorry for the trouble!
* the delete files dialog now starts with keyboard focus on the action radiobox (it was defaulting to ok button since I added the recent panel disable tech)
* if a network job has a connection error or serverside bandwidth block and then waits before retrying, it now checks if all network jobs have just been paused and will not reattempt the connection if so (issue #1095)
* fixed a bug in thumbnail fallback rendering
* fixed another problem with cloudscraper's new method names. it should work for users still on an old version
* wrote a little 'extract version' sql and bat file for the db folder that simply pull the version from the client.db file in the same directory. I removed the extract options/subscriptions sql scripts since they are super old and out of date, but this general system may return in future
### file history chart
* added 'archive' line to the file history chart. this isn't exactly (current_count - inbox_count), but it pretty much is
* added a 'show deleted' checkbox to the file history chart. it will recalculate the y axis range on click, so if you have loads of deleted files, you can now hide them to see current better
* improved the way data is aggregated in the file history chart. diagonal lines should be reduced during any periods of client import-inactivity, and spikes should show better
* also bumped the number of steps up to 8,000, so it should look nice maximised on a 4k
* the file history chart now remembers its last size and position--it has an entry under options->gui
### client api
* thanks to a user, the Client API now accepts any file_id, file_ids, hash, or hashes as arguments in any place where you need to specify a file or files
* like 'return_hashes', the 'search_files' command in the Client API now takes an optional 'return_file_ids' parameter, default true, to turn off the file ids if you only want hashes
* added 'only_return_basic_information' parameter, default false, to 'get_metadata' call, which is fast for first-time requests (it is slim but not well cached) and just delivers the basics like resolution and file size
* added unit tests and updated the help to reflect the above
* client api version is now 29
### help
* split up the 'more files' help section into 'powerful searching' and 'exporting files', both still under the 'next steps' section
* moved the semi-advanced 'OR' section from 'tags' to 'searching'
* brushed up misc help
* a couple of users added some misc help updates too, thank you!
### misc boring cleanup
* cleaned up an old wx label patch
* cleaned up an old wx system colour patch
* cleaned up some misc initialisation code
## [Version 478](https://github.com/hydrusnetwork/hydrus/releases/tag/v478)
### misc

View File

@ -1242,7 +1242,8 @@ Arguments (in percent-encoded JSON):
* `tag_service_key`: (optional, selective, hexadecimal, the tag domain on which to search)
* `file_sort_type`: (optional, integer, the results sort method)
* `file_sort_asc`: true or false (optional, the results sort order)
* `return_hashes`: true or false (optional, default false, returns hex hashes in addition to file ids, hashes and file ids are in the same order)
* `return_file_ids`: true or false (optional, default true, returns file id results)
* `return_hashes`: true or false (optional, default false, returns hex hash results)
* _`system_inbox`: true or false (obsolete, use tags)_
* _`system_archive`: true or false (obsolete, use tags)_
@ -1394,7 +1395,9 @@ Response:
}
```
File ids are internal and specific to an individual client. For a client, a file with hash H always has the same file id N, but two clients will have different ideas about which N goes with which H. They are a bit faster than hashes to retrieve and search with _en masse_, which is why they are exposed here.
You can of course also specify `return_hashes=true&return_file_ids=false` just to get the hashes. The order of both lists is the same.
File ids are internal and specific to an individual client. For a client, a file with hash H always has the same file id N, but two clients will have different ideas about which N goes with which H. IDs are a bit faster to retrieve than hashes and search with _en masse_, which is why they are exposed here.
This search does **not** apply the implicit limit that most clients set to all searches (usually 10,000), so if you do system:everything on a client with millions of files, expect to get boshed. Even with a system:limit included, complicated queries with large result sets may take several seconds to respond. Just like the client itself.
@ -1414,6 +1417,7 @@ Arguments (in percent-encoded JSON):
* `hash`: (selective, a hexadecimal SHA256 hash)
* `hashes`: (selective, a list of hexadecimal SHA256 hashes)
* `only_return_identifiers`: true or false (optional, defaulting to false)
* `only_return_basic_information`: true or false (optional, defaulting to false)
* `detailed_url_information`: true or false (optional, defaulting to false)
* `hide_service_names_tags`: true or false (optional, defaulting to false)
* `include_notes`: true or false (optional, defaulting to false)
@ -1559,6 +1563,38 @@ Response:
]
}
```
```json title="And where only_return_basic_information is true"
{
"metadata": [
{
"file_id": 123,
"hash": "4c77267f93415de0bc33b7725b8c331a809a924084bee03ab2f5fae1c6019eb2",
"size": 63405,
"mime": "image/jpg",
"ext": ".jpg",
"width": 640,
"height": 480,
"duration": null,
"has_audio": false,
"num_frames": null,
"num_words": null,
},
{
"file_id": 4567,
"hash": "3e7cb9044fe81bda0d7a84b5cb781cba4e255e4871cba6ae8ecd8207850d5b82",
"size": 199713,
"mime": "video/webm",
"ext": ".webm",
"width": 1920,
"height": 1080,
"duration": 4040,
"has_audio": true,
"num_frames": 102,
"num_words": null,
}
]
}
```
Size is in bytes. Duration is in milliseconds, and may be an int or a float.
@ -1578,6 +1614,8 @@ The tag structure is duplicated for both `name` and `key`. The use of `name` is
While `service_XXX_to_statuses_to_tags` represent the actual tags stored on the database for a file, the <code>service_XXX_to_statuses_to_<i>display</i>_tags</code> structures reflect how tags appear in the UI, after siblings are collapsed and parents are added. If you want to edit a file's tags, start with `service_keys_to_statuses_to_tags`. If you want to render to the user, use `service_keys_to_statuses_to_displayed_tags`.
If you set `only_return_basic_information=true`, this will be much faster for first-time requests than the full metadata result, but it will be slower for repeat requests. The full metadata object is cached after first fetch, the limited file info object is not.
If you add `hide_service_names_tags=true`, the `service_names_to_statuses_to_tags` and `service_names_to_statuses_to_display_tags` Objects will not be included. Use this to save data/CPU on large queries.
If you add `detailed_url_information=true`, a new entry, `detailed_known_urls`, will be added for each file, with a list of the same structure as /`add_urls/get_url_info`. This may be an expensive request if you are querying thousands of files at once.

View File

@ -0,0 +1,49 @@
---
title: exporting files
---
# exporting files
## exporting { id="exporting" }
There are many ways to export files from the client:
* **drag and drop**
Just dragging from the thumbnail view will export (copy) all the selected files to wherever you drop them. You can also start a drag and drop for single files from the media viewer using this arrow button on the top hover window:
![](images/media_viewer_dnd.png)
If you want to drag and drop to discord, check the special BUGFIX option under _options->gui_.
By default, the files will be named by their ugly hexadecimal [hash](faq.md#hashes), which is how they are stored inside the database. Once you learn filename patterns (practise with manual exports, as below!), you will be able to change this in the options if you wish.
If you use a drag and drop to open a file inside an image editing program, remember to hit 'save as' and give it a new filename in a new location! The client does not expect files inside its db directory to ever change.
* **share->copy->files**
This will copy the files themselves to your clipboard. You can then paste them wherever you like, just as with normal files. They will have their hashes for filenames.
This is a very quick operation. It can also be triggered by hitting Ctrl+C.
* **share->copy->image (bitmap)**
This copies a file's rendered image data to your clipboard. This is useful for pasting into an image editor, but do not use it to upload images to the internet.
* **share->copy->hashes**
This will copy the files' unique identifiers to your clipboard, in hexadecimal.
You will not have to do this often. It is best when you want to identify a number of files to someone else without having to send them the actual files.
* **export dialog**
Right clicking some files and selecting _share->export->files_ will open this dialog:
![](images/export.png)
Which lets you export the selected files with custom filenames. It will initialise trying to export the files named by their hashes, but once you are comfortable with tags, you'll be able to generate much cleverer and prettier filenames.
* **export folders**
You can set up a regularly repeating export under _file->import and export folders_. This is an advanced operation, so best left until you know the client a bit better, but it is very useful if you want to regularly export some of your collection to a revolving wallpaper directory or similar.

View File

@ -1,8 +1,39 @@
---
title: more files
title: powerful searching
---
# more getting started with files
# powerful searching
## the dropdown controls
Let's look at the tag autocomplete dropdown again:
![](images/ac_dropdown.png)
* **favourite searches star**
Once you get experience with the client, have a play with this. Rather than leaving common search pages open, save them in here and load them up as needed. You will keep your client lightweight and save time.
* **include current/pending tags**
Turn these on and off to control whether tag _search predicates_ apply to tags the exist, or limit just to those pending to be uploaded to a tag repository. Just searching 'pending' tags is useful if you want to scan what you have pending to go up to the PTR--just turn off 'current' tags and search `system:num tags > 0`.
* **searching immediately**
This controls whether a change to the search tags will instantly run the new search and get new results. Turning this off is helpful if you want to add, remove, or replace several heavy search terms in a row without getting UI lag.
* **OR**
You only see this if you have 'advanced mode' on. It lets you enter some pretty complicated tags!
* **file/tag domains**
By default, you will search in 'my files' and 'all known tags' domain. This is the intersection of your local media files (on your hard disk) and the union of all known tag searches. If you search for `character:samus aran`, then you will get file results from your 'my files' domain that have `character:samus aran` in any tag service. For most purposes, this search domain is fine, but as you use the client more, you may want to access different search domains.
For instance, if you change the file domain to 'trash', then you will instead get files that are in your trash. Setting the tag domain to 'my tags' will ignore other tag services (e.g. the PTR) for all tag search predicates, so a `system:num_tags` or a `character:samus aran` will only look 'my tags'.
Turning on 'advanced mode' gives access to more search domains. Some of them are subtly complicated and only useful for clever jobs--most of the time, you still want 'my files' and 'all known tags'.
## searching with wildcards { id="wildcards" }
@ -24,36 +55,27 @@ This is particularly useful if you have a number of files with commonly structur
In this case, selecting the `title:cool pic*` predicate will return all three images in the same search, where you can conveniently give them some more-easily searched tags like `series:cool pic` and `page:1`, `page:2`, `page:3`.
## more searching
## OR searching
Let's look at the tag autocomplete dropdown again:
Searches find files that match every search 'predicate' in the list (it is an **AND** search), which makes it difficult to search for files that include one **OR** another tag. More recently, simple OR search support was added. All you have to do is hold down Shift when you enter/double-click a tag in the autocomplete entry area. Instead of sending the tag up to the active search list up top, it will instead start an under-construction 'OR chain' in the tag results below:
![](images/ac_dropdown.png)
![](images/or_under_construction.png)
* **favourite searches star**
Once you get experience with the client, have a play with this. Rather than leaving common search pages open, save them in here and load them up as needed. You will keep your client lightweight and save time.
* **include current/pending tags**
Turn these on and off to control whether tag _search predicates_ apply to tags the exist, or limit just to those pending to be uploaded to a tag repository. Just searching 'pending' tags is useful if you want to scan what you have pending to go up to the PTR--just turn off 'current' tags and search `system:num tags > 0`.
* **searching immediately**
This controls whether a change to the search tags will instantly run the new search and get new results. Turning this off is helpful if you want to add, remove, or replace several heavy search terms in a row without getting UI lag.
* **OR**
You only see this if you have 'advanced mode' on. It is an experimental module. Have a play with it--it lets you enter some pretty complicated tags!
* **file/tag domains**
By default, you will search in 'my files' and 'all known tags' domain. This is the intersection of your local media files (on your hard disk) and the union of all known tag searches. If you search for `character:samus aran`, then you will get file results from your 'my files' domain that have `character:samus aran` in any tag service. For most purposes, this search domain is fine, but as you use the client more, you may want to access different search domains.
For instance, if you change the file domain to 'trash', then you will instead get files that are in your trash. Setting the tag domain to 'my tags' will ignore other tag services (e.g. the PTR) for all tag search predicates, so a `system:num_tags` or a `character:samus aran` will only look 'my tags'.
Turning on 'advanced mode' gives access to more search domains. Some of them are subtly complicated and only useful for clever jobs--most of the time, you still want 'my files' and 'all known tags'.
You can keep searching for and entering new tags. Holding down Shift on new tags will extend the OR chain, and entering them as normal will 'cap' the chain and send it to the complete and active search predicates above.
![](images/or_done.png)
Any file that has one or more of those OR sub-tags will match.
If you enter an OR tag incorrectly, you can either cancel or 'rewind' the under-construction search predicate with these new buttons that will appear:
![](images/or_buttons.png)
You can also cancel an under-construction OR by hitting Esc on an empty input. You can add any sort of search term to an OR search predicate, including system predicates. Some unusual sub-predicates (typically a `-tag`, or a very broad system predicate) can run very slowly, but they will run much faster if you include non-OR search predicates in the search:
![](images/or_mixed.png)
This search will return all files that have the tag `fanfic` and one or more of `medium:text`, a positive value for the like/dislike rating 'read later', or PDF mime.
## sorting with system limit
@ -62,35 +84,3 @@ If you add system:limit to a search, the client will consider what that page's f
If you change the sort, hydrus will not refresh the search, it'll just re-sort the n files you have. Hit F5 to refresh the search with a new sort.
Not all sorts are supported. Anything complicated like tag sort will result in a random sample instead.
## exporting and uploading { id="intro" }
There are many ways to export files from the client:
* **drag and drop**
Just dragging from the thumbnail view will export (copy) all the selected files to wherever you drop them.
The files will be named by their ugly hexadecimal [hash](faq.md#hashes), which is how they are stored inside the database.
If you use this to open a file inside an image editing program, remember to go 'save as' and give it a new filename! The client does not expect files inside its db directory to change.
* **export dialog**
Right clicking some files and selecting _share->export->files_ will open this dialog:
![](images/export.png)
Which lets you export the selected files with custom filenames. It will initialise trying to export the files named by their hashes, but once you are comfortable with tags, you'll be able to generate much cleverer and prettier filenames.
* **share->copy->files**
This will copy the files themselves to your clipboard. You can then paste them wherever you like, just as with normal files. They will have their hashes for filenames.
This is a very quick operation. It can also be triggered by hitting Ctrl+C.
* **share->copy->hashes**
This will copy the files' unique identifiers to your clipboard, in hexadecimal.
You will not have to do this often. It is best when you want to identify a number of files to someone else without having to send them the actual files.

View File

@ -2,7 +2,7 @@
title: subscriptions
---
# getting started with subscriptions
# subscriptions
Do not try to create a subscription until you are comfortable with a normal gallery download page! Go [here](getting_started_downloading.md).
@ -125,4 +125,4 @@ The second case is a safety stopgap for hydrus. If a site decides to have `/post
## I put character queries in my artist sub, and now things are all mixed up { id="merging_and_separating" }
On the main subscription dialog, there are 'merge' and 'separate' buttons. These are powerful, but they will walk you through the process of pulling queries out of a sub and merging them back into a different one. Only subs that use the same download source can be merged. Give them a go, and if it all goes wrong, just hit the cancel button on the dialog.
On the main subscription dialog, there are 'merge' and 'separate' buttons. These are powerful, but they will walk you through the process of pulling queries out of a sub and merging them back into a different one. Only subs that use the same download source can be merged. Give them a go, and if it all goes wrong, just hit the cancel button on the dialog.

View File

@ -41,28 +41,6 @@ If you add more tags or system predicates to a search, you will limit the result
You can also exclude a tag by prefixing it with a hyphen (e.g. `-heresy`).
## OR searching
Searches find files that match every search 'predicate' in the list (it is an **AND** search), which makes it difficult to search for files that include one **OR** another tag. More recently, simple OR search support was added. All you have to do is hold down Shift when you enter/double-click a tag in the autocomplete entry area. Instead of sending the tag up to the active search list up top, it will instead start an under-construction 'OR chain' in the tag results below:
![](images/or_under_construction.png)
You can keep searching for and entering new tags. Holding down Shift on new tags will extend the OR chain, and entering them as normal will 'cap' the chain and send it to the complete and active search predicates above.
![](images/or_done.png)
Any file that has one or more of those OR sub-tags will match.
If you enter an OR tag incorrectly, you can either cancel or 'rewind' the under-construction search predicate with these new buttons that will appear:
![](images/or_buttons.png)
You can also cancel an under-construction OR by hitting Esc on an empty input. You can add any sort of search term to an OR search predicate, including system predicates. Some unusual sub-predicates (typically a `-tag`, or a very broad system predicate) can run very slowly, but they will run much faster if you include non-OR search predicates in the search:
![](images/or_mixed.png)
This search will return all files that have the tag `fanfic` and one or more of `medium:text`, a positive value for the like/dislike rating 'read later', or PDF mime.
## tag repositories
It can take a long time to tag even small numbers of files well, so I created _tag repositories_ so people can share the work.
@ -93,4 +71,4 @@ I recommend you not spam tags to the public tag repo until you get a rough feel
You can connect to more than one tag repository if you like. When you are in the _manage tags_ dialog, pressing the up or down arrow keys on an empty input switches between your services.
[FAQ: why can my friend not see what I just uploaded?](faq.md#delays)
[FAQ: why can my friend not see what I just uploaded?](faq.md#delays)

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -33,6 +33,49 @@
<div class="content">
<h3 id="changelog"><a href="#changelog">changelog</a></h3>
<ul>
<li><h3 id="version_479"><a href="#version_479">version 479</a></h3></li>
<ul>
<li>misc:</li>
<li>when shift-selecting some thumbnails, you can now reverse the direction of the select and what you just selected will be deselected, basically a full undo (issue #1105)</li>
<li>when ctrl-selecting thumbnails, if you add to the selection, the file you click is now focused and always previewed (previously this only happened if there was no focused file already). this is related to the shift-select logic above, but it may be annoying when making a big ctrl-selection of videos etc.. so let me know and I can make this more clever if needed</li>
<li>added file sort 'file->hash', which sorts pseudorandomly but repeatably. it sounds not super clever, but it will be useful for certain comparison operations across clients</li>
<li>when you hit 'copy->hash' on a file right-click, it now shows the sha256 hash for quick review</li>
<li>in the duplicate filter, the zoom locking tech now works better™ when one of the pair is portrait and the other landscape. it now tries to select either width or height to lock both when going AB and BA. it also chooses the 'better' of width or height by choosing the zoom that'll change the size less radically. previously, it could do width on AB and height on BA, which lead to a variety of odd situations. there are probably still some issues here, most likely when one of the files almost exactly fills the whole canvas, so let me know how you get on</li>
<li>webps with transparency should now load correct! previously they were going crazy in the transparent area. all webps are scheduled a thumbnail regen this week</li>
<li>when import folders run, the count on their progress bar now ignores previous failed and ignored entries. it should always start 0, like 0/100, rather than 20/120 etc...</li>
<li>when import folders run, any imports where the status type is set to 'leave the file alone' is now still scanned at the end of a job. if the path does not exist any more, it is removed from the import list</li>
<li>fixed a typo bug in the recent delete code cleanup that meant 'delete files after export' after a manual export was only working on the last file in the selection. sorry for the trouble!</li>
<li>the delete files dialog now starts with keyboard focus on the action radiobox (it was defaulting to ok button since I added the recent panel disable tech)</li>
<li>if a network job has a connection error or serverside bandwidth block and then waits before retrying, it now checks if all network jobs have just been paused and will not reattempt the connection if so (issue #1095)</li>
<li>fixed a bug in thumbnail fallback rendering</li>
<li>fixed another problem with cloudscraper's new method names. it should work for users still on an old version</li>
<li>wrote a little 'extract version' sql and bat file for the db folder that simply pull the version from the client.db file in the same directory. I removed the extract options/subscriptions sql scripts since they are super old and out of date, but this general system may return in future</li>
<li>.</li>
<li>file history chart:</li>
<li>added 'archive' line to the file history chart. this isn't exactly (current_count - inbox_count), but it pretty much is</li>
<li>added a 'show deleted' checkbox to the file history chart. it will recalculate the y axis range on click, so if you have loads of deleted files, you can now hide them to see current better</li>
<li>improved the way data is aggregated in the file history chart. diagonal lines should be reduced during any periods of client import-inactivity, and spikes should show better</li>
<li>also bumped the number of steps up to 8,000, so it should look nice maximised on a 4k</li>
<li>the file history chart now remembers its last size and position--it has an entry under options->gui</li>
<li>.</li>
<li>client api:</li>
<li>thanks to a user, the Client API now accepts any file_id, file_ids, hash, or hashes as arguments in any place where you need to specify a file or files</li>
<li>like 'return_hashes', the 'search_files' command in the Client API now takes an optional 'return_file_ids' parameter, default true, to turn off the file ids if you only want hashes</li>
<li>added 'only_return_basic_information' parameter, default false, to 'get_metadata' call, which is fast for first-time requests (it is slim but not well cached) and just delivers the basics like resolution and file size</li>
<li>added unit tests and updated the help to reflect the above</li>
<li>client api version is now 29</li>
<li>.</li>
<li>help:</li>
<li>split up the 'more files' help section into 'powerful searching' and 'exporting files', both still under the 'next steps' section</li>
<li>moved the semi-advanced 'OR' section from 'tags' to 'searching'</li>
<li>brushed up misc help</li>
<li>a couple of users added some misc help updates too, thank you!</li>
<li>.</li>
<li>misc boring cleanup:</li>
<li>cleaned up an old wx label patch</li>
<li>cleaned up an old wx system colour patch</li>
<li>cleaned up some misc initialisation code</li>
</ul>
<li><h3 id="version_478"><a href="#version_478">version 478</a></h3></li>
<ul>
<li>misc:</li>

View File

@ -654,8 +654,9 @@ class ThumbnailCache( object ):
thumbnail_mime = HC.IMAGE_JPEG
# we don't actually know this, it comes down to detailed stuff, but since this is png vs jpeg it isn't a huge deal down in the guts of image loading
# only really matters with transparency, so anything that can be transparent we'll prime with a png thing
# ain't like I am encoding EXIF rotation in my jpeg thumbs
if mime in ( HC.IMAGE_APNG, HC.IMAGE_PNG, HC.IMAGE_GIF, HC.IMAGE_ICON ):
if mime in ( HC.IMAGE_APNG, HC.IMAGE_PNG, HC.IMAGE_GIF, HC.IMAGE_ICON, HC.IMAGE_WEBP ):
thumbnail_mime = HC.IMAGE_PNG

View File

@ -323,6 +323,7 @@ SORT_FILES_BY_NUM_FRAMES = 16
SORT_FILES_BY_NUM_COLLECTION_FILES = 17
SORT_FILES_BY_LAST_VIEWED_TIME = 18
SORT_FILES_BY_ARCHIVED_TIMESTAMP = 19
SORT_FILES_BY_HASH = 20
SYSTEM_SORT_TYPES = {
SORT_FILES_BY_NUM_COLLECTION_FILES,
@ -344,7 +345,8 @@ SYSTEM_SORT_TYPES = {
SORT_FILES_BY_IMPORT_TIME,
SORT_FILES_BY_FILE_MODIFIED_TIMESTAMP,
SORT_FILES_BY_LAST_VIEWED_TIME,
SORT_FILES_BY_ARCHIVED_TIMESTAMP
SORT_FILES_BY_ARCHIVED_TIMESTAMP,
SORT_FILES_BY_HASH
}
system_sort_type_submetatype_string_lookup = {
@ -358,6 +360,7 @@ system_sort_type_submetatype_string_lookup = {
SORT_FILES_BY_NUM_FRAMES : 'duration',
SORT_FILES_BY_APPROX_BITRATE : 'file',
SORT_FILES_BY_FILESIZE : 'file',
SORT_FILES_BY_HASH : 'file',
SORT_FILES_BY_MIME : 'file',
SORT_FILES_BY_HAS_AUDIO : 'file',
SORT_FILES_BY_RANDOM : None,
@ -388,6 +391,7 @@ sort_type_basic_string_lookup = {
SORT_FILES_BY_ARCHIVED_TIMESTAMP : 'archived time',
SORT_FILES_BY_LAST_VIEWED_TIME : 'last viewed time',
SORT_FILES_BY_RANDOM : 'random',
SORT_FILES_BY_HASH : 'hash',
SORT_FILES_BY_NUM_TAGS : 'number of tags',
SORT_FILES_BY_MEDIA_VIEWS : 'media views',
SORT_FILES_BY_MEDIA_VIEWTIME : 'media viewtime'

View File

@ -631,6 +631,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'frame_locations' ][ 'review_services' ] = ( False, True, None, None, ( -1, -1 ), 'topleft', False, False )
self._dictionary[ 'frame_locations' ][ 'deeply_nested_dialog' ] = ( False, False, None, None, ( -1, -1 ), 'topleft', False, False )
self._dictionary[ 'frame_locations' ][ 'regular_center_dialog' ] = ( False, False, None, None, ( -1, -1 ), 'center', False, False )
self._dictionary[ 'frame_locations' ][ 'file_history_chart' ] = ( True, True, ( 960, 720 ), None, ( -1, -1 ), 'topleft', False, False )
#

View File

@ -3999,13 +3999,15 @@ class DB( HydrusDB.HydrusDB ):
since_deleted = self._STL( self._Execute( 'SELECT original_timestamp FROM {} WHERE original_timestamp IS NOT NULL;'.format( deleted_files_table_name ) ) )
current_timestamps.extend( since_deleted )
all_known_import_timestamps = list( current_timestamps )
current_timestamps.sort()
all_known_import_timestamps.extend( since_deleted )
all_known_import_timestamps.sort()
deleted_timestamps = self._STL( self._Execute( 'SELECT timestamp FROM {} WHERE timestamp IS NOT NULL ORDER BY timestamp ASC;'.format( deleted_files_table_name ) ) )
combined_timestamps_with_delta = [ ( timestamp, 1 ) for timestamp in current_timestamps ]
combined_timestamps_with_delta = [ ( timestamp, 1 ) for timestamp in all_known_import_timestamps ]
combined_timestamps_with_delta.extend( ( ( timestamp, -1 ) for timestamp in deleted_timestamps ) )
combined_timestamps_with_delta.sort()
@ -4028,11 +4030,11 @@ class DB( HydrusDB.HydrusDB ):
for ( timestamp, delta ) in combined_timestamps_with_delta:
if timestamp > step_timestamp + step_gap:
while timestamp > step_timestamp + step_gap:
current_file_history.append( ( step_timestamp, total_current_files ) )
step_timestamp = timestamp
step_timestamp += step_gap
total_current_files += delta
@ -4062,11 +4064,11 @@ class DB( HydrusDB.HydrusDB ):
for deleted_timestamp in deleted_timestamps:
if deleted_timestamp > step_timestamp + step_gap:
while deleted_timestamp > step_timestamp + step_gap:
deleted_file_history.append( ( step_timestamp, total_deleted_files ) )
step_timestamp = deleted_timestamp
step_timestamp += step_gap
total_deleted_files += 1
@ -4080,11 +4082,19 @@ class DB( HydrusDB.HydrusDB ):
# working backwards in time (which reverses increment/decrement):
# an archive increments
# a file import decrements
# note that we archive right before we delete a file, so file deletes shouldn't change anything. all deletes are on archived files, so the increment will already be counted
# note that we archive right before we delete a file, so file deletes shouldn't change anything for inbox count. all deletes are on archived files, so the increment will already be counted
# UPDATE: and now we add archived, which is mostly the same deal but we subtract from current files to start and don't care about file imports since they are always inbox but do care about file deletes
inbox_file_history = []
archive_file_history = []
( total_inbox_files, ) = self._Execute( 'SELECT COUNT( * ) FROM file_inbox;' ).fetchone()
total_current_files = len( current_timestamps )
total_update_files = self.modules_files_storage.GetCurrentFilesCount( self.modules_services.local_update_service_id, HC.CONTENT_STATUS_CURRENT )
total_trash_files = self.modules_files_storage.GetCurrentFilesCount( self.modules_services.trash_service_id, HC.CONTENT_STATUS_CURRENT )
total_archive_files = ( total_current_files - total_update_files - total_trash_files ) - total_inbox_files
# note also that we do not scrub archived time on a file delete, so this upcoming fetch is for all files ever. this is useful, so don't undo it m8
archive_timestamps = self._STL( self._Execute( 'SELECT archived_timestamp FROM archive_timestamps ORDER BY archived_timestamp ASC;' ) )
@ -4093,46 +4103,122 @@ class DB( HydrusDB.HydrusDB ):
first_archive_time = archive_timestamps[0]
combined_timestamps_with_delta = [ ( timestamp, 1 ) for timestamp in archive_timestamps ]
combined_timestamps_with_delta.extend( ( ( timestamp, -1 ) for timestamp in current_timestamps if timestamp >= first_archive_time ) )
combined_timestamps_with_deltas = [ ( timestamp, 1, -1 ) for timestamp in archive_timestamps ]
combined_timestamps_with_deltas.extend( ( ( timestamp, -1, 0 ) for timestamp in all_known_import_timestamps if timestamp >= first_archive_time ) )
combined_timestamps_with_deltas.extend( ( ( timestamp, 0, 1 ) for timestamp in deleted_timestamps if timestamp >= first_archive_time ) )
combined_timestamps_with_delta.sort( reverse = True )
combined_timestamps_with_deltas.sort( reverse = True )
if len( combined_timestamps_with_delta ) > 0:
if len( combined_timestamps_with_deltas ) > 0:
if len( combined_timestamps_with_delta ) < 2:
if len( combined_timestamps_with_deltas ) < 2:
step_gap = 1
else:
# reversed, so first minus last
step_gap = max( ( combined_timestamps_with_delta[0][0] - combined_timestamps_with_delta[-1][0] ) // num_steps, 1 )
step_gap = max( ( combined_timestamps_with_deltas[0][0] - combined_timestamps_with_deltas[-1][0] ) // num_steps, 1 )
step_timestamp = combined_timestamps_with_delta[0][0]
step_timestamp = combined_timestamps_with_deltas[0][0]
for ( archived_timestamp, delta ) in combined_timestamps_with_delta:
for ( archived_timestamp, inbox_delta, archive_delta ) in combined_timestamps_with_deltas:
if archived_timestamp < step_timestamp - step_gap:
while archived_timestamp < step_timestamp - step_gap:
inbox_file_history.append( ( archived_timestamp, total_inbox_files ) )
archive_file_history.append( ( archived_timestamp, total_archive_files ) )
step_timestamp = archived_timestamp
step_timestamp -= step_gap
total_inbox_files += delta
total_inbox_files += inbox_delta
total_archive_files += archive_delta
inbox_file_history.reverse()
archive_file_history.reverse()
file_history[ 'inbox' ] = inbox_file_history
file_history[ 'archive' ] = archive_file_history
return file_history
def _GetFileInfoManagers( self, hash_ids: typing.Collection[ int ], sorted = False ) -> typing.List[ ClientMediaManagers.FileInfoManager ]:
( cached_media_results, missing_hash_ids ) = self._weakref_media_result_cache.GetMediaResultsAndMissing( hash_ids )
file_info_managers = [ media_result.GetFileInfoManager() for media_result in cached_media_results ]
if len( missing_hash_ids ) > 0:
missing_hash_ids_to_hashes = self.modules_hashes_local_cache.GetHashIdsToHashes( hash_ids = missing_hash_ids )
with self._MakeTemporaryIntegerTable( missing_hash_ids, 'hash_id' ) as temp_table_name:
# temp hashes to metadata
hash_ids_to_info = { hash_id : ClientMediaManagers.FileInfoManager( hash_id, missing_hash_ids_to_hashes[ hash_id ], size, mime, width, height, duration, num_frames, has_audio, num_words ) for ( hash_id, size, mime, width, height, duration, num_frames, has_audio, num_words ) in self._Execute( 'SELECT * FROM {} CROSS JOIN files_info USING ( hash_id );'.format( temp_table_name ) ) }
# build it
for hash_id in missing_hash_ids:
if hash_id in hash_ids_to_info:
file_info_manager = hash_ids_to_info[ hash_id ]
else:
hash = missing_hash_ids_to_hashes[ hash_id ]
file_info_manager = ClientMediaManagers.FileInfoManager( hash_id, hash )
file_info_managers.append( file_info_manager )
if sorted:
if len( hash_ids ) > len( file_info_managers ):
hash_ids = HydrusData.DedupeList( hash_ids )
hash_ids_to_file_info_managers = { file_info_manager.hash_id : file_info_manager for file_info_manager in file_info_managers }
file_info_managers = [ hash_ids_to_file_info_managers[ hash_id ] for hash_id in hash_ids if hash_id in hash_ids_to_file_info_managers ]
return file_info_managers
def _GetFileInfoManagersFromHashes( self, hashes: typing.Collection[ bytes ], sorted: bool = False ) -> typing.List[ ClientMediaManagers.FileInfoManager ]:
query_hash_ids = set( self.modules_hashes_local_cache.GetHashIds( hashes ) )
file_info_managers = self._GetFileInfoManagers( query_hash_ids )
if sorted:
if len( hashes ) > len( query_hash_ids ):
hashes = HydrusData.DedupeList( hashes )
hashes_to_file_info_managers = { file_info_manager.hash : file_info_manager for file_info_manager in file_info_managers }
file_info_managers = [ hashes_to_file_info_managers[ hash ] for hash in hashes if hash in hashes_to_file_info_managers ]
return file_info_managers
def _GetFileNotes( self, hash ):
hash_id = self.modules_hashes_local_cache.GetHashId( hash )
@ -10365,6 +10451,8 @@ class DB( HydrusDB.HydrusDB ):
elif action == 'file_duplicate_info': result = self.modules_files_duplicates.DuplicatesGetFileDuplicateInfo( *args, **kwargs )
elif action == 'file_hashes': result = self.modules_hashes.GetFileHashes( *args, **kwargs )
elif action == 'file_history': result = self._GetFileHistory( *args, **kwargs )
elif action == 'file_info_managers': result = self._GetFileInfoManagersFromHashes( *args, **kwargs )
elif action == 'file_info_managers_from_ids': result = self._GetFileInfoManagers( *args, **kwargs )
elif action == 'file_maintenance_get_job': result = self.modules_files_maintenance_queue.GetJob( *args, **kwargs )
elif action == 'file_maintenance_get_job_counts': result = self.modules_files_maintenance_queue.GetJobCounts( *args, **kwargs )
elif action == 'file_query_ids': result = self._GetHashIdsFromQuery( *args, **kwargs )
@ -12199,6 +12287,16 @@ class DB( HydrusDB.HydrusDB ):
did_sort = True
elif sort_data == CC.SORT_FILES_BY_HASH:
hash_ids_to_hashes = self.modules_hashes_local_cache.GetHashIdsToHashes( hash_ids = hash_ids )
hash_ids_to_hex_hashes = { hash_id : hash.hex() for ( hash_id, hash ) in hash_ids_to_hashes }
hash_ids = sorted( hash_ids, key = lambda hash_id: hash_ids_to_hex_hashes[ hash_id ] )
did_sort = True
if query is not None:
@ -14183,6 +14281,29 @@ class DB( HydrusDB.HydrusDB ):
self.pub_initial_message( message )
if version == 478:
try:
# transparent webp regen
table_join = self.modules_files_storage.GetTableJoinLimitedByFileDomain( self.modules_services.combined_local_file_service_id, 'files_info', HC.CONTENT_STATUS_CURRENT )
hash_ids = self._STL( self._Execute( 'SELECT hash_id FROM {} WHERE mime = ?;'.format( table_join ), ( HC.IMAGE_WEBP, ) ) )
self.modules_files_maintenance_queue.AddJobs( hash_ids, ClientFiles.REGENERATE_FILE_DATA_JOB_FORCE_THUMBNAIL )
except Exception as e:
HydrusData.PrintException( e )
message = 'Some webp regen scheduling failed to set! This is not super important, but hydev would be interested in seeing the error that was printed to the log.'
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

@ -1740,7 +1740,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
service = self._controller.services_manager.GetService( service_key )
with QP.BusyCursor(): response = service.Request( HC.GET, 'ip', { 'hash' : hash } )
with ClientGUICommon.BusyCursor(): response = service.Request( HC.GET, 'ip', { 'hash' : hash } )
ip = response[ 'ip' ]
timestamp = response[ 'timestamp' ]
@ -2276,7 +2276,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
HG.client_controller.pub( 'message', job_key )
num_steps = 2000
num_steps = 7680
file_history = HG.client_controller.Read( 'file_history', num_steps )
@ -2289,7 +2289,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
job_key.Delete()
frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( self, 'file history' )
frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( self, 'file history', frame_key = 'file_history_chart' )
panel = ClientGUIScrolledPanelsReview.ReviewFileHistory( frame, file_history )

View File

@ -365,8 +365,8 @@ class SimpleSubPanel( QW.QWidget ):
self._seek_direction = ClientGUICommon.BetterRadioBox( self._seek_panel, choices = choices )
self._seek_duration_s = QP.MakeQSpinBox( self._seek_panel, max=3599, width = 60 )
self._seek_duration_ms = QP.MakeQSpinBox( self._seek_panel, max=999, width = 60 )
self._seek_duration_s = ClientGUICommon.BetterSpinBox( self._seek_panel, max=3599, width = 60 )
self._seek_duration_ms = ClientGUICommon.BetterSpinBox( self._seek_panel, max=999, width = 60 )
#

View File

@ -76,79 +76,140 @@ try:
QCh.QtCharts.QChartView.__init__( self, parent )
self._file_history = file_history
self._show_deleted = True
# this lad takes ms timestamp, not s, so * 1000
# note you have to give this floats for the ms or it throws a type problem of big number to C long
current_files_series = QCh.QtCharts.QLineSeries()
self._current_files_series = QCh.QtCharts.QLineSeries()
current_files_series.setName( 'files in storage' )
self._current_files_series.setName( 'files in storage' )
max_num_files = 0
self._max_num_files_current = 0
for ( timestamp, num_files ) in file_history[ 'current' ]:
for ( timestamp, num_files ) in self._file_history[ 'current' ]:
current_files_series.append( timestamp * 1000.0, num_files )
self._current_files_series.append( timestamp * 1000.0, num_files )
max_num_files = max( max_num_files, num_files )
self._max_num_files_current = max( self._max_num_files_current, num_files )
deleted_files_series = QCh.QtCharts.QLineSeries()
#
deleted_files_series.setName( 'deleted' )
self._deleted_files_series = QCh.QtCharts.QLineSeries()
for ( timestamp, num_files ) in file_history[ 'deleted' ]:
self._deleted_files_series.setName( 'deleted' )
self._max_num_files_deleted = 0
for ( timestamp, num_files ) in self._file_history[ 'deleted' ]:
deleted_files_series.append( timestamp * 1000.0, num_files )
self._deleted_files_series.append( timestamp * 1000.0, num_files )
max_num_files = max( max_num_files, num_files )
self._max_num_files_deleted = max( self._max_num_files_deleted, num_files )
inbox_files_series = QCh.QtCharts.QLineSeries()
#
inbox_files_series.setName( 'inbox' )
self._inbox_files_series = QCh.QtCharts.QLineSeries()
for ( timestamp, num_files ) in file_history[ 'inbox' ]:
self._inbox_files_series.setName( 'inbox' )
self._max_num_files_inbox = 0
for ( timestamp, num_files ) in self._file_history[ 'inbox' ]:
inbox_files_series.append( timestamp * 1000.0, num_files )
self._inbox_files_series.append( timestamp * 1000.0, num_files )
max_num_files = max( max_num_files, num_files )
self._max_num_files_inbox = max( self._max_num_files_inbox, num_files )
#
self._archive_files_series = QCh.QtCharts.QLineSeries()
self._archive_files_series.setName( 'archive' )
self._max_num_files_archive = 0
for ( timestamp, num_files ) in self._file_history[ 'archive' ]:
self._archive_files_series.append( timestamp * 1000.0, num_files )
self._max_num_files_archive = max( self._max_num_files_archive, num_files )
# takes ms since epoch
x_datetime_axis = QCh.QtCharts.QDateTimeAxis()
self._x_datetime_axis = QCh.QtCharts.QDateTimeAxis()
x_datetime_axis.setTickCount( 25 )
x_datetime_axis.setLabelsAngle( 90 )
self._x_datetime_axis.setTickCount( 25 )
self._x_datetime_axis.setLabelsAngle( 90 )
x_datetime_axis.setFormat( 'yyyy-MM-dd' )
self._x_datetime_axis.setFormat( 'yyyy-MM-dd' )
y_value_axis = QCh.QtCharts.QValueAxis()
self._y_value_axis = QCh.QtCharts.QValueAxis()
y_value_axis.setLabelFormat( '%\'i' )
self._y_value_axis.setLabelFormat( '%\'i' )
chart = QCh.QtCharts.QChart()
self._chart = QCh.QtCharts.QChart()
chart.addSeries( current_files_series )
chart.addSeries( inbox_files_series )
chart.addSeries( deleted_files_series )
self._chart.addSeries( self._current_files_series )
self._chart.addSeries( self._inbox_files_series )
self._chart.addSeries( self._archive_files_series )
self._chart.addSeries( self._deleted_files_series )
chart.addAxis( x_datetime_axis, QC.Qt.AlignBottom )
chart.addAxis( y_value_axis, QC.Qt.AlignLeft )
self._chart.addAxis( self._x_datetime_axis, QC.Qt.AlignBottom )
self._chart.addAxis( self._y_value_axis, QC.Qt.AlignLeft )
current_files_series.attachAxis( x_datetime_axis )
current_files_series.attachAxis( y_value_axis )
self._current_files_series.attachAxis( self._x_datetime_axis )
self._current_files_series.attachAxis( self._y_value_axis )
deleted_files_series.attachAxis( x_datetime_axis )
deleted_files_series.attachAxis( y_value_axis )
self._deleted_files_series.attachAxis( self._x_datetime_axis )
self._deleted_files_series.attachAxis( self._y_value_axis )
inbox_files_series.attachAxis( x_datetime_axis )
inbox_files_series.attachAxis( y_value_axis )
self._inbox_files_series.attachAxis( self._x_datetime_axis )
self._inbox_files_series.attachAxis( self._y_value_axis )
y_value_axis.setRange( 0, max_num_files )
self._archive_files_series.attachAxis( self._x_datetime_axis )
self._archive_files_series.attachAxis( self._y_value_axis )
y_value_axis.applyNiceNumbers()
self._CalculateYRange()
self.setChart( chart )
self.setChart( self._chart )
def _CalculateYRange( self ):
max_num_files = max( self._max_num_files_current, self._max_num_files_inbox, self._max_num_files_archive )
if self._show_deleted:
max_num_files = max( self._max_num_files_deleted, max_num_files )
self._y_value_axis.setRange( 0, max_num_files )
self._y_value_axis.applyNiceNumbers()
def FlipDeletedVisible( self ):
self._show_deleted = not self._show_deleted
if self._show_deleted:
self._chart.addSeries( self._deleted_files_series )
self._deleted_files_series.attachAxis( self._x_datetime_axis )
self._deleted_files_series.attachAxis( self._y_value_axis )
else:
self._chart.removeSeries( self._deleted_files_series )
self._CalculateYRange()

View File

@ -101,7 +101,12 @@ class DialogChooseNewServiceMethod( Dialog ):
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._register, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, QP.MakeQLabelWithAlignment('-or-', self, QC.Qt.AlignHCenter | QC.Qt.AlignVCenter ), CC.FLAGS_EXPAND_PERPENDICULAR )
st = ClientGUICommon.BetterStaticText( self, '-or-' )
st.setAlignment( QC.Qt.AlignCenter )
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._setup, CC.FLAGS_EXPAND_PERPENDICULAR )
self.setLayout( vbox )
@ -135,7 +140,7 @@ class DialogGenerateNewAccounts( Dialog ):
self._service_key = service_key
self._num = QP.MakeQSpinBox( self, min=1, max=10000, width = 80 )
self._num = ClientGUICommon.BetterSpinBox( self, min=1, max=10000, width = 80 )
self._account_types = ClientGUICommon.BetterChoice( self )
@ -586,15 +591,15 @@ class DialogInputUPnPMapping( Dialog ):
Dialog.__init__( self, parent, 'configure upnp mapping' )
self._external_port = QP.MakeQSpinBox( self, min=0, max=65535 )
self._external_port = ClientGUICommon.BetterSpinBox( self, min=0, max=65535 )
self._protocol_type = ClientGUICommon.BetterChoice( self )
self._protocol_type.addItem( 'TCP', 'TCP' )
self._protocol_type.addItem( 'UDP', 'UDP' )
self._internal_port = QP.MakeQSpinBox( self, min=0, max=65535 )
self._internal_port = ClientGUICommon.BetterSpinBox( self, min=0, max=65535 )
self._description = QW.QLineEdit( self )
self._duration = QP.MakeQSpinBox( self, min=0, max=86400 )
self._duration = ClientGUICommon.BetterSpinBox( self, min=0, max=86400 )
self._ok = ClientGUICommon.BetterButton( self, 'OK', self.done, QW.QDialog.Accepted )
self._ok.setObjectName( 'HydrusAccept' )

View File

@ -1091,7 +1091,7 @@ class EditURLClassPanel( ClientGUIScrolledPanels.EditPanel ):
self._next_gallery_page_choice = ClientGUICommon.BetterChoice( self._next_gallery_page_panel )
self._next_gallery_page_delta = QP.MakeQSpinBox( self._next_gallery_page_panel, min=1, max=65536 )
self._next_gallery_page_delta = ClientGUICommon.BetterSpinBox( self._next_gallery_page_panel, min=1, max=65536 )
self._next_gallery_page_url = QW.QLineEdit( self._next_gallery_page_panel )
self._next_gallery_page_url.setReadOnly( True )

View File

@ -920,7 +920,7 @@ class ReviewExportFilesPanel( ClientGUIScrolledPanels.ReviewPanel ):
for service_key in media.GetLocationsManager().GetCurrent():
service_keys_to_hashes[ service_key ].add( hash )
service_keys_to_hashes[ service_key ].add( media.GetHash() )

View File

@ -267,11 +267,11 @@ class FilenameTaggingOptionsPanel( QW.QWidget ):
self._num_panel = ClientGUICommon.StaticBox( self, '#' )
self._num_base = QP.MakeQSpinBox( self._num_panel, min=-10000000, max=10000000, width = 60 )
self._num_base = ClientGUICommon.BetterSpinBox( self._num_panel, min=-10000000, max=10000000, width = 60 )
self._num_base.setValue( 1 )
self._num_base.valueChanged.connect( self.tagsChanged )
self._num_step = QP.MakeQSpinBox( self._num_panel, min=-1000000, max=1000000, width = 60 )
self._num_step = ClientGUICommon.BetterSpinBox( self._num_panel, min=-1000000, max=1000000, width = 60 )
self._num_step.setValue( 1 )
self._num_step.valueChanged.connect( self.tagsChanged )
@ -1112,7 +1112,7 @@ class EditImportFolderPanel( ClientGUIScrolledPanels.EditPanel ):
rows = []
rows.append( ( 'mimes to import: ', self._mimes ) )
rows.append( ( 'file types to import: ', self._mimes ) )
mimes_gridbox = ClientGUICommon.WrapInGrid( self._file_box, rows, expand_text = True )

View File

@ -1021,7 +1021,7 @@ class EditHTMLTagRulePanel( ClientGUIScrolledPanels.EditPanel ):
self._tag_index = ClientGUICommon.NoneableSpinCtrl( self, 'index to fetch', none_phrase = 'get all', min = -65536, max = 65535 )
self._tag_index.setToolTip( 'You can make this negative to do negative indexing, i.e. "Select the second from last item".' )
self._tag_depth = QP.MakeQSpinBox( self, min=1, max=255 )
self._tag_depth = ClientGUICommon.BetterSpinBox( self, min=1, max=255 )
self._should_test_tag_string = QW.QCheckBox( self )
@ -1452,7 +1452,7 @@ class EditJSONParsingRulePanel( ClientGUIScrolledPanels.EditPanel ):
self._string_match = ClientGUIStringPanels.EditStringMatchPanel( self, string_match )
self._index = QP.MakeQSpinBox( self, min=-65536, max=65535 )
self._index = ClientGUICommon.BetterSpinBox( self, min=-65536, max=65535 )
self._index.setToolTip( 'You can make this negative to do negative indexing, i.e. "Select the second from last item".' )
#
@ -1825,7 +1825,7 @@ class EditContentParserPanel( ClientGUIScrolledPanels.EditPanel ):
self._url_type.addItem( 'GALLERY parsers only: next gallery page (not queued if no post/file urls found)', HC.URL_TYPE_NEXT )
self._url_type.addItem( 'GALLERY parsers only: sub-gallery page (is queued even if no post/file urls found--be careful, only use if you know you need it)', HC.URL_TYPE_SUB_GALLERY )
self._file_priority = QP.MakeQSpinBox( self._urls_panel, min=0, max=100 )
self._file_priority = ClientGUICommon.BetterSpinBox( self._urls_panel, min=0, max=100 )
self._file_priority.setValue( 50 )
self._mappings_panel = QW.QWidget( self._content_panel )
@ -1856,7 +1856,7 @@ class EditContentParserPanel( ClientGUIScrolledPanels.EditPanel ):
self._title_panel = QW.QWidget( self._content_panel )
self._title_priority = QP.MakeQSpinBox( self._title_panel, min=0, max=100 )
self._title_priority = ClientGUICommon.BetterSpinBox( self._title_panel, min=0, max=100 )
self._title_priority.setValue( 50 )
self._veto_panel = QW.QWidget( self._content_panel )

View File

@ -22,9 +22,18 @@ class QuestionCommitInterstitialFilteringPanel( ClientGUIScrolledPanels.Resizing
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, QP.MakeQLabelWithAlignment( label, self, QC.Qt.AlignVCenter | QC.Qt.AlignHCenter ), CC.FLAGS_EXPAND_PERPENDICULAR )
st = ClientGUICommon.BetterStaticText( self, label )
st.setAlignment( QC.Qt.AlignCenter )
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._commit, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
QP.AddToLayout( vbox, QP.MakeQLabelWithAlignment( '-or-', self, QC.Qt.AlignVCenter | QC.Qt.AlignHCenter ), CC.FLAGS_EXPAND_PERPENDICULAR )
st = ClientGUICommon.BetterStaticText( self, '-or-' )
st.setAlignment( QC.Qt.AlignCenter )
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._back, CC.FLAGS_EXPAND_PERPENDICULAR )
self.widget().setLayout( vbox )
@ -59,9 +68,18 @@ class QuestionFinishFilteringPanel( ClientGUIScrolledPanels.ResizingScrolledPane
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, QP.MakeQLabelWithAlignment( label, self, QC.Qt.AlignVCenter | QC.Qt.AlignHCenter ), CC.FLAGS_EXPAND_PERPENDICULAR )
st = ClientGUICommon.BetterStaticText( self, label )
st.setAlignment( QC.Qt.AlignCenter )
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
QP.AddToLayout( vbox, QP.MakeQLabelWithAlignment( '-or-', self, QC.Qt.AlignVCenter | QC.Qt.AlignHCenter ), CC.FLAGS_EXPAND_PERPENDICULAR )
st = ClientGUICommon.BetterStaticText( self, '-or-' )
st.setAlignment( QC.Qt.AlignCenter )
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._back, CC.FLAGS_EXPAND_PERPENDICULAR )
self.widget().setLayout( vbox )

View File

@ -531,6 +531,8 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
self.widget().setLayout( vbox )
QP.CallAfter( self._SetFocus )
def _FilterForDeleteLock( self, media, suggested_file_service_key: bytes ):
@ -723,6 +725,18 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
def _SetFocus( self ):
if self._action_radio.isVisible():
self._action_radio.setFocus( QC.Qt.OtherFocusReason )
elif self._reason_panel.isVisible() and self._reason_panel.isEnabled():
self._reason_radio.setFocus( QC.Qt.OtherFocusReason )
def _UpdateControls( self ):
( file_service_key, hashes, description ) = self._action_radio.GetValue()

View File

@ -303,19 +303,19 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
max_network_jobs_per_domain_max = 5
self._network_timeout = QP.MakeQSpinBox( general, min = network_timeout_min, max = network_timeout_max )
self._network_timeout = ClientGUICommon.BetterSpinBox( general, min = network_timeout_min, max = network_timeout_max )
self._network_timeout.setToolTip( 'If a network connection cannot be made in this duration or, if once started, it experiences uninterrupted inactivity for six times this duration, it will be abandoned.' )
self._connection_error_wait_time = QP.MakeQSpinBox( general, min = error_wait_time_min, max = error_wait_time_max )
self._connection_error_wait_time = ClientGUICommon.BetterSpinBox( general, min = error_wait_time_min, max = error_wait_time_max )
self._connection_error_wait_time.setToolTip( 'If a network connection times out as above, it will wait increasing multiples of this base time before retrying.' )
self._serverside_bandwidth_wait_time = QP.MakeQSpinBox( general, min = error_wait_time_min, max = error_wait_time_max )
self._serverside_bandwidth_wait_time = ClientGUICommon.BetterSpinBox( general, min = error_wait_time_min, max = error_wait_time_max )
self._serverside_bandwidth_wait_time.setToolTip( 'If a server returns a failure status code indicating it is short on bandwidth, the network job will wait increasing multiples of this base time before retrying.' )
self._domain_network_infrastructure_error_velocity = ClientGUITime.VelocityCtrl( general, 0, 100, 30, hours = True, minutes = True, seconds = True, per_phrase = 'within', unit = 'errors' )
self._max_network_jobs = QP.MakeQSpinBox( general, min = 1, max = max_network_jobs_max )
self._max_network_jobs_per_domain = QP.MakeQSpinBox( general, min = 1, max = max_network_jobs_per_domain_max )
self._max_network_jobs = ClientGUICommon.BetterSpinBox( general, min = 1, max = max_network_jobs_max )
self._max_network_jobs_per_domain = ClientGUICommon.BetterSpinBox( general, min = 1, max = max_network_jobs_per_domain_max )
#
@ -453,7 +453,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._default_gug = ClientGUIImport.GUGKeyAndNameSelector( gallery_downloader, gug_key_and_name )
self._gallery_page_wait_period_pages = QP.MakeQSpinBox( gallery_downloader, min=1, max=120 )
self._gallery_page_wait_period_pages = ClientGUICommon.BetterSpinBox( gallery_downloader, min=1, max=120 )
self._gallery_file_limit = ClientGUICommon.NoneableSpinCtrl( gallery_downloader, none_phrase = 'no limit', min = 1, max = 1000000 )
self._highlight_new_query = QW.QCheckBox( gallery_downloader )
@ -462,8 +462,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
subscriptions = ClientGUICommon.StaticBox( self, 'subscriptions' )
self._gallery_page_wait_period_subscriptions = QP.MakeQSpinBox( subscriptions, min=1, max=30 )
self._max_simultaneous_subscriptions = QP.MakeQSpinBox( subscriptions, min=1, max=100 )
self._gallery_page_wait_period_subscriptions = ClientGUICommon.BetterSpinBox( subscriptions, min=1, max=30 )
self._max_simultaneous_subscriptions = ClientGUICommon.BetterSpinBox( subscriptions, min=1, max=100 )
self._subscription_file_error_cancel_threshold = ClientGUICommon.NoneableSpinCtrl( subscriptions, min = 1, max = 1000000, unit = 'errors' )
self._subscription_file_error_cancel_threshold.setToolTip( 'This is a simple patch and will be replaced with a better "retry network errors later" system at some point, but is useful to increase if you have subs to unreliable websites.' )
@ -479,7 +479,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
watchers = ClientGUICommon.StaticBox( self, 'watchers' )
self._watcher_page_wait_period = QP.MakeQSpinBox( watchers, min=1, max=120 )
self._watcher_page_wait_period = ClientGUICommon.BetterSpinBox( watchers, min=1, max=120 )
self._highlight_new_watcher = QW.QCheckBox( watchers )
checker_options = self._new_options.GetDefaultWatcherCheckerOptions()
@ -659,19 +659,19 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
weights_panel = ClientGUICommon.StaticBox( self, 'duplicate filter comparison score weights' )
self._duplicate_comparison_score_higher_jpeg_quality = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_much_higher_jpeg_quality = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_higher_filesize = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_much_higher_filesize = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_higher_resolution = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_much_higher_resolution = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_more_tags = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_older = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_nicer_ratio = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_higher_jpeg_quality = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_much_higher_jpeg_quality = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_higher_filesize = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_much_higher_filesize = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_higher_resolution = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_much_higher_resolution = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_more_tags = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_older = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_nicer_ratio = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_nicer_ratio.setToolTip( 'For instance, 16:9 vs 640:357.')
self._duplicate_filter_max_batch_size = QP.MakeQSpinBox( self, min = 10, max = 1024 )
self._duplicate_filter_max_batch_size = ClientGUICommon.BetterSpinBox( self, min = 10, max = 1024 )
#
@ -1200,7 +1200,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
tt = 'In many places across the program (typically import status lists), the client will state a timestamp as "5 days ago". If you would prefer a standard ISO string, like "2018-03-01 12:40:23", check this.'
self._always_show_iso_time.setToolTip( tt )
self._human_bytes_sig_figs = QP.MakeQSpinBox( self._misc_panel, min = 1, max = 6 )
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.')
self._discord_dnd_fix = QW.QCheckBox( self._misc_panel )
@ -1384,13 +1384,13 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._default_gui_session = QW.QComboBox( self._sessions_panel )
self._last_session_save_period_minutes = QP.MakeQSpinBox( self._sessions_panel, min = 1, max = 1440 )
self._last_session_save_period_minutes = ClientGUICommon.BetterSpinBox( self._sessions_panel, min = 1, max = 1440 )
self._only_save_last_session_during_idle = QW.QCheckBox( self._sessions_panel )
self._only_save_last_session_during_idle.setToolTip( 'This is useful if you usually have a very large session (200,000+ files/import items open) and a client that is always on.' )
self._number_of_gui_session_backups = QP.MakeQSpinBox( self._sessions_panel, min = 1, max = 32 )
self._number_of_gui_session_backups = ClientGUICommon.BetterSpinBox( self._sessions_panel, min = 1, max = 32 )
self._number_of_gui_session_backups.setToolTip( 'The client keeps multiple rolling backups of your gui sessions. If you have very large sessions, you might like to reduce this number.' )
@ -1416,7 +1416,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._notebook_tab_alignment.addItem( CC.directions_alignment_string_lookup[ value ], value )
self._total_pages_warning = QP.MakeQSpinBox( self._pages_panel, min=5, max=65565 )
self._total_pages_warning = ClientGUICommon.BetterSpinBox( self._pages_panel, min=5, max=65565 )
tt = 'If you have a gigantic session, or you have very page-spammy subscriptions, you can try boosting this, but be warned it may lead to resource limit crashes. The best solution to a large session is to make it smaller!'
@ -1433,7 +1433,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._page_names_panel = ClientGUICommon.StaticBox( self._pages_panel, 'page tab names' )
self._max_page_name_chars = QP.MakeQSpinBox( self._page_names_panel, min=1, max=256 )
self._max_page_name_chars = ClientGUICommon.BetterSpinBox( self._page_names_panel, min=1, max=256 )
self._elide_page_tab_names = QW.QCheckBox( self._page_names_panel )
self._page_file_count_display = ClientGUICommon.BetterChoice( self._page_names_panel )
@ -1686,7 +1686,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._idle_period = ClientGUICommon.NoneableSpinCtrl( self._idle_panel, '', min = 1, max = 1000, multiplier = 60, unit = 'minutes', none_phrase = 'ignore normal browsing' )
self._idle_mouse_period = ClientGUICommon.NoneableSpinCtrl( self._idle_panel, '', min = 1, max = 1000, multiplier = 60, unit = 'minutes', none_phrase = 'ignore mouse movements' )
self._idle_mode_client_api_timeout = ClientGUICommon.NoneableSpinCtrl( self._idle_panel, '', min = 1, max = 1000, multiplier = 60, unit = 'minutes', none_phrase = 'ignore client api' )
self._system_busy_cpu_percent = QP.MakeQSpinBox( self._idle_panel, min = 5, max = 99 )
self._system_busy_cpu_percent = ClientGUICommon.BetterSpinBox( self._idle_panel, min = 5, max = 99 )
self._system_busy_cpu_count = ClientGUICommon.NoneableSpinCtrl( self._idle_panel, min = 1, max = 64, unit = 'cores', none_phrase = 'ignore cpu usage' )
#
@ -1700,7 +1700,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._idle_shutdown.currentIndexChanged.connect( self._EnableDisableIdleShutdown )
self._idle_shutdown_max_minutes = QP.MakeQSpinBox( self._shutdown_panel, min=1, max=1440 )
self._idle_shutdown_max_minutes = ClientGUICommon.BetterSpinBox( self._shutdown_panel, min=1, max=1440 )
self._shutdown_work_period = ClientGUITime.TimeDeltaButton( self._shutdown_panel, min = 60, days = True, hours = True, minutes = True )
#
@ -1921,7 +1921,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._new_options = HG.client_controller.new_options
self._animation_start_position = QP.MakeQSpinBox( self, min=0, max=100 )
self._animation_start_position = ClientGUICommon.BetterSpinBox( self, min=0, max=100 )
self._disable_cv_for_gifs = QW.QCheckBox( self )
self._disable_cv_for_gifs.setToolTip( 'OpenCV is good at rendering gifs, but if you have problems with it and your graphics card, check this and the less reliable and slower PIL will be used instead. EDIT: OpenCV is much better these days--this is mostly not needed.' )
@ -1958,8 +1958,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._mpv_conf_path = QP.FilePickerCtrl( self, starting_directory = os.path.join( HC.STATIC_DIR, 'mpv-conf' ) )
self._animated_scanbar_height = QP.MakeQSpinBox( self, min=1, max=255 )
self._animated_scanbar_nub_width = QP.MakeQSpinBox( self, min=1, max=63 )
self._animated_scanbar_height = ClientGUICommon.BetterSpinBox( self, min=1, max=255 )
self._animated_scanbar_nub_width = ClientGUICommon.BetterSpinBox( self, min=1, max=63 )
self._media_viewer_panel = ClientGUICommon.StaticBox( self, 'media viewer filetype handling' )
@ -2320,7 +2320,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._popup_panel = ClientGUICommon.StaticBox( self, 'popup window toaster' )
self._popup_message_character_width = QP.MakeQSpinBox( self._popup_panel, min = 16, max = 256 )
self._popup_message_character_width = ClientGUICommon.BetterSpinBox( self._popup_panel, min = 16, max = 256 )
self._popup_message_force_min_width = QW.QCheckBox( self._popup_panel )
@ -2442,11 +2442,11 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
tt = 'The autocomplete dropdown can either \'float\' on top of dialogs like _manage tags_, or if that does not work well for you (it can sometimes annoyingly overlap the ok/cancel buttons), it can embed into the parent dialog panel.'
self._autocomplete_float_frames.setToolTip( tt )
self._ac_read_list_height_num_chars = QP.MakeQSpinBox( self._autocomplete_panel, min = 1, max = 128 )
self._ac_read_list_height_num_chars = ClientGUICommon.BetterSpinBox( self._autocomplete_panel, min = 1, max = 128 )
tt = 'Read autocompletes are those in search pages, where you are looking through existing tags to find your files.'
self._ac_read_list_height_num_chars.setToolTip( tt )
self._ac_write_list_height_num_chars = QP.MakeQSpinBox( self._autocomplete_panel, min = 1, max = 128 )
self._ac_write_list_height_num_chars = ClientGUICommon.BetterSpinBox( self._autocomplete_panel, min = 1, max = 128 )
tt = 'Write autocompletes are those in most dialogs, where you are adding new tags to files.'
self._ac_write_list_height_num_chars.setToolTip( tt )
@ -2664,7 +2664,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
thumbnail_cache_panel = ClientGUICommon.StaticBox( self, 'thumbnail cache' )
self._thumbnail_cache_size = QP.MakeQSpinBox( thumbnail_cache_panel, min=5, max=3000 )
self._thumbnail_cache_size = ClientGUICommon.BetterSpinBox( thumbnail_cache_panel, min=5, max=3000 )
self._thumbnail_cache_size.valueChanged.connect( self.EventThumbnailsUpdate )
self._estimated_number_thumbnails = QW.QLabel( '', thumbnail_cache_panel )
@ -2674,7 +2674,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
image_cache_panel = ClientGUICommon.StaticBox( self, 'image cache' )
self._fullscreen_cache_size = QP.MakeQSpinBox( image_cache_panel, min=25, max=8192 )
self._fullscreen_cache_size = ClientGUICommon.BetterSpinBox( image_cache_panel, min=25, max=8192 )
self._fullscreen_cache_size.valueChanged.connect( self.EventImageCacheUpdate )
self._estimated_number_fullscreens = QW.QLabel( '', image_cache_panel )
@ -2682,18 +2682,18 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._image_cache_timeout = ClientGUITime.TimeDeltaButton( image_cache_panel, min = 300, days = True, hours = True, minutes = True )
self._image_cache_timeout.setToolTip( 'The amount of time after which a rendered image in the cache will naturally be removed, if it is not shunted out due to a new member exceeding the size limit.' )
self._media_viewer_prefetch_delay_base_ms = QP.MakeQSpinBox( image_cache_panel, min = 0, max = 2000 )
self._media_viewer_prefetch_delay_base_ms = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 0, max = 2000 )
tt = 'How long to wait, after the current image is rendered, to start rendering neighbours. Does not matter so much any more, but if you have CPU lag, you can try boosting it a bit.'
self._media_viewer_prefetch_delay_base_ms.setToolTip( tt )
self._media_viewer_prefetch_num_previous = QP.MakeQSpinBox( image_cache_panel, min = 0, max = 5 )
self._media_viewer_prefetch_num_next = QP.MakeQSpinBox( image_cache_panel, min = 0, max = 5 )
self._media_viewer_prefetch_num_previous = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 0, max = 5 )
self._media_viewer_prefetch_num_next = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 0, max = 5 )
self._image_cache_storage_limit_percentage = QP.MakeQSpinBox( image_cache_panel, min = 20, max = 50 )
self._image_cache_storage_limit_percentage = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 20, max = 50 )
self._image_cache_storage_limit_percentage_st = ClientGUICommon.BetterStaticText( image_cache_panel, label = '' )
self._image_cache_prefetch_limit_percentage = QP.MakeQSpinBox( image_cache_panel, min = 5, max = 20 )
self._image_cache_prefetch_limit_percentage = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 5, max = 20 )
self._image_cache_prefetch_limit_percentage_st = ClientGUICommon.BetterStaticText( image_cache_panel, label = '' )
@ -2707,14 +2707,14 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._image_tile_cache_timeout = ClientGUITime.TimeDeltaButton( image_tile_cache_panel, min = 300, hours = True, minutes = True )
self._image_tile_cache_timeout.setToolTip( 'The amount of time after which a rendered image tile in the cache will naturally be removed, if it is not shunted out due to a new member exceeding the size limit.' )
self._ideal_tile_dimension = QP.MakeQSpinBox( image_tile_cache_panel, min = 256, max = 4096 )
self._ideal_tile_dimension = ClientGUICommon.BetterSpinBox( image_tile_cache_panel, min = 256, max = 4096 )
self._ideal_tile_dimension.setToolTip( 'This is the square size the system will aim for. Smaller tiles are more memory efficient but prone to warping and other artifacts. Extreme values may waste CPU.' )
#
buffer_panel = ClientGUICommon.StaticBox( self, 'video buffer' )
self._video_buffer_size_mb = QP.MakeQSpinBox( buffer_panel, min=48, max=16*1024 )
self._video_buffer_size_mb = ClientGUICommon.BetterSpinBox( buffer_panel, min=48, max= 16 * 1024 )
self._video_buffer_size_mb.valueChanged.connect( self.EventVideoBufferUpdate )
self._estimated_number_video_frames = QW.QLabel( '', buffer_panel )
@ -3138,7 +3138,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
sleep_panel = ClientGUICommon.StaticBox( self, 'system sleep' )
self._wake_delay_period = QP.MakeQSpinBox( sleep_panel, min = 0, max = 60 )
self._wake_delay_period = ClientGUICommon.BetterSpinBox( sleep_panel, min = 0, max = 60 )
tt = 'It sometimes takes a few seconds for your network adapter to reconnect after a wake. This adds a grace period after a detected wake-from-sleep to allow your OS to sort that out before Hydrus starts making requests.'
@ -3554,7 +3554,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
suggested_tags_panel = ClientGUICommon.StaticBox( self, 'suggested tags' )
self._suggested_tags_width = QP.MakeQSpinBox( suggested_tags_panel, min=20, max=65535 )
self._suggested_tags_width = ClientGUICommon.BetterSpinBox( suggested_tags_panel, min=20, max=65535 )
self._suggested_tags_layout = ClientGUICommon.BetterChoice( suggested_tags_panel )
@ -3594,9 +3594,9 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._show_related_tags = QW.QCheckBox( suggested_tags_related_panel )
self._related_tags_search_1_duration_ms = QP.MakeQSpinBox( suggested_tags_related_panel, min=50, max=60000 )
self._related_tags_search_2_duration_ms = QP.MakeQSpinBox( suggested_tags_related_panel, min=50, max=60000 )
self._related_tags_search_3_duration_ms = QP.MakeQSpinBox( suggested_tags_related_panel, min=50, max=60000 )
self._related_tags_search_1_duration_ms = ClientGUICommon.BetterSpinBox( suggested_tags_related_panel, min=50, max=60000 )
self._related_tags_search_2_duration_ms = ClientGUICommon.BetterSpinBox( suggested_tags_related_panel, min=50, max=60000 )
self._related_tags_search_3_duration_ms = ClientGUICommon.BetterSpinBox( suggested_tags_related_panel, min=50, max=60000 )
#
@ -3793,15 +3793,15 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._new_options = new_options
self._thumbnail_width = QP.MakeQSpinBox( self, min=20, max=2048 )
self._thumbnail_height = QP.MakeQSpinBox( self, min=20, max=2048 )
self._thumbnail_width = ClientGUICommon.BetterSpinBox( self, min=20, max=2048 )
self._thumbnail_height = ClientGUICommon.BetterSpinBox( self, min=20, max=2048 )
self._thumbnail_border = QP.MakeQSpinBox( self, min=0, max=20 )
self._thumbnail_margin = QP.MakeQSpinBox( self, min=0, max=20 )
self._thumbnail_border = ClientGUICommon.BetterSpinBox( self, min=0, max=20 )
self._thumbnail_margin = ClientGUICommon.BetterSpinBox( self, min=0, max=20 )
self._thumbnail_scale_type = ClientGUICommon.BetterChoice( self )
self._video_thumbnail_percentage_in = QP.MakeQSpinBox( self, min=0, max=100 )
self._video_thumbnail_percentage_in = ClientGUICommon.BetterSpinBox( self, min=0, max=100 )
for t in ( HydrusImageHandling.THUMBNAIL_SCALE_DOWN_ONLY, HydrusImageHandling.THUMBNAIL_SCALE_TO_FIT, HydrusImageHandling.THUMBNAIL_SCALE_TO_FILL ):
@ -3810,7 +3810,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._thumbnail_scroll_rate = QW.QLineEdit( self )
self._thumbnail_visibility_scroll_percent = QP.MakeQSpinBox( self, min=1, max=99 )
self._thumbnail_visibility_scroll_percent = ClientGUICommon.BetterSpinBox( self, min=1, max=99 )
self._thumbnail_visibility_scroll_percent.setToolTip( 'Lower numbers will cause fewer scrolls, higher numbers more.' )
self._media_background_bmp_path = QP.FilePickerCtrl( self )

View File

@ -2296,11 +2296,11 @@ class ReviewFileHistory( ClientGUIScrolledPanels.ReviewPanel ):
file_history_chart = ClientGUICharts.FileHistory( self, file_history )
file_history_chart.setMinimumSize( 640, 480 )
file_history_chart.setMinimumSize( 720, 480 )
vbox = QP.VBoxLayout()
label = 'Please note that delete and inbox time tracking are new so you may not have full data for them.'
label = 'Please note that delete and inbox time tracking are new so you may not have full data for them. Also, files in storage includes trash and any repository updates, so inbox and archive may not add up to 100% of it.'
st = ClientGUICommon.BetterStaticText( self, label = label )
@ -2309,6 +2309,14 @@ class ReviewFileHistory( ClientGUIScrolledPanels.ReviewPanel ):
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
flip_deleted = QW.QCheckBox( 'show deleted', self )
flip_deleted.setChecked( True )
flip_deleted.clicked.connect( file_history_chart.FlipDeletedVisible )
QP.AddToLayout( vbox, flip_deleted, CC.FLAGS_CENTER )
QP.AddToLayout( vbox, file_history_chart, CC.FLAGS_EXPAND_BOTH_WAYS )
self.widget().setLayout( vbox )

View File

@ -34,7 +34,7 @@ class PNGExportPanel( ClientGUIScrolledPanels.ReviewPanel ):
self._text = QW.QLineEdit( self )
self._width = QP.MakeQSpinBox( self, min=100, max=4096 )
self._width = ClientGUICommon.BetterSpinBox( self, min=100, max=4096 )
self._export = ClientGUICommon.BetterButton( self, 'export', self.Export )
@ -190,7 +190,7 @@ class PNGsExportPanel( ClientGUIScrolledPanels.ReviewPanel ):
self._directory_picker.setMinimumWidth( dp_width )
self._width = QP.MakeQSpinBox( self, min=100, max=4096 )
self._width = ClientGUICommon.BetterSpinBox( self, min=100, max=4096 )
self._export = ClientGUICommon.BetterButton( self, 'export', self.Export )

View File

@ -685,14 +685,14 @@ class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ):
self._data_text = QW.QLineEdit( self._control_panel )
self._data_number = QP.MakeQSpinBox( self._control_panel, min=0, max=65535 )
self._data_number = ClientGUICommon.BetterSpinBox( self._control_panel, min=0, max=65535 )
self._data_encoding = ClientGUICommon.BetterChoice( self._control_panel )
self._data_decoding = ClientGUICommon.BetterChoice( self._control_panel )
self._data_regex_repl = QW.QLineEdit( self._control_panel )
self._data_date_link = ClientGUICommon.BetterHyperLink( self._control_panel, 'link to date info', 'https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior' )
self._data_timezone_decode = ClientGUICommon.BetterChoice( self._control_panel )
self._data_timezone_encode = ClientGUICommon.BetterChoice( self._control_panel )
self._data_timezone_offset = QP.MakeQSpinBox( self._control_panel, min=-86400, max=86400 )
self._data_timezone_offset = ClientGUICommon.BetterSpinBox( self._control_panel, min=-86400, max=86400 )
for e in ( 'hex', 'base64', 'url percent encoding', 'unicode escape characters', 'html entities' ):
@ -1318,7 +1318,7 @@ class EditStringSlicerPanel( ClientGUIScrolledPanels.EditPanel ):
self._single_panel = QW.QWidget( self._controls_panel )
self._index_single = QP.MakeQSpinBox( self._single_panel, min = -65536, max = 65536 )
self._index_single = ClientGUICommon.BetterSpinBox( self._single_panel, min = -65536, max = 65536 )
self._range_panel = QW.QWidget( self._controls_panel )

View File

@ -197,10 +197,10 @@ class EditSubscriptionPanel( ClientGUIScrolledPanels.EditPanel ):
limits_max = 1000
self._initial_file_limit = QP.MakeQSpinBox( self._file_limits_panel, min=1, max=limits_max )
self._initial_file_limit = ClientGUICommon.BetterSpinBox( self._file_limits_panel, min=1, max=limits_max )
self._initial_file_limit.setToolTip( 'The first sync will add no more than this many URLs.' )
self._periodic_file_limit = QP.MakeQSpinBox( self._file_limits_panel, min=1, max=limits_max )
self._periodic_file_limit = ClientGUICommon.BetterSpinBox( self._file_limits_panel, min=1, max=limits_max )
self._periodic_file_limit.setToolTip( 'Normal syncs will add no more than this many URLs, stopping early if they find several URLs the query has seen before.' )
self._this_is_a_random_sample_sub = QW.QCheckBox( self._file_limits_panel )

View File

@ -72,7 +72,7 @@ class EditCheckerOptions( ClientGUIScrolledPanels.EditPanel ):
self._reactive_check_panel = ClientGUICommon.StaticBox( self, 'reactive checking' )
self._intended_files_per_check = QP.MakeQSpinBox( self._reactive_check_panel, min=1, max=1000 )
self._intended_files_per_check = ClientGUICommon.BetterSpinBox( self._reactive_check_panel, min=1, max=1000 )
self._intended_files_per_check.setToolTip( 'How many new files you want the checker to find on each check. If a source is producing about 2 files a day, and this is set to 6, you will probably get a check every three days. You probably want this to be a low number, like 1-4.' )
self._never_faster_than = TimeDeltaCtrl( self._reactive_check_panel, min = never_faster_than_min, days = True, hours = True, minutes = True, seconds = True )
@ -348,7 +348,7 @@ class TimeDeltaCtrl( QW.QWidget ):
if self._show_days:
self._days = QP.MakeQSpinBox( self, min=0, max=3653, width = 50 )
self._days = ClientGUICommon.BetterSpinBox( self, min=0, max=3653, width = 50 )
self._days.valueChanged.connect( self.EventChange )
QP.AddToLayout( hbox, self._days, CC.FLAGS_CENTER_PERPENDICULAR )
@ -357,7 +357,7 @@ class TimeDeltaCtrl( QW.QWidget ):
if self._show_hours:
self._hours = QP.MakeQSpinBox( self, min=0, max=23, width = 45 )
self._hours = ClientGUICommon.BetterSpinBox( self, min=0, max=23, width = 45 )
self._hours.valueChanged.connect( self.EventChange )
QP.AddToLayout( hbox, self._hours, CC.FLAGS_CENTER_PERPENDICULAR )
@ -366,7 +366,7 @@ class TimeDeltaCtrl( QW.QWidget ):
if self._show_minutes:
self._minutes = QP.MakeQSpinBox( self, min=0, max=59, width = 45 )
self._minutes = ClientGUICommon.BetterSpinBox( self, min=0, max=59, width = 45 )
self._minutes.valueChanged.connect( self.EventChange )
QP.AddToLayout( hbox, self._minutes, CC.FLAGS_CENTER_PERPENDICULAR )
@ -375,7 +375,7 @@ class TimeDeltaCtrl( QW.QWidget ):
if self._show_seconds:
self._seconds = QP.MakeQSpinBox( self, min=0, max=59, width = 45 )
self._seconds = ClientGUICommon.BetterSpinBox( self, min=0, max=59, width = 45 )
self._seconds.valueChanged.connect( self.EventChange )
QP.AddToLayout( hbox, self._seconds, CC.FLAGS_CENTER_PERPENDICULAR )
@ -549,7 +549,7 @@ class VelocityCtrl( QW.QWidget ):
QW.QWidget.__init__( self, parent )
self._num = QP.MakeQSpinBox( self, min=min_unit_value, max=max_unit_value, width = 60 )
self._num = ClientGUICommon.BetterSpinBox( self, min=min_unit_value, max=max_unit_value, width = 60 )
self._times = TimeDeltaCtrl( self, min = min_time_delta, days = days, hours = hours, minutes = minutes, seconds = seconds )

View File

@ -26,7 +26,6 @@ from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
import math
import typing
from collections import defaultdict
@ -941,16 +940,6 @@ def SplitHorizontally( splitter: QW.QSplitter, w1, w2, vpos ):
splitter.setSizes( [ vpos, total_sum - vpos ] )
def MakeQLabelWithAlignment( label, parent, align ):
res = QW.QLabel( label, parent )
res.setAlignment( align )
return res
class GridLayout( QW.QGridLayout ):
def __init__( self, cols = 1, spacing = 2 ):
@ -1197,17 +1186,6 @@ def AddShortcut( widget, modifier, key, callable, *args ):
shortcut.activated.connect( lambda: callable( *args ) )
class BusyCursor:
def __enter__( self ):
QW.QApplication.setOverrideCursor( QC.Qt.WaitCursor )
def __exit__( self, exc_type, exc_val, exc_tb ):
QW.QApplication.restoreOverrideCursor()
def GetBackgroundColour( widget ):
return widget.palette().color( QG.QPalette.Window )
@ -1325,10 +1303,6 @@ def Unsplit( splitter, widget ):
widget.setVisible( False )
def GetSystemColour( colour ):
return QG.QPalette().color( colour )
def CenterOnWindow( parent, window ):
parent_window = parent.window()
@ -1394,21 +1368,6 @@ def ListWidgetSetSelection( widget, idxs ):
def MakeQSpinBox( parent = None, initial = None, min = None, max = None, width = None ):
spinbox = QW.QSpinBox( parent )
if min is not None: spinbox.setMinimum( min )
if max is not None: spinbox.setMaximum( max )
if initial is not None: spinbox.setValue( initial )
if width is not None: spinbox.setMinimumWidth( width )
return spinbox
def SetInitialSize( widget, size ):
if hasattr( widget, 'SetInitialSize' ):
@ -1701,6 +1660,18 @@ class RadioBox( QW.QFrame ):
self._choices[-1].setChecked( True )
def _GetCurrentChoiceWidget( self ):
for choice in self._choices:
if choice.isChecked():
return choice
return None
def GetCurrentIndex( self ):
for i in range( len( self._choices ) ):
@ -1731,6 +1702,20 @@ class RadioBox( QW.QFrame ):
return None
def setFocus( self, reason ):
item = self._GetCurrentChoiceWidget()
if item is not None:
item.setFocus( reason )
else:
QW.QFrame.setFocus( self, reason )
def SetValue( self, data ):
pass

View File

@ -740,6 +740,7 @@ class Canvas( QW.QWidget ):
( previous_width, previous_height ) = CalculateMediaSize( previous_media, self._current_zoom )
( previous_media_100_width, previous_media_100_height ) = previous_media.GetResolution()
( current_media_100_width, current_media_100_height ) = self._current_media.GetResolution()
width_locked_zoom = previous_width / current_media_100_width
@ -748,30 +749,43 @@ class Canvas( QW.QWidget ):
width_locked_size = CalculateMediaContainerSize( self._current_media, width_locked_zoom, media_show_action )
height_locked_size = CalculateMediaContainerSize( self._current_media, height_locked_zoom, media_show_action )
# if we have both landscape, we'll go height, otherwise default width
if previous_width > previous_height and current_media_100_width > current_media_100_height:
# if landscape, go height, portrait, go width
if previous_media_100_width > previous_media_100_height and current_media_100_width > current_media_100_height:
lock_height = True
else:
elif previous_media_100_width < previous_media_100_height and current_media_100_width < current_media_100_height:
lock_height = False
else:
# for weird stuff, we'll choose the smaller of the two ratios
width_difference = max( previous_media_100_width, current_media_100_width ) / min( previous_media_100_width, current_media_100_width )
height_difference = max( previous_media_100_height, current_media_100_height ) / min( previous_media_100_height, current_media_100_height )
lock_height = height_difference <= width_difference
if previous_current_zoom == previous_default_zoom and previous_current_zoom <= previous_canvas_zoom * 1.02:
# however we don't want to accidentally zoom in if the media we are switching to is larger. it'll spill over the bottom of the canvas
# therefore let's have a little safety check
if previous_current_zoom == previous_default_zoom and previous_current_zoom <= previous_canvas_zoom * 1.05:
# we were looking at the default zoom, near or at canvas edge(s), probably hadn't zoomed before switching comparison
# we want to make sure our comparison does not spill over the canvas edge
width_a_concern = self._media_container.width() >= self.width() * 0.95
height_a_concern = self._media_container.height() >= self.height() * 0.95
# locking by width will spill over bottom of screen
if height_a_concern and width_locked_size.height() > self._media_container.height():
if height_a_concern and width_locked_size.height() >= self._media_container.height():
lock_height = True
width_a_concern = self._media_container.width() >= self.width() * 0.95
# locking by height will spill over right of screen
if width_a_concern and height_locked_size.width() > self._media_container.width():
@ -1997,7 +2011,7 @@ class CanvasPanel( Canvas ):
copy_hash_menu = QW.QMenu( copy_menu )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 (hydrus default)', 'Copy this file\'s SHA256 hash.', self._CopyHashToClipboard, 'sha256' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 ({})'.format( self._current_media.GetHash().hex() ), 'Copy this file\'s SHA256 hash.', self._CopyHashToClipboard, 'sha256' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Copy this file\'s MD5 hash.', self._CopyHashToClipboard, 'md5' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Copy this file\'s SHA1 hash.', self._CopyHashToClipboard, 'sha1' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Copy this file\'s SHA512 hash.', self._CopyHashToClipboard, 'sha512' )
@ -4514,7 +4528,7 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
copy_hash_menu = QW.QMenu( copy_menu )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 (hydrus default)', 'Copy this file\'s SHA256 hash to your clipboard.', self._CopyHashToClipboard, 'sha256' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 ({})'.format( self._current_media.GetHash().hex() ), 'Copy this file\'s SHA256 hash to your clipboard.', self._CopyHashToClipboard, 'sha256' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Copy this file\'s MD5 hash to your clipboard.', self._CopyHashToClipboard, 'md5' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Copy this file\'s SHA1 hash to your clipboard.', self._CopyHashToClipboard, 'sha1' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Copy this file\'s SHA512 hash to your clipboard.', self._CopyHashToClipboard, 'sha512' )

View File

@ -1282,7 +1282,9 @@ class CanvasHoverFrameTopRight( CanvasHoverFrame ):
# repo strings
self._file_repos = QP.MakeQLabelWithAlignment( '', self, QC.Qt.AlignRight | QC.Qt.AlignVCenter )
self._file_repos = ClientGUICommon.BetterStaticText( self, '' )
self._file_repos.setAlignment( QC.Qt.AlignRight | QC.Qt.AlignVCenter )
# urls

View File

@ -1027,7 +1027,7 @@ class MediaContainer( QW.QWidget ):
self._controls_bar = QW.QWidget( self )
QP.SetBackgroundColour( self._controls_bar, QP.GetSystemColour( QG.QPalette.Shadow ) )
QP.SetBackgroundColour( self._controls_bar, QG.QPalette().color( QG.QPalette.Shadow ) )
self._animation_bar = AnimationBar( self._controls_bar )
self._volume_control = ClientGUIMediaControls.VolumeControl( self._controls_bar, self._canvas_type, direction = 'up' )
@ -1595,11 +1595,11 @@ class EmbedButton( QW.QWidget ):
painter.setTransform( QG.QTransform().scale( 1.0, 1.0 ) )
painter.setBrush( QG.QBrush( QP.GetSystemColour( QG.QPalette.Button ) ) )
painter.setBrush( QG.QBrush( QG.QPalette().color( QG.QPalette.Button ) ) )
painter.drawEllipse( QC.QPointF( center_x, center_y ), radius, radius )
painter.setBrush( QG.QBrush( QP.GetSystemColour( QG.QPalette.Window ) ) )
painter.setBrush( QG.QBrush( QG.QPalette().color( QG.QPalette.Window ) ) )
# play symbol is a an equilateral triangle
@ -1623,7 +1623,7 @@ class EmbedButton( QW.QWidget ):
#
painter.setPen( QG.QPen( QP.GetSystemColour( QG.QPalette.Shadow ) ) )
painter.setPen( QG.QPen( QG.QPalette().color( QG.QPalette.Shadow ) ) )
painter.setBrush( QC.Qt.NoBrush )

View File

@ -1174,7 +1174,7 @@ class ManagementPanelDuplicateFilter( ManagementPanel ):
self._search_distance_button = ClientGUIMenuButton.MenuButton( self._searching_panel, 'similarity', menu_items )
self._search_distance_spinctrl = QP.MakeQSpinBox( self._searching_panel, min=0, max=64, width = 50 )
self._search_distance_spinctrl = ClientGUICommon.BetterSpinBox( self._searching_panel, min=0, max=64, width = 50 )
self._search_distance_spinctrl.setSingleStep( 2 )
self._num_searched = ClientGUICommon.TextAndGauge( self._searching_panel )
@ -1214,7 +1214,7 @@ class ManagementPanelDuplicateFilter( ManagementPanel ):
self._pixel_dupes_preference.addItem( CC.similar_files_pixel_dupes_string_lookup[ p ], p )
self._max_hamming_distance = QP.MakeQSpinBox( self._filtering_panel, min = 0, max = 64 )
self._max_hamming_distance = ClientGUICommon.BetterSpinBox( self._filtering_panel, min = 0, max = 64 )
self._max_hamming_distance.setSingleStep( 2 )
self._num_potential_duplicates = ClientGUICommon.BetterStaticText( self._filtering_panel, ellipsize_end = True )
@ -4655,7 +4655,7 @@ class ManagementPanelPetitions( ManagementPanel ):
location_context = self._management_controller.GetVariable( 'location_context' )
with QP.BusyCursor():
with ClientGUICommon.BusyCursor():
media_results = self._controller.Read( 'media_results', hashes )

View File

@ -79,7 +79,8 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
self._focused_media = None
self._next_best_media_after_focused_media_removed = None
self._shift_focused_media = None
self._shift_select_started_with_this_media = None
self._media_added_in_current_shift_select = set()
self._empty_page_status_override = None
@ -406,6 +407,12 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
def _EndShiftSelect( self ):
self._shift_select_started_with_this_media = None
self._media_added_in_current_shift_select = set()
def _ExportFiles( self, do_export_and_then_quit = False ):
if len( self._selected_media ) > 0:
@ -844,7 +851,8 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
self._Select( ClientMedia.FileFilter( ClientMedia.FILE_FILTER_NONE ) )
self._SetFocusedMedia( None )
self._shift_focused_media = None
self._EndShiftSelect()
else:
@ -860,32 +868,42 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
self._SetFocusedMedia( None )
self._shift_focused_media = None
self._EndShiftSelect()
else:
self._DeselectSelect( (), ( media, ) )
if self._focused_media is None: self._SetFocusedMedia( media )
self._SetFocusedMedia( media )
self._shift_focused_media = media
self._StartShiftSelect( media )
elif shift and self._shift_focused_media is not None:
elif shift and self._shift_select_started_with_this_media is not None:
start_index = self._sorted_media.index( self._shift_focused_media )
start_index = self._sorted_media.index( self._shift_select_started_with_this_media )
end_index = self._sorted_media.index( media )
if start_index < end_index: media_to_select = set( self._sorted_media[ start_index : end_index + 1 ] )
else: media_to_select = set( self._sorted_media[ end_index : start_index + 1 ] )
if start_index < end_index:
media_from_start_of_shift_to_end = set( self._sorted_media[ start_index : end_index + 1 ] )
else:
media_from_start_of_shift_to_end = set( self._sorted_media[ end_index : start_index + 1 ] )
self._DeselectSelect( (), media_to_select )
media_to_deselect = [ m for m in self._media_added_in_current_shift_select if m not in media_from_start_of_shift_to_end ]
media_to_select = [ m for m in media_from_start_of_shift_to_end if not m.IsSelected() ]
self._media_added_in_current_shift_select.difference_update( media_to_deselect )
self._media_added_in_current_shift_select.update( media_to_select )
self._DeselectSelect( media_to_deselect, media_to_select )
self._SetFocusedMedia( media )
self._shift_focused_media = media
else:
if not media.IsSelected():
@ -898,7 +916,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
self._SetFocusedMedia( media )
self._shift_focused_media = media
self._StartShiftSelect( media )
@ -1343,9 +1361,9 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
move_focus = self._focused_media in media_to_deselect or self._focused_media is None
if move_focus or self._shift_focused_media in media_to_deselect:
if move_focus or self._shift_select_started_with_this_media in media_to_deselect:
self._shift_focused_media = None
self._EndShiftSelect()
self._DeselectSelect( media_to_deselect, media_to_select )
@ -1749,6 +1767,12 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
def _StartShiftSelect( self, media ):
self._shift_select_started_with_this_media = media
self._media_added_in_current_shift_select = set()
def _Undelete( self ):
media = self._GetSelectedFlatMedia()
@ -2876,7 +2900,7 @@ class MediaPanelThumbnails( MediaPanel ):
self._selected_media.difference_update( singleton_media )
self._selected_media.difference_update( collected_media )
self._shift_focused_media = None
self._EndShiftSelect()
self._RecalculateVirtualSize()
@ -4103,10 +4127,15 @@ class MediaPanelThumbnails( MediaPanel ):
copy_hash_menu = QW.QMenu( copy_menu )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 (hydrus default)', 'Copy the selected file\'s SHA256 hash to the clipboard.', self._CopyHashToClipboard, 'sha256' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Copy the selected file\'s MD5 hash to the clipboard.', self._CopyHashToClipboard, 'md5' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Copy the selected file\'s SHA1 hash to the clipboard.', self._CopyHashToClipboard, 'sha1' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Copy the selected file\'s SHA512 hash to the clipboard.', self._CopyHashToClipboard, 'sha512' )
if self._HasFocusSingleton():
focus_singleton = self._GetFocusSingleton()
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 ({})'.format( focus_singleton.GetHash().hex() ), 'Copy the selected file\'s SHA256 hash to the clipboard.', self._CopyHashToClipboard, 'sha256' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Copy the selected file\'s MD5 hash to the clipboard.', self._CopyHashToClipboard, 'md5' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Copy the selected file\'s SHA1 hash to the clipboard.', self._CopyHashToClipboard, 'sha1' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Copy the selected file\'s SHA512 hash to the clipboard.', self._CopyHashToClipboard, 'sha512' )
ClientGUIMenus.AppendMenu( copy_menu, copy_hash_menu, 'hash' )

View File

@ -241,10 +241,10 @@ class PanelPredicateSystemAgeDelta( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'>'] )
self._years = QP.MakeQSpinBox( self, max=30, width = 60 )
self._months = QP.MakeQSpinBox( self, max=60, width = 60 )
self._days = QP.MakeQSpinBox( self, max=90, width = 60 )
self._hours = QP.MakeQSpinBox( self, max=24, width = 60 )
self._years = ClientGUICommon.BetterSpinBox( self, max=30, width = 60 )
self._months = ClientGUICommon.BetterSpinBox( self, max=60, width = 60 )
self._days = ClientGUICommon.BetterSpinBox( self, max=90, width = 60 )
self._hours = ClientGUICommon.BetterSpinBox( self, max=24, width = 60 )
#
@ -360,10 +360,10 @@ class PanelPredicateSystemLastViewedDelta( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'>'] )
self._years = QP.MakeQSpinBox( self, max=30 )
self._months = QP.MakeQSpinBox( self, max=60 )
self._days = QP.MakeQSpinBox( self, max=90 )
self._hours = QP.MakeQSpinBox( self, max=24 )
self._years = ClientGUICommon.BetterSpinBox( self, max=30 )
self._months = ClientGUICommon.BetterSpinBox( self, max=60 )
self._days = ClientGUICommon.BetterSpinBox( self, max=90 )
self._hours = ClientGUICommon.BetterSpinBox( self, max=24 )
#
@ -479,10 +479,10 @@ class PanelPredicateSystemModifiedDelta( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'>'] )
self._years = QP.MakeQSpinBox( self, max=30 )
self._months = QP.MakeQSpinBox( self, max=60 )
self._days = QP.MakeQSpinBox( self, max=90 )
self._hours = QP.MakeQSpinBox( self, max=24 )
self._years = ClientGUICommon.BetterSpinBox( self, max=30 )
self._months = ClientGUICommon.BetterSpinBox( self, max=60 )
self._days = ClientGUICommon.BetterSpinBox( self, max=90 )
self._hours = ClientGUICommon.BetterSpinBox( self, max=24 )
#
@ -539,7 +539,7 @@ class PanelPredicateSystemDuplicateRelationships( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices = choices )
self._num = QP.MakeQSpinBox( self, min=0, max=65535 )
self._num = ClientGUICommon.BetterSpinBox( self, min=0, max=65535 )
choices = [ ( HC.duplicate_type_string_lookup[ status ], status ) for status in ( HC.DUPLICATE_MEMBER, HC.DUPLICATE_ALTERNATE, HC.DUPLICATE_FALSE_POSITIVE, HC.DUPLICATE_POTENTIAL ) ]
@ -595,8 +595,8 @@ class PanelPredicateSystemDuration( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices = choices )
self._duration_s = QP.MakeQSpinBox( self, max=3599, width = 60 )
self._duration_ms = QP.MakeQSpinBox( self, max=999, width = 60 )
self._duration_s = ClientGUICommon.BetterSpinBox( self, max=3599, width = 60 )
self._duration_ms = ClientGUICommon.BetterSpinBox( self, max=999, width = 60 )
#
@ -720,7 +720,7 @@ class PanelPredicateSystemFileViewingStatsViews( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'=','>'] )
self._num = QP.MakeQSpinBox( self, min=0, max=1000000 )
self._num = ClientGUICommon.BetterSpinBox( self, min=0, max=1000000 )
#
@ -861,7 +861,7 @@ class PanelPredicateSystemFramerate( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices = choices )
self._framerate = QP.MakeQSpinBox( self, min = 1, max = 3600, width = 60 )
self._framerate = ClientGUICommon.BetterSpinBox( self, min = 1, max = 3600, width = 60 )
#
@ -1042,7 +1042,7 @@ class PanelPredicateSystemHeight( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'=',CC.UNICODE_NOT_EQUAL_TO,'>'] )
self._height = QP.MakeQSpinBox( self, max=200000, width = 60 )
self._height = ClientGUICommon.BetterSpinBox( self, max=200000, width = 60 )
#
@ -1392,7 +1392,7 @@ class PanelPredicateSystemLimit( PanelPredicateSystemSingle ):
PanelPredicateSystemSingle.__init__( self, parent )
self._limit = QP.MakeQSpinBox( self, max=1000000, width = 60 )
self._limit = ClientGUICommon.BetterSpinBox( self, max=1000000, width = 60 )
#
@ -1485,7 +1485,7 @@ class PanelPredicateSystemNumPixels( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=[ '<', CC.UNICODE_ALMOST_EQUAL_TO, '=', CC.UNICODE_NOT_EQUAL_TO, '>' ] )
self._num_pixels = QP.MakeQSpinBox( self, max=1048576, width = 60 )
self._num_pixels = ClientGUICommon.BetterSpinBox( self, max=1048576, width = 60 )
self._unit = QP.RadioBox( self, choices=['pixels','kilopixels','megapixels'] )
@ -1541,7 +1541,7 @@ class PanelPredicateSystemNumFrames( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices = choices )
self._num_frames = QP.MakeQSpinBox( self, min = 0, max = 1000000, width = 80 )
self._num_frames = ClientGUICommon.BetterSpinBox( self, min = 0, max = 1000000, width = 80 )
#
@ -1591,7 +1591,7 @@ class PanelPredicateSystemNumTags( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'=','>'] )
self._num_tags = QP.MakeQSpinBox( self, max=2000, width = 60 )
self._num_tags = ClientGUICommon.BetterSpinBox( self, max=2000, width = 60 )
#
@ -1666,7 +1666,7 @@ class PanelPredicateSystemNumNotes( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices = [ '<', '=', '>' ] )
self._num_notes = QP.MakeQSpinBox( self, max = 256, width = 60 )
self._num_notes = ClientGUICommon.BetterSpinBox( self, max = 256, width = 60 )
#
@ -1714,7 +1714,7 @@ class PanelPredicateSystemNumWords( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'=',CC.UNICODE_NOT_EQUAL_TO,'>'] )
self._num_words = QP.MakeQSpinBox( self, max=1000000, width = 60 )
self._num_words = ClientGUICommon.BetterSpinBox( self, max=1000000, width = 60 )
#
@ -1762,9 +1762,9 @@ class PanelPredicateSystemRatio( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['=','wider than','taller than',CC.UNICODE_ALMOST_EQUAL_TO,CC.UNICODE_NOT_EQUAL_TO] )
self._width = QP.MakeQSpinBox( self, max=50000, width = 60 )
self._width = ClientGUICommon.BetterSpinBox( self, max=50000, width = 60 )
self._height = QP.MakeQSpinBox( self, max=50000, width = 60 )
self._height = ClientGUICommon.BetterSpinBox( self, max=50000, width = 60 )
#
@ -1821,7 +1821,7 @@ class PanelPredicateSystemSimilarTo( PanelPredicateSystemSingle ):
self._hashes.setMinimumSize( QC.QSize( init_width, init_height ) )
self._max_hamming = QP.MakeQSpinBox( self, max=256, width = 60 )
self._max_hamming = ClientGUICommon.BetterSpinBox( self, max=256, width = 60 )
#
@ -1933,7 +1933,7 @@ class PanelPredicateSystemTagAsNumber( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices = choices )
self._num = QP.MakeQSpinBox( self, min=-(2**31), max=(2**31)-1 )
self._num = ClientGUICommon.BetterSpinBox( self, min=-(2 ** 31), max= (2 ** 31) - 1 )
#
@ -1983,7 +1983,7 @@ class PanelPredicateSystemWidth( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'=',CC.UNICODE_NOT_EQUAL_TO,'>'] )
self._width = QP.MakeQSpinBox( self, max=200000, width = 60 )
self._width = ClientGUICommon.BetterSpinBox( self, max=200000, width = 60 )
#

View File

@ -417,7 +417,7 @@ class EditServiceRemoteSubPanel( ClientGUICommon.StaticBox ):
credentials = dictionary[ 'credentials' ]
self._host = QW.QLineEdit( self )
self._port = QP.MakeQSpinBox( self, min=1, max=65535, width = 80 )
self._port = ClientGUICommon.BetterSpinBox( self, min=1, max=65535, width = 80 )
self._test_address_button = ClientGUICommon.BetterButton( self, 'test address', self._TestAddress )
@ -1292,7 +1292,7 @@ class EditServiceRatingsNumericalSubPanel( ClientGUICommon.StaticBox ):
ClientGUICommon.StaticBox.__init__( self, parent, 'numerical ratings' )
self._num_stars = QP.MakeQSpinBox( self, min=1, max=20 )
self._num_stars = ClientGUICommon.BetterSpinBox( self, min=1, max=20 )
self._allow_zero = QW.QCheckBox( self )
#

View File

@ -86,7 +86,7 @@ class EditServersideService( ClientGUIScrolledPanels.EditPanel ):
ClientGUICommon.StaticBox.__init__( self, parent, 'basic information' )
self._name = QW.QLineEdit( self )
self._port = QP.MakeQSpinBox( self, min=1, max=65535 )
self._port = ClientGUICommon.BetterSpinBox( self, min=1, max=65535 )
self._upnp_port = ClientGUICommon.NoneableSpinCtrl( self, 'external upnp port', none_phrase = 'do not forward port', min = 1, max = 65535 )
self._bandwidth_tracker_st = ClientGUICommon.BetterStaticText( self )

View File

@ -2,7 +2,7 @@ import os
import re
import typing
from qtpy import QtCore as QC
from qtpy import QtCore as QC, QtWidgets as QW
from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
@ -559,6 +559,33 @@ class BetterNotebook( QW.QTabWidget ):
self._ShiftSelection( 1 )
class BetterSpinBox( QW.QSpinBox ):
def __init__( self, parent: QW.QWidget, initial = None, min = None, max = None, width = None ):
QW.QSpinBox.__init__( self, parent )
if min is not None:
self.setMinimum( min )
if max is not None:
self.setMaximum( max )
if initial is not None:
self.setValue( initial )
if width is not None:
self.setMinimumWidth( width )
class ButtonWithMenuArrow( QW.QToolButton ):
def __init__( self, parent: QW.QWidget, action: QW.QAction ):
@ -765,6 +792,17 @@ class BufferedWindowIcon( BufferedWindow ):
class BusyCursor( object ):
def __enter__( self ):
QW.QApplication.setOverrideCursor( QC.Qt.WaitCursor )
def __exit__( self, exc_type, exc_val, exc_tb ):
QW.QApplication.restoreOverrideCursor()
class CheckboxManager( object ):
def GetCurrentValue( self ):
@ -868,7 +906,7 @@ class AlphaColourControl( QW.QWidget ):
self._colour_picker = BetterColourControl( self )
self._alpha_selector = QP.MakeQSpinBox( self, min=0, max=255 )
self._alpha_selector = BetterSpinBox( self, min=0, max=255 )
hbox = QP.HBoxLayout( spacing = 5 )
@ -1409,7 +1447,7 @@ class NoneableSpinCtrl( QW.QWidget ):
self._checkbox.stateChanged.connect( self.EventCheckBox )
self._checkbox.setText( none_phrase )
self._one = QP.MakeQSpinBox( self, min=min, max=max )
self._one = BetterSpinBox( self, min=min, max=max )
width = ClientGUIFunctions.ConvertTextToPixelWidth( self._one, len( str( max ) ) + 5 )
@ -1417,7 +1455,7 @@ class NoneableSpinCtrl( QW.QWidget ):
if num_dimensions == 2:
self._two = QP.MakeQSpinBox( self, initial=0, min=min, max=max )
self._two = BetterSpinBox( self, initial=0, min=min, max=max )
self._two.valueChanged.connect( self._HandleValueChanged )
width = ClientGUIFunctions.ConvertTextToPixelWidth( self._two, len( str( max ) ) + 5 )
@ -1931,4 +1969,3 @@ class TextAndGauge( QW.QWidget ):
self._gauge.SetRange( range )
self._gauge.SetValue( value )

View File

@ -194,7 +194,7 @@ class BandwidthRulesCtrl( ClientGUICommon.StaticBox ):
self._bandwidth_type.currentIndexChanged.connect( self._UpdateEnabled )
self._max_allowed_bytes = BytesControl( self )
self._max_allowed_requests = QP.MakeQSpinBox( self, min=1, max=1048576 )
self._max_allowed_requests = ClientGUICommon.BetterSpinBox( self, min=1, max=1048576 )
self._time_delta = ClientGUITime.TimeDeltaButton( self, min = 1, days = True, hours = True, minutes = True, seconds = True, monthly_allowed = True )
@ -272,7 +272,7 @@ class BytesControl( QW.QWidget ):
QW.QWidget.__init__( self, parent )
self._spin = QP.MakeQSpinBox( self, min=0, max=1048576 )
self._spin = ClientGUICommon.BetterSpinBox( self, min=0, max=1048576 )
self._unit = ClientGUICommon.BetterChoice( self )

View File

@ -4,7 +4,6 @@ import time
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusFileHandling
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusPaths
@ -554,7 +553,24 @@ class ImportFolder( HydrusSerialisable.SerialisableBaseNamed ):
elif status == CC.IMPORT_FOLDER_IGNORE:
pass
file_seeds = self._file_seed_cache.GetFileSeeds( status )
for file_seed in file_seeds:
path = file_seed.file_seed_data
try:
if not os.path.exists( path ):
self._file_seed_cache.RemoveFileSeeds( ( file_seed, ) )
except Exception as e:
raise Exception( 'Tried to check existence of "{}", but could not.'.format( path ) )
@ -621,9 +637,9 @@ class ImportFolder( HydrusSerialisable.SerialisableBaseNamed ):
i = 0
num_total = len( self._file_seed_cache )
num_total_unknown = self._file_seed_cache.GetFileSeedCount( CC.STATUS_UNKNOWN )
num_total_done = num_total - num_total_unknown
# don't want to start at 23/100 because of carrying over failed results or whatever
# num_to_do is num currently unknown
num_total = self._file_seed_cache.GetFileSeedCount( CC.STATUS_UNKNOWN )
while True:
@ -647,7 +663,7 @@ class ImportFolder( HydrusSerialisable.SerialisableBaseNamed ):
time_to_save = HydrusData.GetNow() + 600
gauge_num_done = num_total_done + num_files_imported + 1
gauge_num_done = num_files_imported + 1
job_key.SetVariable( 'popup_text_1', 'importing file ' + HydrusData.ConvertValueRangeToPrettyString( gauge_num_done, num_total ) )
job_key.SetVariable( 'popup_gauge_1', ( gauge_num_done, num_total ) )
@ -658,6 +674,8 @@ class ImportFolder( HydrusSerialisable.SerialisableBaseNamed ):
if file_seed.status in CC.SUCCESSFUL_IMPORT_STATES:
hash = None
if file_seed.HasHash():
hash = file_seed.GetHash()

View File

@ -2840,7 +2840,7 @@ class MediaSort( HydrusSerialisable.SerialisableBase ):
if sort_metatype == 'system':
if sort_data in ( CC.SORT_FILES_BY_MIME, CC.SORT_FILES_BY_RANDOM ):
if sort_data in ( CC.SORT_FILES_BY_MIME, CC.SORT_FILES_BY_RANDOM, CC.SORT_FILES_BY_HASH ):
return False
@ -2884,6 +2884,13 @@ class MediaSort( HydrusSerialisable.SerialisableBase ):
return random.random()
elif sort_data == CC.SORT_FILES_BY_HASH:
def sort_key( x ):
return x.GetHash().hex()
elif sort_data == CC.SORT_FILES_BY_APPROX_BITRATE:
def sort_key( x ):
@ -3179,6 +3186,7 @@ class MediaSort( HydrusSerialisable.SerialisableBase ):
sort_string_lookup[ CC.SORT_FILES_BY_ARCHIVED_TIMESTAMP ] = ( 'oldest first', 'newest first', CC.SORT_DESC )
sort_string_lookup[ CC.SORT_FILES_BY_MIME ] = ( 'filetype', 'filetype', CC.SORT_ASC )
sort_string_lookup[ CC.SORT_FILES_BY_RANDOM ] = ( 'random', 'random', CC.SORT_ASC )
sort_string_lookup[ CC.SORT_FILES_BY_HASH ] = ( 'hash', 'hash', CC.SORT_ASC )
sort_string_lookup[ CC.SORT_FILES_BY_WIDTH ] = ( 'slimmest first', 'widest first', CC.SORT_ASC )
sort_string_lookup[ CC.SORT_FILES_BY_HEIGHT ] = ( 'shortest first', 'tallest first', CC.SORT_ASC )
sort_string_lookup[ CC.SORT_FILES_BY_RATIO ] = ( 'tallest first', 'widest first', CC.SORT_ASC )

View File

@ -55,7 +55,7 @@ LOCAL_BOORU_JSON_BYTE_LIST_PARAMS = set()
CLIENT_API_INT_PARAMS = { 'file_id', 'file_sort_type' }
CLIENT_API_BYTE_PARAMS = { 'hash', 'destination_page_key', 'page_key', 'Hydrus-Client-API-Access-Key', 'Hydrus-Client-API-Session-Key', 'tag_service_key', 'file_service_key' }
CLIENT_API_STRING_PARAMS = { 'name', 'url', 'domain', 'search', 'file_service_name', 'tag_service_name', 'reason' }
CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'system_inbox', 'system_archive', 'tags', 'file_ids', 'only_return_identifiers', 'detailed_url_information', 'hide_service_names_tags', 'simple', 'file_sort_asc', 'return_hashes', 'include_notes', 'notes', 'note_names' }
CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'system_inbox', 'system_archive', 'tags', 'file_ids', 'only_return_identifiers', 'only_return_basic_information', 'detailed_url_information', 'hide_service_names_tags', 'simple', 'file_sort_asc', 'return_hashes', 'return_file_ids', 'include_notes', 'notes', 'note_names' }
CLIENT_API_JSON_BYTE_LIST_PARAMS = { 'hashes' }
CLIENT_API_JSON_BYTE_DICT_PARAMS = { 'service_keys_to_tags', 'service_keys_to_actions_to_tags', 'service_keys_to_additional_tags' }
@ -2108,20 +2108,30 @@ class HydrusResourceClientAPIRestrictedGetFilesSearchFiles( HydrusResourceClient
return_hashes = request.parsed_request_args.GetValue( 'return_hashes', bool )
return_file_ids = True
if 'return_file_ids' in request.parsed_request_args:
return_file_ids = request.parsed_request_args.GetValue( 'return_file_ids', bool )
hash_ids = HG.client_controller.Read( 'file_query_ids', file_search_context, sort_by = sort_by, apply_implicit_limit = False )
request.client_api_permissions.SetLastSearchResults( hash_ids )
body_dict = {}
if return_hashes:
hash_ids_to_hashes = HG.client_controller.Read( 'hash_ids_to_hashes', hash_ids = hash_ids )
# maintain sort
body_dict = { 'hashes' : [ hash_ids_to_hashes[ hash_id ].hex() for hash_id in hash_ids ], 'file_ids' : list( hash_ids ) }
body_dict[ 'hashes' ] = [ hash_ids_to_hashes[ hash_id ].hex() for hash_id in hash_ids ]
else:
if return_file_ids:
body_dict = { 'file_ids' : list( hash_ids ) }
body_dict[ 'file_ids' ] = list( hash_ids )
body = Dumps( body_dict, request.preferred_mime )
@ -2190,6 +2200,7 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ):
only_return_identifiers = request.parsed_request_args.GetValue( 'only_return_identifiers', bool, default_value = False )
only_return_basic_information = request.parsed_request_args.GetValue( 'only_return_basic_information', bool, default_value = False )
hide_service_names_tags = request.parsed_request_args.GetValue( 'hide_service_names_tags', bool, default_value = False )
detailed_url_information = request.parsed_request_args.GetValue( 'detailed_url_information', bool, default_value = False )
include_notes = request.parsed_request_args.GetValue( 'include_notes', bool, default_value = False )
@ -2212,6 +2223,10 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
file_ids_to_hashes = HG.client_controller.Read( 'hash_ids_to_hashes', hash_ids = file_ids )
elif only_return_basic_information:
file_info_managers = HG.client_controller.Read( 'file_info_managers_from_ids', file_ids, sorted = True )
else:
media_results = HG.client_controller.Read( 'media_results_from_ids', file_ids, sorted = True )
@ -2235,6 +2250,10 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
file_ids_to_hashes = HG.client_controller.Read( 'hash_ids_to_hashes', hashes = hashes )
elif only_return_basic_information:
file_info_managers = HG.client_controller.Read( 'file_info_managers', hashes, sorted = True )
else:
media_results = HG.client_controller.Read( 'media_results', hashes, sorted = True )
@ -2266,6 +2285,27 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
metadata.append( metadata_row )
elif only_return_basic_information:
for file_info_manager in file_info_managers:
metadata_row = {
'file_id' : file_info_manager.hash_id,
'hash' : file_info_manager.hash.hex(),
'size' : file_info_manager.size,
'mime' : HC.mime_mimetype_string_lookup[ file_info_manager.mime ],
'ext' : HC.mime_ext_lookup[ file_info_manager.mime ],
'width' : file_info_manager.width,
'height' : file_info_manager.height,
'duration' : file_info_manager.duration,
'num_frames' : file_info_manager.num_frames,
'num_words' : file_info_manager.num_words,
'has_audio' : file_info_manager.has_audio
}
metadata.append( metadata_row )
else:
services_manager = HG.client_controller.services_manager

View File

@ -19,13 +19,13 @@ JOB_STATUS_AWAITING_LOGIN = 2
JOB_STATUS_AWAITING_SLOT = 3
JOB_STATUS_RUNNING = 4
job_status_str_lookup = {}
job_status_str_lookup[ JOB_STATUS_AWAITING_VALIDITY ] = 'waiting for validation'
job_status_str_lookup[ JOB_STATUS_AWAITING_BANDWIDTH ] = 'waiting for bandwidth'
job_status_str_lookup[ JOB_STATUS_AWAITING_LOGIN ] = 'waiting for login'
job_status_str_lookup[ JOB_STATUS_AWAITING_SLOT ] = 'waiting for free work slot'
job_status_str_lookup[ JOB_STATUS_RUNNING ] = 'running'
job_status_str_lookup = {
JOB_STATUS_AWAITING_VALIDITY : 'waiting for validation',
JOB_STATUS_AWAITING_BANDWIDTH : 'waiting for bandwidth',
JOB_STATUS_AWAITING_LOGIN : 'waiting for login',
JOB_STATUS_AWAITING_SLOT : 'waiting for free work slot',
JOB_STATUS_RUNNING : 'running'
}
class NetworkEngine( object ):
@ -49,6 +49,9 @@ class NetworkEngine( object ):
self._lock = threading.Lock()
self.MAX_JOBS = 1
self.MAX_JOBS_PER_DOMAIN = 1
self.RefreshOptions()
self._new_work_to_do = threading.Event()

View File

@ -626,6 +626,10 @@ class NetworkBandwidthManager( HydrusSerialisable.SerialisableBase ):
delay = HG.client_controller.new_options.GetInteger( 'watcher_page_wait_period' )
else:
raise NotImplementedError( 'Unknown query type' )
next_timestamp = timestamps_dict[ second_level_domain ] + delay
@ -640,8 +644,6 @@ class NetworkBandwidthManager( HydrusSerialisable.SerialisableBase ):
return ( False, next_timestamp )
raise NotImplementedError( 'Unknown query type' )
def TryToStartRequest( self, network_contexts ):

View File

@ -1,9 +1,6 @@
import collections
import threading
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusSerialisable
from hydrus.core.networking import HydrusNetworking

View File

@ -2,7 +2,6 @@ import collections
import os
import threading
import time
import typing
import urllib.parse
from hydrus.core import HydrusConstants as HC
@ -22,11 +21,11 @@ VALID_DENIED = 0
VALID_APPROVED = 1
VALID_UNKNOWN = 2
valid_str_lookup = {}
valid_str_lookup[ VALID_DENIED ] = 'denied'
valid_str_lookup[ VALID_APPROVED ] = 'approved'
valid_str_lookup[ VALID_UNKNOWN ] = 'unknown'
valid_str_lookup = {
VALID_DENIED : 'denied',
VALID_APPROVED : 'approved',
VALID_UNKNOWN : 'unknown'
}
class NetworkDomainManager( HydrusSerialisable.SerialisableBase ):
@ -2137,4 +2136,3 @@ class DomainValidationPopupProcess( object ):
self._is_done = True

View File

@ -30,7 +30,7 @@ def AddCookieToSession( session, name, value, domain, path, expires, secure = Fa
def ConvertDomainIntoAllApplicableDomains( domain, discard_www = True ):
# is an ip address or localhost, possibly with a port
if '.' not in domain or re.search( r'^[\d\.:]+$', domain ) is not None:
if '.' not in domain or re.search( r'^[\d.:]+$', domain ) is not None:
return [ domain ]
@ -420,6 +420,7 @@ def ParseURL( url: str ) -> urllib.parse.ParseResult:
return urllib.parse.urlparse( url )
OH_NO_NO_NETLOC_CHARACTERS = '?#'
OH_NO_NO_NETLOC_CHARACTERS_UNICODE_TRANSLATE = { ord( char ) : '_' for char in OH_NO_NO_NETLOC_CHARACTERS }
@ -476,4 +477,3 @@ def UnicodeNormaliseURL( url: str ):
return url

View File

@ -160,7 +160,9 @@ class NetworkJob( object ):
self._method = method
self._url = url
self._current_connection_attempt_number = 1
self._max_connection_attempts_allowed = 5
self._we_tried_cloudflare_once = False
self._domain = ClientNetworkingFunctions.ConvertURLIntoDomain( self._url )
self._second_level_domain = ClientNetworkingFunctions.ConvertURLIntoSecondLevelDomain( self._url )
@ -185,9 +187,6 @@ class NetworkJob( object ):
self._files = None
self._for_login = False
self._current_connection_attempt_number = 1
self._we_tried_cloudflare_once = False
self._additional_headers = {}
self._creation_time = HydrusData.GetNow()
@ -288,9 +287,7 @@ class NetworkJob( object ):
def _GenerateNetworkContexts( self ):
network_contexts = []
network_contexts.append( ClientNetworkingContexts.GLOBAL_NETWORK_CONTEXT )
network_contexts = [ ClientNetworkingContexts.GLOBAL_NETWORK_CONTEXT ]
domains = ClientNetworkingFunctions.ConvertDomainIntoAllApplicableDomains( self._domain )
@ -761,15 +758,38 @@ class NetworkJob( object ):
# cloudscraper refactored a bit around 1.2.60, so we now have some different paths to what we want
old_module = None
new_module = None
if hasattr( cloudscraper, 'CloudScraper' ):
old_module = getattr( cloudscraper, 'CloudScraper' )
if hasattr( cloudscraper, 'cloudflare' ):
m = getattr( cloudscraper, 'cloudflare' )
if hasattr( m, 'Cloudflare' ):
new_module = getattr( m, 'Cloudflare' )
possible_paths = [
( cloudscraper.CloudScraper, 'is_Firewall_Blocked' ),
( cloudscraper.cloudflare.Cloudflare, 'is_Firewall_Blocked' )
( old_module, 'is_Firewall_Blocked' ),
( new_module, 'is_Firewall_Blocked' )
]
is_firewall = False
for ( m, method_name ) in possible_paths:
if m is None:
continue
if hasattr( m, method_name ):
is_firewall = getattr( m, method_name )( response )
@ -782,15 +802,20 @@ class NetworkJob( object ):
possible_paths = [
( cloudscraper.CloudScraper, 'is_reCaptcha_Challenge' ),
( cloudscraper.CloudScraper, 'is_Captcha_Challenge' ),
( cloudscraper.cloudflare.Cloudflare, 'is_Captcha_Challenge' )
( old_module, 'is_reCaptcha_Challenge' ),
( old_module, 'is_Captcha_Challenge' ),
( new_module, 'is_Captcha_Challenge' )
]
is_captcha = False
for ( m, method_name ) in possible_paths:
if m is None:
continue
if hasattr( m, method_name ):
is_captcha = getattr( m, method_name )( response )
@ -803,15 +828,20 @@ class NetworkJob( object ):
possible_paths = [
( cloudscraper.CloudScraper, 'is_IUAM_Challenge' ),
( cloudscraper.cloudflare.Cloudflare, 'is_IUAM_Challenge' ),
( cloudscraper.cloudflare.Cloudflare, 'is_New_IUAM_Challenge' )
( old_module, 'is_IUAM_Challenge' ),
( new_module, 'is_IUAM_Challenge' ),
( new_module, 'is_New_IUAM_Challenge' )
]
is_iuam = False
for ( m, method_name ) in possible_paths:
if m is None:
continue
if hasattr( m, method_name ):
is_iuam = getattr( m, method_name )( response )
@ -892,7 +922,7 @@ class NetworkJob( object ):
def _WaitOnConnectionError( self, status_text ):
def _WaitOnConnectionError( self, status_text: str ):
connection_error_wait_time = HG.client_controller.new_options.GetInteger( 'connection_error_wait_time' )
@ -902,7 +932,22 @@ class NetworkJob( object ):
with self._lock:
self._status_text = status_text + ' - retrying in {}'.format( ClientData.TimestampToPrettyTimeDelta( self._connection_error_wake_time ) )
self._status_text = '{} - retrying in {}'.format( status_text, ClientData.TimestampToPrettyTimeDelta( self._connection_error_wake_time ) )
time.sleep( 1 )
self._WaitOnNetworkTrafficPaused( status_text )
def _WaitOnNetworkTrafficPaused( self, status_text: str ):
while HG.client_controller.new_options.GetBoolean( 'pause_all_new_network_traffic' ) and not self._IsCancelled():
with self._lock:
self._status_text = '{} - now waiting because all network traffic is paused'.format( status_text )
time.sleep( 1 )
@ -917,7 +962,7 @@ class NetworkJob( object ):
def _WaitOnServersideBandwidth( self, status_text ):
def _WaitOnServersideBandwidth( self, status_text: str ):
# 429 or 509 response from server. basically means 'I'm under big load mate'
# a future version of this could def talk to domain manager and add a temp delay so other network jobs can be informed
@ -930,12 +975,14 @@ class NetworkJob( object ):
with self._lock:
self._status_text = status_text + ' - retrying in {}'.format( ClientData.TimestampToPrettyTimeDelta( self._serverside_bandwidth_wake_time ) )
self._status_text = '{} - retrying in {}'.format( status_text, ClientData.TimestampToPrettyTimeDelta( self._serverside_bandwidth_wake_time ) )
time.sleep( 1 )
self._WaitOnNetworkTrafficPaused( status_text )
def AddAdditionalHeader( self, key, value ):
@ -1884,6 +1931,43 @@ class NetworkJobSubscription( NetworkJob ):
return network_contexts
def CheckHydrusVersion( service_type, response ):
service_string = HC.service_string_lookup[ service_type ]
headers = response.headers
if 'server' in headers and service_string in headers[ 'server' ]:
server_header = headers[ 'server' ]
elif 'hydrus-server' in headers and service_string in headers[ 'hydrus-server' ]:
server_header = headers[ 'hydrus-server' ]
else:
raise HydrusExceptions.WrongServiceTypeException( 'Target was not a ' + service_string + '!' )
( service_string_gumpf, network_version ) = server_header.split( '/' )
network_version = int( network_version )
if network_version != HC.NETWORK_VERSION:
if network_version > HC.NETWORK_VERSION:
message = 'Your client is out of date; please download the latest release.'
else:
message = 'The server is out of date; please ask its admin to update to the latest release.'
raise HydrusExceptions.NetworkVersionException( 'Network version mismatch! The server\'s network version was ' + str( network_version ) + ', whereas your client\'s is ' + str( HC.NETWORK_VERSION ) + '! ' + message )
class NetworkJobHydrus( NetworkJob ):
WILLING_TO_WAIT_ON_INVALID_LOGIN = False
@ -1896,50 +1980,12 @@ class NetworkJobHydrus( NetworkJob ):
NetworkJob.__init__( self, method, url, body = body, referral_url = referral_url, temp_path = temp_path )
def _CheckHydrusVersion( self, service_type, response ):
service_string = HC.service_string_lookup[ service_type ]
headers = response.headers
if 'server' in headers and service_string in headers[ 'server' ]:
server_header = headers[ 'server' ]
elif 'hydrus-server' in headers and service_string in headers[ 'hydrus-server' ]:
server_header = headers[ 'hydrus-server' ]
else:
raise HydrusExceptions.WrongServiceTypeException( 'Target was not a ' + service_string + '!' )
( service_string_gumpf, network_version ) = server_header.split( '/' )
network_version = int( network_version )
if network_version != HC.NETWORK_VERSION:
if network_version > HC.NETWORK_VERSION:
message = 'Your client is out of date; please download the latest release.'
else:
message = 'The server is out of date; please ask its admin to update to the latest release.'
raise HydrusExceptions.NetworkVersionException( 'Network version mismatch! The server\'s network version was ' + str( network_version ) + ', whereas your client\'s is ' + str( HC.NETWORK_VERSION ) + '! ' + message )
def _GenerateNetworkContexts( self ):
network_contexts = []
network_contexts.append( ClientNetworkingContexts.GLOBAL_NETWORK_CONTEXT )
network_contexts.append( ClientNetworkingContexts.NetworkContext( CC.NETWORK_CONTEXT_HYDRUS, self._service_key ) )
network_contexts = [
ClientNetworkingContexts.GLOBAL_NETWORK_CONTEXT,
ClientNetworkingContexts.NetworkContext( CC.NETWORK_CONTEXT_HYDRUS, self._service_key )
]
return network_contexts
@ -1987,7 +2033,7 @@ class NetworkJobHydrus( NetworkJob ):
if response.ok and service_type in HC.RESTRICTED_SERVICES:
self._CheckHydrusVersion( service_type, response )
CheckHydrusVersion( service_type, response )
return response

View File

@ -79,8 +79,8 @@ options = {}
# Misc
NETWORK_VERSION = 20
SOFTWARE_VERSION = 478
CLIENT_API_VERSION = 28
SOFTWARE_VERSION = 479
CLIENT_API_VERSION = 29
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -97,7 +97,7 @@ try:
# this preserves colour info but does EXIF reorientation and flipping
CV_IMREAD_FLAGS_JPEG = cv2.IMREAD_ANYDEPTH | cv2.IMREAD_ANYCOLOR
# this seems to allow weirdass tiffs to load as non greyscale, although the LAB conversion 'whitepoint' or whatever can be wrong
CV_IMREAD_FLAGS_WEIRD = cv2.IMREAD_ANYDEPTH | cv2.IMREAD_ANYCOLOR
CV_IMREAD_FLAGS_WEIRD = CV_IMREAD_FLAGS_PNG
CV_JPEG_THUMBNAIL_ENCODE_PARAMS = [ cv2.IMWRITE_JPEG_QUALITY, 92 ]
CV_PNG_THUMBNAIL_ENCODE_PARAMS = [ cv2.IMWRITE_PNG_COMPRESSION, 9 ]
@ -310,7 +310,7 @@ def GenerateNumPyImage( path, mime, force_pil = False ) -> numpy.array:
HydrusData.ShowText( 'Loading with OpenCV' )
if mime == HC.IMAGE_JPEG:
if mime in ( HC.IMAGE_JPEG, HC.IMAGE_TIFF ):
flags = CV_IMREAD_FLAGS_JPEG
@ -450,7 +450,7 @@ def GenerateThumbnailBytesFromStaticImagePath( path, target_resolution, mime, cl
pil_image = GeneratePILImage( path )
if clip_rect is None:
if clip_rect is not None:
pil_image = ClipPILImage( pil_image, clip_rect )

View File

@ -2237,6 +2237,69 @@ class TestClientAPI( unittest.TestCase ):
self.assertEqual( set( d[ 'hashes' ] ), expected_hashes_set )
self.assertIn( 'file_ids', d )
[ ( args, kwargs ) ] = HG.test_controller.GetRead( 'file_query_ids' )
( file_search_context, ) = args
self.assertEqual( file_search_context.GetLocationContext().current_service_keys, { CC.LOCAL_FILE_SERVICE_KEY } )
self.assertEqual( file_search_context.GetTagSearchContext().service_key, CC.COMBINED_TAG_SERVICE_KEY )
self.assertEqual( set( file_search_context.GetPredicates() ), { ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, tag ) for tag in tags } )
self.assertIn( 'sort_by', kwargs )
sort_by = kwargs[ 'sort_by' ]
self.assertEqual( sort_by.sort_type, ( 'system', CC.SORT_FILES_BY_IMPORT_TIME ) )
self.assertEqual( sort_by.sort_order, CC.SORT_DESC )
self.assertIn( 'apply_implicit_limit', kwargs )
self.assertEqual( kwargs[ 'apply_implicit_limit' ], False )
[ ( args, kwargs ) ] = HG.test_controller.GetRead( 'hash_ids_to_hashes' )
hash_ids = kwargs[ 'hash_ids' ]
self.assertEqual( set( hash_ids ), sample_hash_ids )
self.assertEqual( set( hash_ids ), set( d[ 'file_ids' ] ) )
# search files and only get hashes
HG.test_controller.ClearReads( 'file_query_ids' )
sample_hash_ids = set( random.sample( hash_ids, 3 ) )
hash_ids_to_hashes = { hash_id : os.urandom( 32 ) for hash_id in sample_hash_ids }
HG.test_controller.SetRead( 'file_query_ids', set( sample_hash_ids ) )
HG.test_controller.SetRead( 'hash_ids_to_hashes', hash_ids_to_hashes )
tags = [ 'kino', 'green' ]
path = '/get_files/search_files?tags={}&return_hashes=true&return_file_ids=false'.format( urllib.parse.quote( json.dumps( tags ) ) )
connection.request( 'GET', path, headers = headers )
response = connection.getresponse()
data = response.read()
text = str( data, 'utf-8' )
self.assertEqual( response.status, 200 )
d = json.loads( text )
expected_hashes_set = { hash.hex() for hash in hash_ids_to_hashes.values() }
self.assertEqual( set( d[ 'hashes' ] ), expected_hashes_set )
self.assertNotIn( 'file_ids', d )
[ ( args, kwargs ) ] = HG.test_controller.GetRead( 'file_query_ids' )
( file_search_context, ) = args
@ -2629,6 +2692,7 @@ class TestClientAPI( unittest.TestCase ):
expected_identifier_result = { 'metadata' : metadata }
media_results = []
file_info_managers = []
urls = { "https://gelbooru.com/index.php?page=post&s=view&id=4841557", "https://img2.gelbooru.com//images/80/c8/80c8646b4a49395fb36c805f316c49a9.jpg" }
@ -2653,6 +2717,8 @@ class TestClientAPI( unittest.TestCase ):
file_info_manager = ClientMediaManagers.FileInfoManager( file_id, hash, size = size, mime = mime, width = width, height = height, duration = duration, has_audio = has_audio )
file_info_managers.append( file_info_manager )
service_keys_to_statuses_to_tags = { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY : { HC.CONTENT_STATUS_CURRENT : [ 'blue_eyes', 'blonde_hair' ], HC.CONTENT_STATUS_PENDING : [ 'bodysuit' ] } }
service_keys_to_statuses_to_display_tags = { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY : { HC.CONTENT_STATUS_CURRENT : [ 'blue eyes', 'blonde hair' ], HC.CONTENT_STATUS_PENDING : [ 'bodysuit', 'clothing' ] } }
@ -2684,6 +2750,7 @@ class TestClientAPI( unittest.TestCase ):
metadata = []
detailed_known_urls_metadata = []
with_notes_metadata = []
only_return_basic_information_metadata = []
services_manager = HG.client_controller.services_manager
@ -2704,7 +2771,12 @@ class TestClientAPI( unittest.TestCase ):
'duration' : file_info_manager.duration,
'has_audio' : file_info_manager.has_audio,
'num_frames' : file_info_manager.num_frames,
'num_words' : file_info_manager.num_words,
'num_words' : file_info_manager.num_words
}
only_return_basic_information_metadata.append( dict( metadata_row ) )
metadata_row.update( {
'file_services' : {
'current' : {
random_file_service_hex_current.hex() : {
@ -2723,7 +2795,7 @@ class TestClientAPI( unittest.TestCase ):
'is_local' : False,
'is_trashed' : False,
'known_urls' : list( sorted_urls )
}
} )
tags_manager = media_result.GetTagsManager()
@ -2806,11 +2878,14 @@ class TestClientAPI( unittest.TestCase ):
expected_metadata_result = { 'metadata' : metadata }
expected_detailed_known_urls_metadata_result = { 'metadata' : detailed_known_urls_metadata }
expected_notes_metadata_result = { 'metadata' : with_notes_metadata }
expected_only_return_basic_information_result = { 'metadata' : only_return_basic_information_metadata }
HG.test_controller.SetRead( 'hash_ids_to_hashes', file_ids_to_hashes )
HG.test_controller.SetRead( 'media_results', media_results )
HG.test_controller.SetRead( 'media_results_from_ids', media_results )
HG.test_controller.SetRead( 'file_info_managers', file_info_managers )
HG.test_controller.SetRead( 'file_info_managers_from_ids', file_info_managers )
api_permissions.SetLastSearchResults( [ 1, 2, 3, 4, 5, 6 ] )
@ -2856,6 +2931,24 @@ class TestClientAPI( unittest.TestCase ):
self.assertEqual( d, expected_identifier_result )
# basic metadata from file_ids
path = '/get_files/file_metadata?file_ids={}&only_return_basic_information=true'.format( urllib.parse.quote( json.dumps( [ 1, 2, 3 ] ) ) )
connection.request( 'GET', path, headers = headers )
response = connection.getresponse()
data = response.read()
text = str( data, 'utf-8' )
self.assertEqual( response.status, 200 )
d = json.loads( text )
self.assertEqual( d, expected_only_return_basic_information_result )
# metadata from file_ids
path = '/get_files/file_metadata?file_ids={}'.format( urllib.parse.quote( json.dumps( [ 1, 2, 3 ] ) ) )
@ -2900,6 +2993,24 @@ class TestClientAPI( unittest.TestCase ):
self.assertEqual( d, expected_identifier_result )
# basic metadata from hashes
path = '/get_files/file_metadata?hashes={}&only_return_basic_information=true'.format( urllib.parse.quote( json.dumps( [ hash.hex() for hash in file_ids_to_hashes.values() ] ) ) )
connection.request( 'GET', path, headers = headers )
response = connection.getresponse()
data = response.read()
text = str( data, 'utf-8' )
self.assertEqual( response.status, 200 )
d = json.loads( text )
self.assertEqual( d, expected_only_return_basic_information_result )
# metadata from hashes
path = '/get_files/file_metadata?hashes={}'.format( urllib.parse.quote( json.dumps( [ hash.hex() for hash in file_ids_to_hashes.values() ] ) ) )

View File

@ -33,7 +33,6 @@ plugins:
'help/getting_started_downloading.md': 'getting_started_downloading.md'
'help/getting_started_files.md': 'getting_started_files.md'
'help/getting_started_installing.md': 'getting_started_installing.md'
'help/getting_started_more_files.md': 'getting_started_more_files.md'
'help/getting_started_ratings.md': 'getting_started_ratings.md'
'help/getting_started_subscriptions.md': 'getting_started_subscriptions.md'
'help/getting_started_tags.md': 'getting_started_tags.md'
@ -47,4 +46,4 @@ plugins:
'help/running_from_source.md': 'running_from_source.md'
'help/server.md': 'server.md'
'help/support.md': 'support.md'
'help/wine.md': 'wine.md'
'help/wine.md': 'wine.md'

View File

@ -19,7 +19,8 @@ nav:
- PTR.md
- petitionPractices.md
- Next Steps:
- getting_started_more_files.md
- getting_started_searching.md
- getting_started_exporting.md
- adding_new_downloaders.md
- getting_started_subscriptions.md
- filtering duplicates: duplicates.md