Version 531

closes #1037
This commit is contained in:
Hydrus Network Developer 2023-06-07 15:07:22 -05:00
parent 14693ecd9c
commit e0798b235b
No known key found for this signature in database
GPG Key ID: 76249F053212133C
27 changed files with 618 additions and 305 deletions

View File

@ -7,6 +7,38 @@ title: Changelog
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
## [Version 531](https://github.com/hydrusnetwork/hydrus/releases/tag/v531)
### misc
* fixed editing favourite searches, which I accidentally broke last week with the collect-by updates
* when you right-click a tag and get the siblings/parents menus, the list of copyable siblings, parents, and children is now truncated to 10 items each per service. stuff like pokemon has hundreds of children and for a very long time has been spamming giganto 11-column menus that cover the entire screen
* same menu truncation for the open/copy URLs menu. if there's a file that has 600 URLs for interesting technical reasons, it won't nuke you any more (issue #1037)
* updated the default pixiv file page parser, which recently broke for users who were not logged in. they seem to hide original size behind the login now, so if you do a lot of pixiv work, get Hydrus Companion or figure out a cookies.txt solution and get yourself logged in
* the downloader progress panels have a couple of status text improvements: first, they will stop saying 'waiting for a work slot' when the actual error is something unusual such as the gallery search hitting the file limit. second, when there is an unusual status and the downloader is in the paused state, it can now properly differentiate between 'paused' and 'pausing'
* some invalid URL strings now raise the correct error in the downloader system, causing them to be properly filtered away instead of sticking around and being unhelpful
* if there is a connection error because of an SSL issue, the network job is now retried like any other connection error. I originally thought these were all non-retryable like cert validation errors, but it seems some of them are just write timeouts etc.. during the negotiation, so let's see how it goes
* I believe I have fixed an error when selecting a tag in a list when that list had been previously shift-selected and then cleared and repopulated
* manage siblings and parents should be better about focusing the correct text input after they boot and load
* in future, if a taglist tries to deselect something it no longer has, it'll do an emergency 'deselect all' to exorcise the ghosts fully
* reworded the text around 'reset potential duplicates' action in the duplicates page to be more clear on what it does
* I tinkered with some of the shutdown code hoping to catch an odd issue of the exit 'last session' not saving correctly, but I don't think I figured the issue out. if you have noticed you boot up and get a session that missed up to the last 15 minutes of changes before you last shut down, please let me know you your details
* added a link to `tagrank`, a new Client API project at https://github.com/matjojo/tagrank, to the Client API help. it shows you pairs of comparison images over and over and uses `trueskill` ranking algorithm to figure out which tags are your favourite
* added a link to 'Send to Hydrus', a Client API project at https://github.com/Wyrrrd/send-to-hydrus, to the Client API help. it sends URLs from an Android device to your client
### client api
* as part of a plan to migrate to service_key indexing everywhere and reduce file_metadata bloat, the client api has a new `services` structure, a service information Object where `service_key` is the key. this is now in the `/get_services` call and `/get_files/file_metadata`, under `services` under the root. the old type-based structure in `/get_services` and the in-file embedding of service info in `/get_files/file_metadata` are still in place, so nothing breaks today, but I am officially declaring them deprecated, to be deleted in 2024, and recommend all Client API devs move to the new system before the new year
* the new service object also includes info on the local rating services. I'd like to add ratings to file_metadata fairly soon
* if you don't want the services object in `/get_files/file_metadata`, there's a new `include_services_object` param you can set to false to hide it
* updated the unit tests and client api help to reflect all this. main new section: https://hydrusnetwork.github.io/hydrus/developer_api.html#services_object
* the client api version is now 46
### update woes
* I somewhat successfully pounded my head against an issue where the first tab (usually 'my tags') was disappearing in the _manage tags/siblings/parents_ dialogs for some users. this bug, for real, seems to be the combination of (Python 3.11 + PyQt6 6.5.x + two tabs + total tab text characters > ~12 + tab selection is set to 1 during init event). Change any of those things and it doesn't happen. This is so weird a problem to otherwise normal code that I won't pivot all my 50-odd instances of tab selection to handle it and instead have hacked an answer for the three tag dialogs and filename tagging. Sorry for the trouble if you got this! Let me know if you see any more
* in a similar-but-different thing, PySide6 6.5.1 has a bug related to certain Signal connections. don't use it with hydrus, it messes up all my menus! their dev notes suggest they are going to have a fix/revert for 6.5.1.1
## [Version 530](https://github.com/hydrusnetwork/hydrus/releases/tag/v530)
### autocomplete and system predicates
@ -353,43 +385,3 @@ title: Changelog
* fixed/cleaned some bad code all around http header management
* wrote some unit tests for http headers in the client api
* wrote some unit tests for notes in sidecars
## [Version 521](https://github.com/hydrusnetwork/hydrus/releases/tag/v521)
### some tag presentation
* building on last week's custom sibling connector, if you don't like the fade you can now override the 'namespace' colour of the sibling connector if you like
* you can also set the ' OR ' connector text
* and you can set the OR connector's 'namespace' colour. it was 'system' before
* also turned off the new namespace colour fading for OR predicates, where it was unintentionally kicking in and looking horrible lol
### misc
* added a checkbox to 'file viewing statistcs' to turn off tracking for the archive/delete filter, if you don't like that
* file viewing statistics now maxes out at five times a duration-having media's duration, if that is more than your max view time
* the simple version of the file delete dialog will now never overwrite a file deletion reason if all of the to-be-deleted files already have deletion reasons (e.g. when physically deleting trash)
* the advanced version of the dialog now always selects 'keep existing reason' or 'do not alter existing reasons' when they exist, regardless of your 'remember previous reason' action. also, the 'remember previous reason' saved reason no longer updates if 'keep existing reason' or 'do not alter existing reasons' is set--it will stick on whatever it was before
* I might have fixed a height-layout bug in the petition management page
### advanced change to unnamespaced tags and their parsing
* the rule that allows ':p' as a tag (by secretly storing it as '::p') has been expanded--now any unnamespaced tag can include a colon as long as it starts with an explicit colon, which in hydrus rendering contexts is usually hidden. you can now type these in simply by beginning your tag with ':'--the secret character will be quickly swallowed
* for the parsing system, content parsers that get tags can now decide whether to set an explicit namespace or not. from now on, content parsers that are set to get unnamespaced tags will force all tags they get to be unnamespaced! this stops some site that has incidental colons in their 'subtags' from spamming twenty different new namespaces to hydrus. to preserve old parser behaviour, all existing content parsers that were left blank (no namespace) will be updated to not set an explicit namespace. if you are a parser maker, please consider whether you want to go with 'unnamespaced' or 'any namespace' going forward in your parsers--since most places don't use the hydrus 'namespace:subtag' format, I suspect when we want to make the decision, we'll want 'unnamespaced'
* I updated the pixiv parser to specifically ask for unnamespaced tags when parsing regular user tags, since it has some of these colon-having tags
* as a side thing, extra colons are now collapsed at the start of a tag--anything that starts with four colons will be collapsed down to two, with one displaying to humans
* also, during parsing, if a content parser gets a tag and the subtag already starts with its namespace, it will no longer double the namespace. parse 'character:dave' with namespace set to 'character', it will no longer produce 'character:character:dave'
### advanced file domain and file import options stuff
* all import pages that need to consult their file domain now do so on a 'realised' version of 'default file import options', so if you are set to import to 'my imports', and you open a new page from a tag or some thumbs on that import page, the new file page will be set to 'my imports', not some weird 'my files' stub value (in clients that deleted 'my files', this would be 'initialising...' forever)
* more stages of the file import process 'realise' default file import options stubs, just in case more of these problems slip through in future (e.g. in my file import unit tests, which I just discovered were all broken)
* the 'default' file import options stub is now initialised with your first local file domain rather than 'my files', so if this thing is ever still consulted anywhere, it should serve as a better last resort
* also fixed the file domain button getting stuck on 'initialising' if it starts with an empty file domain
* when you open the edit file import options dialog on a 'default' FIO and switch to a non-default, it now fills in all the details with the current LOUD FIO
### boring cleanup
* extracted the master file search method (~1800 lines of code) from the monolithic database object and into its own module. then broke several sub-pieces like rating or note searching code out into that module and cleaned misc stuff along the way. not done by any means, but this was a big db-cleanup hump
* reshuffled all the page management objects so they no longer keep an explicit copy of their current file domain--now they always consult their respective sub-objects, whether that is a file search or an importer or what. any time a page needs to consult its file domain, it'll always get the live and sensible version. as above, they also 'realise' default file import options stubs
* broke the 'getting started with tags' help page into two and straddled the 'getting started with searching' page with them. the intention is to get users typing a few tags into their first import pages, just that, and then playing around with them in search, before moving on to more complicated tag subjects
* split the 'autocomplete' section of the 'search' options into two, for read/write a/c contexts, and the default file and tag domain options have been moved there from 'files and trash' and 'tags'

View File

@ -29,7 +29,9 @@ Once the API is running, go to its entry in _services->review services_. Each ex
* [LoliSnatcher](https://github.com/NO-ob/LoliSnatcher_Droid): a booru client for Android that can talk to hydrus
* [Anime Boxes](https://www.animebox.es/): a booru browser, now supports adding your client as a Hydrus Server
* [FlipFlip](https://ififfy.github.io/flipflip/#/): an advanced slideshow interface, now supports hydrus as a source
* [Send to Hydrus](https://github.com/Wyrrrd/send-to-hydrus): send URLs from your Android device to your client
* [Iwara-Hydrus](https://github.com/GoAwayNow/Iwara-Hydrus): a userscript to simplify sending Iwara videos to Hydrus Network
* [tagrank](https://github.com/matjojo/tagrank): Shows you comparison images and cleverly ranks your favourite tag.
* [Hydrus Archive Delete](https://gitgud.io/koto/hydrus-archive-delete): Archive/Delete filter in your web browser
* [hydrus-dd](https://gitgud.io/koto/hydrus-dd): DeepDanbooru neural network tagging for Hydrus
* [hyextract](https://github.com/floogulinc/hyextract): Extract archives from Hydrus and reimport with tags and URL associations

View File

@ -178,6 +178,124 @@ If you have a clever script/program that does many things, then hit up [/get\_se
Also, note that all users can now copy their service keys from _review services_.
## The Services Object { id="services_object" }
Hydrus manages its different available domains and actions with what it calls _services_. If you are a regular user of the program, you will know about _review services_ and _manage services_. The Client API needs to refer to services, either to accept commands from you or to tell you what metadata files have and where.
When it does this, it gives you this structure, typically under a `services` key right off the root node:
```json title="Services Object"
{
"c6f63616c2074616773" : {
"name" : "my tags",
"type": 5,
"type_pretty" : "local tag service"
},
"5674450950748cfb28778b511024cfbf0f9f67355cf833de632244078b5a6f8d" : {
"name" : "example tag repo",
"type" : 0,
"type_pretty" : "hydrus tag repository"
},
"6c6f63616c2066696c6573" : {
"name" : "my files",
"type" : 2,
"type_pretty" : "local file domain"
},
"7265706f7369746f72792075706461746573" : {
"name" : "repository updates",
"type" : 20,
"type_pretty" : "local update file domain"
},
"ae7d9a603008919612894fc360130ae3d9925b8577d075cd0473090ac38b12b6" : {
"name": "example file repo",
"type" : 1,
"type_pretty" : "hydrus file repository"
},
"616c6c206c6f63616c2066696c6573" : {
"name" : "all local files",
"type": 15,
"type_pretty" : "virtual combined local file service"
},
"616c6c206c6f63616c206d65646961" : {
"name" : "all my files",
"ype" : 21,
"type_pretty" : "virtual combined local media service"
},
"616c6c206b6e6f776e2066696c6573" : {
"name" : "all known files",
"type" : 11,
"type_pretty" : "virtual combined file service"
},
"616c6c206b6e6f776e2074616773" : {
"name" : "all known tags",
"type": 10,
"type_pretty" : "virtual combined tag service"
},
"74d52c6238d25f846d579174c11856b1aaccdb04a185cb2c79f0d0e499284f2c" : {
"name" : "example local rating like service",
"type" : 7,
"type_pretty" : "local like/dislike rating service"
},
"90769255dae5c205c975fc4ce2efff796b8be8a421f786c1737f87f98187ffaf" : {
"name" : "example local rating numerical service",
"type" : 6,
"type_pretty" : "local numerical rating service"
},
"b474e0cbbab02ca1479c12ad985f1c680ea909a54eb028e3ad06750ea40d4106" : {
"name" : "example local rating inc/dec service",
"type" : 22,
"type_pretty" : "local inc/dec rating service"
},
"7472617368" : {
"name" : "trash",
"type" : 14,
"type_pretty" : "local trash file domain"
}
}
```
I hope you recognise some of the information here. But what's that hex key on each section? It is the `service_key`.
All services have these properties:
- `name` - A mutable human-friendly name like 'my tags'. You can use this to present the service to the user--they should recognise it.
- `type` - An integer enum saying whether the service is a local tag service or like/dislike rating service or whatever. This cannot change.
- `service_key` - The true 'id' of the service. It is a string of hex, sometimes just twenty or so characters but in many cases 64 characters. This cannot change, and it is how we will refer to different services.
This `service_key` is important. A user can rename their services, so `name` is not an excellent identifier, and definitely not something you should save to any permanent config file.
If we want to search some files on a particular file and tag domain, we should expect to be saying something like `file_service_key=6c6f63616c2066696c6573` and `tag_service_key=f032e94a38bb9867521a05dc7b189941a9c65c25048911f936fc639be2064a4b` somewhere in the request.
You won't see all of these, but the service `type` enum is:
* 0 - tag repository
* 1 - file repository
* 2 - a local file domain like 'my files'
* 5 - a local tag domain like 'my tags'
* 6 - a 'numerical' rating service with several stars
* 7 - a 'like/dislike' rating service with on/off status
* 10 - all known tags -- a union of all the tag services
* 11 - all known files -- a union of all the file services and files that appear in tag services
* 12 - the local booru -- you can ignore this
* 13 - IPFS
* 14 - trash
* 15 - all local files -- all files on hard disk ('all my files' + updates + trash)
* 17 - file notes
* 18 - Client API
* 19 - all deleted files -- you can ignore this
* 20 - local updates -- a file domain to store repository update files in
* 21 - all my files -- union of all local file domains
* 22 - a 'inc/dec' rating service with positive integer rating
* 99 - server administration
`type_pretty` is something you can show users. Hydrus uses the same labels in _manage services_ and so on.
If you want to know the services in a client, hit up [/get\_services](#get_services), which simply gives the above. The same structure has recently been added to [/get\_files/file\_metadata](#get_files_file_metadata) for convenience, since that refers to many different services when it is talking about file locations and ratings and so on.
I expect to hang more information off these in future, particularly star info for ratings.
Note: If you need to do some quick testing, you should be able to copy the `service_key` of any service by hitting the 'copy service key' button in _review services_.
## Access Management
### **GET `/api_version`** { id="api_version" }
@ -302,7 +420,7 @@ Example requests:
```
Response:
: Some JSON about the service. The same basic format as [/get\_services](#get_services)
: Some JSON about the service. A similar format as [/get\_services](#get_services) and [The Services Object](#services_object).
```json title="Example response"
{
"service" : {
@ -320,7 +438,7 @@ It will only respond to services in the /get_services list. I will expand the av
### **GET `/get_services`** { id="get_services" }
_Ask the client about its file and tag services._
_Ask the client about its services._
Restricted access:
: YES. At least one of Add Files, Add Tags, Manage Pages, or Search Files permission needed.
@ -330,127 +448,18 @@ Required Headers: n/a
Arguments: n/a
Response:
: Some JSON listing the client's file and tag services by name and 'service key'.
: Some JSON listing the client's services.
```json title="Example response"
{
"local_tags" : [
{
"name" : "my tags",
"service_key" : "6c6f63616c2074616773",
"type" : 5,
"type_pretty" : "local tag service"
},
{
"name" : "filenames",
"service_key" : "231a2e992b67101318c410abb6e7d98b6e32050623f138ca93bd4ad2993de31b",
"type" : 5,
"type_pretty" : "local tag service"
}
],
"tag_repositories" : [
{
"name" : "PTR",
"service_key" : "ccb0cf2f9e92c2eb5bd40986f72a339ef9497014a5fb8ce4cea6d6c9837877d9",
"type" : 0,
"type_pretty" : "hydrus tag repository"
}
],
"file_repositories" : [
{
"name" : "kamehameha central",
"service_key" : "89295dc26dae3ea7d395a1746a8fe2cb836b9472b97db48024bd05587f32ab0b",
"type" : 1,
"type_pretty" : "hydrus file repository"
}
],
"local_files" : [
{
"name" : "my files",
"service_key" : "6c6f63616c2066696c6573",
"type" : 2,
"type_pretty" : "local file domain"
}
],
"all_local_media" : [
{
"name" : "all my files",
"service_key" : "616c6c206c6f63616c206d65646961",
"type" : 21,
"type_pretty" : "virtual combined local media service"
}
],
"trash" : [
{
"name" : "trash",
"service_key" : "7472617368",
"type" : 14,
"type_pretty" : "local trash file domain"
}
],
"local_updates" : [
{
"name" : "repository updates",
"service_key" : "7265706f7369746f72792075706461746573",
"type" : 20,
"type_pretty" : "local update file domain"
}
],
"all_local_files" : [
{
"name" : "all local files",
"service_key" : "616c6c206c6f63616c2066696c6573",
"type" : 15,
"type_pretty" : "virtual combined local file service"
}
],
"all_known_files" : [
{
"name" : "all known files",
"service_key" : "616c6c206b6e6f776e2066696c6573",
"type" : 11,
"type_pretty" : "virtual combined file service"
}
],
"all_known_tags" : [
{
"name" : "all known tags",
"service_key" : "616c6c206b6e6f776e2074616773",
"type" : 10,
"type_pretty" : "virtual combined tag service"
}
]
"services" : "The Services Object"
}
```
Note that a user can rename their services, so while they will recognise `name`, it is not an excellent identifier, and definitely not something to save to any permanent config file.
`service_key` is non-mutable and is the main service identifier. The hardcoded/initial services have shorter fixed service key strings (it is usually just 'all known files' etc.. ASCII-converted to hex), but user-created services will have random 64-character hex.
Now that I state `type` and `type_pretty` here, I may rearrange this call, probably into a flat list. The `all_known_files` Object keys here are arbitrary.
For service `type`, you won't see all these, and you'll only ever need some, but the enum is:
* 0 - tag repository
* 1 - file repository
* 2 - a local file domain like 'my files'
* 5 - a local tag domain like 'my tags'
* 6 - a 'numerical' rating service with several stars
* 7 - a 'like/dislike' rating service with on/off status
* 10 - all known tags -- a union of all the tag services
* 11 - all known files -- a union of all the file services and files that appear in tag services
* 12 - the local booru -- you can ignore this
* 13 - IPFS
* 14 - trash
* 15 - all local files -- all files on hard disk ('all my files' + updates + trash)
* 17 - file notes
* 18 - Client API
* 19 - all deleted files -- you can ignore this
* 20 - local updates -- a file domain to store repository update files in
* 21 - all my files -- union of all local file domains
* 22 - a 'inc/dec' rating service with positive integer rating
* 99 - server administration
`type_pretty` is something you can show users if you like. Hydrus uses the same labels in _manage services_ and so on.
This now primarily uses [The Services Object](#services_object).
!!! note
If you do the request and look at the actual response, you will see a lot more data under different keys--this is deprecated, and will be deleted in 2024. If you use the old structure, please move over!
## Importing and Deleting Files
@ -1309,6 +1318,7 @@ Arguments (in percent-encoded JSON):
* `only_return_basic_information`: true or false (optional, defaulting to false)
* `detailed_url_information`: true or false (optional, defaulting to false)
* `include_notes`: true or false (optional, defaulting to false)
* `include_services_object`: true or false (optional, defaulting to true)
* `hide_service_keys_tags`: **Deprecated, will be deleted soon!** true or false (optional, defaulting to true)
If your access key is restricted by tag, **the files you search for must have been in the most recent search result**.
@ -1328,9 +1338,11 @@ If your access key is restricted by tag, **the files you search for must have be
This request string can obviously get pretty ridiculously long. It also takes a bit of time to fetch metadata from the database. In its normal searches, the client usually fetches file metadata in batches of 256.
Response:
: A list of JSON Objects that store a variety of file metadata.
: A list of JSON Objects that store a variety of file metadata. Also [The Services Object](#services_object) for service reference.
```json title="Example response"
{
"services" : "The Services Object",
"metadata" : [
{
"file_id" : 123,
@ -1363,23 +1375,14 @@ Response:
"known_urls" : [],
"tags" : {
"6c6f63616c2074616773" : {
"name" : "local tags",
"type" : 5,
"type_pretty" : "local tag service",
"storage_tags" : {},
"display_tags" : {}
},
"37e3849bda234f53b0e9792a036d14d4f3a9a136d1cb939705dbcd5287941db4" : {
"name" : "public tag repo",
"type" : 1,
"type_pretty" : "hydrus tag repository",
"storage_tags" : {},
"display_tags" : {}
},
"616c6c206b6e6f776e2074616773" : {
"name" : "all known tags",
"type" : 10,
"type_pretty" : "virtual combined tag service",
"storage_tags" : {},
"display_tags" : {}
}
@ -1404,29 +1407,17 @@ Response:
"file_services" : {
"current" : {
"616c6c206c6f63616c2066696c6573" : {
"name" : "all local files",
"type" : 15,
"type_pretty" : "virtual combined local file service",
"time_imported" : 1641044491
},
"616c6c206c6f63616c2066696c6573" : {
"name" : "all my files",
"type" : 21,
"type_pretty" : "virtual combined local media service",
"616c6c206c6f63616c206d65646961" : {
"time_imported" : 1641044491
},
"cb072cffbd0340b67aec39e1953c074e7430c2ac831f8e78fb5dfbda6ec8dcbd" : {
"name" : "cool space babes",
"type" : 2,
"type_pretty" : "local file domain",
"time_imported" : 1641204220
}
},
"deleted" : {
"6c6f63616c2066696c6573" : {
"name" : "my files",
"type" : 2,
"type_pretty" : "local file domain",
"time_deleted" : 1641204274,
"time_imported" : 1641044491
}
@ -1452,9 +1443,6 @@ Response:
],
"tags" : {
"6c6f63616c2074616773" : {
"name" : "local tags",
"type" : 5,
"type_pretty" : "local tag service",
"storage_tags" : {
"0" : ["samus favourites"],
"2" : ["process this later"]
@ -1465,9 +1453,6 @@ Response:
}
},
"37e3849bda234f53b0e9792a036d14d4f3a9a136d1cb939705dbcd5287941db4" : {
"name" : "public tag repo",
"type" : 1,
"type_pretty" : "hydrus tag repository",
"storage_tags" : {
"0" : ["blonde_hair", "blue_eyes", "looking_at_viewer"],
"1" : ["bodysuit"]
@ -1478,9 +1463,6 @@ Response:
}
},
"616c6c206b6e6f776e2074616773" : {
"name" : "all known tags",
"type" : 10,
"type_pretty" : "virtual combined tag service",
"storage_tags" : {
"0" : ["samus favourites", "blonde_hair", "blue_eyes", "looking_at_viewer"],
"1" : ["bodysuit"]
@ -1497,6 +1479,7 @@ Response:
```
```json title="And one where only_return_identifiers is true"
{
"services" : "The Services Object",
"metadata" : [
{
"file_id" : 123,
@ -1511,6 +1494,7 @@ Response:
```
```json title="And where only_return_basic_information is true"
{
"services" : "The Services Object",
"metadata" : [
{
"file_id" : 123,
@ -1523,7 +1507,7 @@ Response:
"duration" : null,
"has_audio" : false,
"num_frames" : null,
"num_words" : null,
"num_words" : null
},
{
"file_id" : 4567,
@ -1536,7 +1520,7 @@ Response:
"duration" : 4040,
"has_audio" : true,
"num_frames" : 102,
"num_words" : null,
"num_words" : null
}
]
}
@ -1556,11 +1540,7 @@ The `thumbnail_width` and `thumbnail_height` are a generally reliable prediction
#### tags
The 'tags' structures are undergoing transition. Previously, this was a mess of different Objects in different domains, all `service_xxx_to_xxx_tags`, but they are being transitioned to the combined `tags` Object.
`hide_service_keys_tags` is deprecated and will be deleted soon. When set to `false`, it shows the old `service_keys_to_statuses_to_tags` and `service_keys_to_statuses_to_display_tags` Objects.
The `tags` structures are similar to the [/add\_tags/add\_tags](#add_tags_add_tags) scheme, excepting that the status numbers are:
The `tags` structure is similar to the [/add\_tags/add\_tags](#add_tags_add_tags) scheme, excepting that the status numbers are:
* 0 - current
* 1 - pending
@ -1570,10 +1550,17 @@ The `tags` structures are similar to the [/add\_tags/add\_tags](#add_tags_add_ta
!!! note
Since JSON Object keys must be strings, these status numbers are strings, not ints.
To learn more about service names and keys on a client, use the [/get\_services](#get_services) call.
While the 'storage_tags' represent the actual tags stored on the database for a file, 'display_tags' reflect how tags appear in the UI, after siblings are collapsed and parents are added. If you want to edit a file's tags, refer to the storage tags. If you want to render to the user, use the display tags. The display tag calculation logic is very complicated; if the storage tags change, do not try to guess the new display tags yourself--just ask the API again.
#### services
The 'tags' and 'file_services' structures use the hexadecimal `service_key` extensively. If you need to look up the respective service name or type, check [The Services Object](#services_object) under the top level `services` key.
!!! note
If you look, those file structures actually include the service name and type already, but this bloated data is deprecated and will be deleted in 2024, so please transition over.
If you don't want the services object (it is generally superfluous on the 'simple' responses), then add `include_services_object=false`.
#### parameters
The `metadata` list _should_ come back in the same sort order you asked, whether that is in `file_ids` or `hashes`!

View File

@ -34,6 +34,35 @@
<div class="content">
<h1 id="changelog"><a href="#changelog">changelog</a></h1>
<ul>
<li>
<h2 id="version_531"><a href="#version_531">version 531</a></h2>
<ul>
<li><h3>misc</h3></li>
<li>fixed editing favourite searches, which I accidentally broke last week with the collect-by updates</li>
<li>when you right-click a tag and get the siblings/parents menus, the list of copyable siblings, parents, and children is now truncated to 10 items each per service. stuff like pokemon has hundreds of children and for a very long time has been spamming giganto 11-column menus that cover the entire screen</li>
<li>same menu truncation for the open/copy URLs menu. if there's a file that has 600 URLs for interesting technical reasons, it won't nuke you any more (issue #1037)</li>
<li>updated the default pixiv file page parser, which recently broke for users who were not logged in. they seem to hide original size behind the login now, so if you do a lot of pixiv work, get Hydrus Companion or figure out a cookies.txt solution and get yourself logged in</li>
<li>the downloader progress panels have a couple of status text improvements: first, they will stop saying 'waiting for a work slot' when the actual error is something unusual such as the gallery search hitting the file limit. second, when there is an unusual status and the downloader is in the paused state, it can now properly differentiate between 'paused' and 'pausing'</li>
<li>some invalid URL strings now raise the correct error in the downloader system, causing them to be properly filtered away instead of sticking around and being unhelpful</li>
<li>if there is a connection error because of an SSL issue, the network job is now retried like any other connection error. I originally thought these were all non-retryable like cert validation errors, but it seems some of them are just write timeouts etc.. during the negotiation, so let's see how it goes</li>
<li>I believe I have fixed an error when selecting a tag in a list when that list had been previously shift-selected and then cleared and repopulated</li>
<li>manage siblings and parents should be better about focusing the correct text input after they boot and load</li>
<li>in future, if a taglist tries to deselect something it no longer has, it'll do an emergency 'deselect all' to exorcise the ghosts fully</li>
<li>reworded the text around 'reset potential duplicates' action in the duplicates page to be more clear on what it does</li>
<li>I tinkered with some of the shutdown code hoping to catch an odd issue of the exit 'last session' not saving correctly, but I don't think I figured the issue out. if you have noticed you boot up and get a session that missed up to the last 15 minutes of changes before you last shut down, please let me know you your details</li>
<li>added a link to `tagrank`, a new Client API project at https://github.com/matjojo/tagrank, to the Client API help. it shows you pairs of comparison images over and over and uses `trueskill` ranking algorithm to figure out which tags are your favourite</li>
<li>added a link to 'Send to Hydrus', a Client API project at https://github.com/Wyrrrd/send-to-hydrus, to the Client API help. it sends URLs from an Android device to your client</li>
<li><h3>client api</h3></li>
<li>as part of a plan to migrate to service_key indexing everywhere and reduce file_metadata bloat, the client api has a new `services` structure, a service information Object where `service_key` is the key. this is now in the `/get_services` call and `/get_files/file_metadata`, under `services` under the root. the old type-based structure in `/get_services` and the in-file embedding of service info in `/get_files/file_metadata` are still in place, so nothing breaks today, but I am officially declaring them deprecated, to be deleted in 2024, and recommend all Client API devs move to the new system before the new year</li>
<li>the new service object also includes info on the local rating services. I'd like to add ratings to file_metadata fairly soon</li>
<li>if you don't want the services object in `/get_files/file_metadata`, there's a new `include_services_object` param you can set to false to hide it</li>
<li>updated the unit tests and client api help to reflect all this. main new section: https://hydrusnetwork.github.io/hydrus/developer_api.html#services_object</li>
<li>the client api version is now 46</li>
<li><h3>update woes</h3></li>
<li>I somewhat successfully pounded my head against an issue where the first tab (usually 'my tags') was disappearing in the _manage tags/siblings/parents_ dialogs for some users. this bug, for real, seems to be the combination of (Python 3.11 + PyQt6 6.5.x + two tabs + total tab text characters > ~12 + tab selection is set to 1 during init event). Change any of those things and it doesn't happen. This is so weird a problem to otherwise normal code that I won't pivot all my 50-odd instances of tab selection to handle it and instead have hacked an answer for the three tag dialogs and filename tagging. Sorry for the trouble if you got this! Let me know if you see any more</li>
<li>in a similar-but-different thing, PySide6 6.5.1 has a bug related to certain Signal connections. don't use it with hydrus, it messes up all my menus! their dev notes suggest they are going to have a fix/revert for 6.5.1.1</li>
</ul>
</li>
<li>
<h2 id="version_530"><a href="#version_530">version 530</a></h2>
<ul>

View File

@ -813,6 +813,8 @@ class Controller( HydrusController.HydrusController ):
if self.gui is not None and QP.isValid( self.gui ):
self.frame_splash_status.SetTitleText( 'saving and hiding gui\u2026' )
self.gui.SaveAndHide()

View File

@ -9360,6 +9360,38 @@ class DB( HydrusDB.HydrusDB ):
self._Execute( 'DELETE FROM local_incdec_ratings WHERE service_id NOT IN ( SELECT service_id FROM services );' )
if version == 530:
try:
domain_manager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
domain_manager.Initialise()
#
domain_manager.OverwriteDefaultParsers( [
'pixiv file page api parser'
] )
#
domain_manager.TryToLinkURLClassesAndParsers()
#
self.modules_serialisable.SetJSONDump( domain_manager )
except Exception as e:
HydrusData.PrintException( e )
message = 'Trying to update some downloader objects failed! Please let hydrus dev know!'
self.pub_initial_message( message )
self._controller.frame_splash_status.SetTitleText( 'updated db to v{}'.format( HydrusData.ToHumanInt( version + 1 ) ) )
self._Execute( 'UPDATE version SET version = ?;', ( version + 1, ) )

View File

@ -504,7 +504,7 @@ class FrameGUI( CAC.ApplicationCommandProcessorMixin, ClientGUITopLevelWindows.M
self._first_session_loaded = False
self._done_save_and_close = False
self._done_save_and_hide = False
self._did_a_backup_this_session = False
@ -8192,7 +8192,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
def SaveAndHide( self ):
if self._done_save_and_close:
if self._done_save_and_hide:
return
@ -8201,12 +8201,15 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
try:
if self._have_system_tray_icon:
self._system_tray_icon.hide()
if QP.isValid( self._message_manager ):
self._message_manager.CleanBeforeDestroy()
self._message_manager.hide()
#
@ -8228,11 +8231,6 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
if self._have_system_tray_icon:
self._system_tray_icon.hide()
#
if self._first_session_loaded:
@ -8263,7 +8261,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
self._controller.WriteSynchronous( 'serialisable', self._new_options )
self._done_save_and_close = True
self._done_save_and_hide = True
except Exception as e:

View File

@ -448,11 +448,12 @@ def AddKnownURLsViewCopyMenu( win, menu, focus_media, selected_media = None ):
if len( focus_labels_and_urls ) > 0:
for ( label, url ) in focus_labels_and_urls:
ClientGUIMenus.AppendMenuItem( urls_visit_menu, label, 'Open this url in your web browser.', ClientPaths.LaunchURLInWebBrowser, url )
ClientGUIMenus.AppendMenuItem( urls_copy_menu, label, 'Copy this url to your clipboard.', HG.client_controller.pub, 'clipboard', 'text', url )
MAX_TO_SHOW = 15
description = 'Open this url in your web browser.'
ClientGUIMenus.SpamItems( urls_visit_menu, [ ( label, description, HydrusData.Call( ClientPaths.LaunchURLInWebBrowser, url ) ) for ( label, url ) in focus_labels_and_urls ], MAX_TO_SHOW )
ClientGUIMenus.SpamLabels( urls_copy_menu, focus_labels_and_urls, MAX_TO_SHOW )
# copy this file's urls

View File

@ -1,3 +1,5 @@
import typing
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
@ -104,14 +106,20 @@ def AppendMenuItem( menu, label, description, callable, *args, **kwargs ):
return menu_item
def AppendMenuLabel( menu, label, description = '' ):
def AppendMenuLabel( menu, label, description = '', copy_text = '' ):
original_label_text = label
if copy_text == '':
copy_text = original_label_text
label = SanitiseLabel( label )
if description == '':
description = f'copy "{label}" to clipboard'
description = f'copy "{copy_text}" to clipboard'
menu_item = QW.QAction( menu )
@ -129,7 +137,7 @@ def AppendMenuLabel( menu, label, description = '' ):
menu.addAction( menu_item )
BindMenuItem( menu_item, HG.client_controller.pub, 'clipboard', 'text', original_label_text )
BindMenuItem( menu_item, HG.client_controller.pub, 'clipboard', 'text', copy_text )
return menu_item
@ -187,12 +195,14 @@ def AppendSeparator( menu ):
def BindMenuItem( menu_item, callable, *args, **kwargs ):
event_callable = GetEventCallable( callable, *args, **kwargs )
menu_item.triggered.connect( event_callable )
def DestroyMenu( menu ):
if menu is None:
@ -272,3 +282,51 @@ def SetMenuTitle( menu: QW.QMenu, label: str ):
menu.setTitle( label )
def SpamItems( menu: QW.QMenu, labels_descriptions_and_calls: typing.Collection[ typing.Tuple[ str, str, typing.Callable ] ], max_allowed: int ):
if len( labels_descriptions_and_calls ) > max_allowed:
num_to_show = max_allowed - 1
else:
num_to_show = max_allowed
for ( label, description, call ) in list( labels_descriptions_and_calls )[:num_to_show]:
AppendMenuItem( menu, label, description, call )
if len( labels_descriptions_and_calls ) > num_to_show:
# maybe one day this becomes a thing that extends the menu to show them all
AppendMenuLabel( menu, '{} more...'.format( len( labels_descriptions_and_calls ) - num_to_show ) )
def SpamLabels( menu: QW.QMenu, labels_and_copy_texts: typing.Collection[ typing.Tuple[ str, str ] ], max_allowed: int ):
if len( labels_and_copy_texts ) > max_allowed:
num_to_show = max_allowed - 1
else:
num_to_show = max_allowed
for ( label, copy_text ) in list( labels_and_copy_texts )[:num_to_show]:
AppendMenuLabel( menu, label, copy_text = copy_text )
if len( labels_and_copy_texts ) > num_to_show:
# maybe one day this becomes a thing that extends the menu to show them all
AppendMenuLabel( menu, '{} more...'.format( len( labels_and_copy_texts ) - num_to_show ) )

View File

@ -344,13 +344,12 @@ class EditTagDisplayApplication( ClientGUIScrolledPanels.EditPanel ):
page = self._Panel( self._tag_services_notebook, master_service_key, sibling_applicable_service_keys, parent_applicable_service_keys )
select = master_service_key == select_service_key
self._tag_services_notebook.addTab( page, name )
if select:
if master_service_key == select_service_key:
self._tag_services_notebook.setCurrentWidget( page )
# Py 3.11/PyQt6 6.5.0/two tabs/total tab characters > ~12/select second tab during init = first tab disappears bug
QP.CallAfter( self._tag_services_notebook.setCurrentWidget, page )
@ -542,10 +541,12 @@ class EditTagDisplayManagerPanel( ClientGUIScrolledPanels.EditPanel ):
page = self._Panel( self._tag_services, self._original_tag_display_manager, service_key )
select = service_key == CC.COMBINED_TAG_SERVICE_KEY
self._tag_services.addTab( page, name )
if select: self._tag_services.setCurrentWidget( page )
if service_key == CC.COMBINED_TAG_SERVICE_KEY:
self._tag_services.setCurrentWidget( page )
#
@ -1955,6 +1956,8 @@ class ManageTagsPanel( CAC.ApplicationCommandProcessorMixin, ClientGUIScrolledPa
page = self._Panel( self._tag_services, self._location_context, service.GetServiceKey(), self._current_media, self._immediate_commit, canvas_key = self._canvas_key )
self._tag_services.addTab( page, name )
page.movePageLeft.connect( self.MovePageLeft )
page.movePageRight.connect( self.MovePageRight )
page.showPrevious.connect( self.ShowPrevious )
@ -1962,13 +1965,10 @@ class ManageTagsPanel( CAC.ApplicationCommandProcessorMixin, ClientGUIScrolledPa
page.okSignal.connect( self.okSignal )
select = service_key == default_tag_service_key
self._tag_services.addTab( page, name )
if select:
if service_key == default_tag_service_key:
self._tag_services.setCurrentIndex( self._tag_services.count() - 1 )
# Py 3.11/PyQt6 6.5.0/two tabs/total tab characters > ~12/select second tab during init = first tab disappears bug
QP.CallAfter( self._tag_services.setCurrentWidget, page )
@ -1985,6 +1985,8 @@ class ManageTagsPanel( CAC.ApplicationCommandProcessorMixin, ClientGUIScrolledPa
self.widget().setLayout( vbox )
QP.CallAfter( self._tag_services.currentChanged.connect, self.EventServiceChanged )
if self._canvas_key is not None:
HG.client_controller.sub( self, 'CanvasHasNewMedia', 'canvas_new_display_media' )
@ -1992,9 +1994,7 @@ class ManageTagsPanel( CAC.ApplicationCommandProcessorMixin, ClientGUIScrolledPa
self._my_shortcut_handler = ClientGUIShortcuts.ShortcutsHandler( self, [ 'global', 'media', 'main_gui' ] )
self._tag_services.currentChanged.connect( self.EventServiceChanged )
self._SetSearchFocus()
QP.CallAfter( self._SetSearchFocus )
def _GetGroupsOfServiceKeysToContentUpdates( self ):
@ -2980,10 +2980,13 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
page = self._Panel( self._tag_services, service_key, tags )
select = service_key == default_tag_service_key
self._tag_services.addTab( page, name )
if select: self._tag_services.setCurrentWidget( page )
if service_key == default_tag_service_key:
# Py 3.11/PyQt6 6.5.0/two tabs/total tab characters > ~12/select second tab during init = first tab disappears bug
QP.CallAfter( self._tag_services.setCurrentWidget, page )
#
@ -4073,6 +4076,11 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
self.EnterChildren( tags )
if self.isVisible():
self.SetTagBoxFocus()
original_statuses_to_pairs = HG.client_controller.Read( 'tag_parents', service_key )
@ -4122,10 +4130,13 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
page = self._Panel( self._tag_services, service_key, tags )
select = service_key == default_tag_service_key
self._tag_services.addTab( page, name )
if select: self._tag_services.setCurrentIndex( self._tag_services.indexOf( page ) )
if service_key == default_tag_service_key:
# Py 3.11/PyQt6 6.5.0/two tabs/total tab characters > ~12/select second tab during init = first tab disappears bug
QP.CallAfter( self._tag_services.setCurrentWidget, page )
#
@ -5454,6 +5465,11 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
self.EnterOlds( tags )
if self.isVisible():
self.SetTagBoxFocus()
original_statuses_to_pairs = HG.client_controller.Read( 'tag_siblings', service_key )
@ -5509,7 +5525,7 @@ class ReviewTagDisplayMaintenancePanel( ClientGUIScrolledPanels.ReviewPanel ):
if service_key == select_service_key:
self._tag_services_notebook.setCurrentWidget( page )
QP.CallAfter( self._tag_services_notebook.setCurrentWidget, page )

View File

@ -823,13 +823,12 @@ class EditLocalImportFilenameTaggingPanel( ClientGUIScrolledPanels.EditPanel ):
page.movePageLeft.connect( self.MovePageLeft )
page.movePageRight.connect( self.MovePageRight )
select = service_key == default_tag_service_key
tab_index = self._notebook.addTab( page, name )
if select:
if service_key == default_tag_service_key:
self._notebook.setCurrentIndex( tab_index )
# Py 3.11/PyQt6 6.5.0/two tabs/total tab characters > ~12/select second tab = first tab disappears bug
QP.CallAfter( self._notebook.setCurrentWidget, page )
@ -843,7 +842,7 @@ class EditLocalImportFilenameTaggingPanel( ClientGUIScrolledPanels.EditPanel ):
self._notebook.currentChanged.connect( self._SaveDefaultTagServiceKey )
self._notebook.currentWidget().SetSearchFocus()
QP.CallAfter( self._SetSearchFocus )
def _SaveDefaultTagServiceKey( self ):
@ -864,6 +863,11 @@ class EditLocalImportFilenameTaggingPanel( ClientGUIScrolledPanels.EditPanel ):
def _SetSearchFocus( self ):
self._notebook.currentWidget().SetSearchFocus()
def GetValue( self ):
metadata_routers = self._metadata_router_page.GetValue()

View File

@ -1156,6 +1156,12 @@ class ListBox( QW.QScrollArea ):
self._positional_indices_to_terms = {}
self._total_positional_rows = 0
self._shift_click_start_logical_index = None
self._logical_indices_selected_this_shift_click = set()
self._logical_indices_deselected_this_shift_click = set()
self._in_drag = False
self._this_drag_is_a_deselection = False
self._last_hit_logical_index = None
self._last_view_start = None
@ -1174,7 +1180,17 @@ class ListBox( QW.QScrollArea ):
def _Deselect( self, index ):
term = self._GetTermFromLogicalIndex( index )
try:
term = self._GetTermFromLogicalIndex( index )
except HydrusExceptions.DataMissing:
# we've got ghosts, so exorcise them
self._DeselectAll()
return
self._selected_terms.discard( term )
@ -2924,6 +2940,8 @@ class ListBoxTags( ListBox ):
return service_key_group_names_and_tags
MAX_ITEMS_HERE = 10
if num_siblings == 0:
siblings_menu.setTitle( 'no siblings' )
@ -2961,10 +2979,7 @@ class ListBoxTags( ListBox ):
ClientGUIMenus.AppendMenuLabel( siblings_menu, '--{}--'.format( s_k_name ) )
for tag in tags:
ClientGUIMenus.AppendMenuLabel( siblings_menu, tag )
ClientGUIMenus.SpamLabels( siblings_menu, [ ( tag, tag ) for tag in tags ], MAX_ITEMS_HERE )
@ -2989,19 +3004,9 @@ class ListBoxTags( ListBox ):
ClientGUIMenus.AppendMenuLabel( parents_menu, '--{}--'.format( s_k_name ) )
for parent in parents:
parent_label = 'parent: {}'.format( parent )
ClientGUIMenus.AppendMenuItem( parents_menu, parent_label, parent_label, HG.client_controller.pub, 'clipboard', 'text', parent )
ClientGUIMenus.SpamLabels( parents_menu, [ ( f'parent: {parent}', parent ) for parent in parents ], MAX_ITEMS_HERE )
for child in children:
child_label = 'child: {}'.format( child )
ClientGUIMenus.AppendMenuItem( parents_menu, child_label, child_label, HG.client_controller.pub, 'clipboard', 'text', child )
ClientGUIMenus.SpamLabels( parents_menu, [ ( f'child: {child}', child ) for child in children ], MAX_ITEMS_HERE )

View File

@ -465,7 +465,7 @@ class ManagementPanelDuplicateFilter( ManagementPanel ):
menu_items = []
menu_items.append( ( 'normal', 'reset potential duplicates', 'This will delete all the potential duplicate pairs found so far and reset their files\' search status.', self._ResetUnknown ) )
menu_items.append( ( 'normal', 'reset potential duplicates', 'This will delete all the discovered potential duplicate pairs. All files that may have potential pairs will be queued up for similar file search again.', self._ResetUnknown ) )
menu_items.append( ( 'separator', 0, 0, 0 ) )
check_manager = ClientGUICommon.CheckboxManagerOptions( 'maintain_similar_files_duplicate_pairs_during_idle' )
@ -834,9 +834,9 @@ class ManagementPanelDuplicateFilter( ManagementPanel ):
def _ResetUnknown( self ):
text = 'This will delete all the potential duplicate pairs and reset their files\' search status.'
text = 'ADVANCED TOOL: This will delete all the current potential duplicate pairs. All files that may be similar will be queued for search again.'
text += os.linesep * 2
text += 'This can be useful if you have accidentally searched too broadly and are now swamped with too many false positives.'
text += 'This can be useful if you know you have database damage and need to reset and re-search everything, or if you have accidentally searched too broadly and are now swamped with too many false positives. It is not useful for much else.'
result = ClientGUIDialogsQuick.GetYesNo( self, text )

View File

@ -92,7 +92,7 @@ def CheckImporterCanDoWorkBecauseStopped( page_key: bytes ):
def GenerateLiveStatusText( text: str, paused: bool, no_work_until: int, no_work_until_reason: str ) -> str:
def GenerateLiveStatusText( text: str, paused: bool, currently_working: bool, no_work_until: int, no_work_until_reason: str ) -> str:
if not HydrusTime.TimeHasPassed( no_work_until ):
@ -101,15 +101,17 @@ def GenerateLiveStatusText( text: str, paused: bool, no_work_until: int, no_work
if paused and text != 'paused':
if text == '':
if currently_working:
text = 'pausing'
pause_text = 'pausing'
else:
text = 'pausing - {}'.format( text )
pause_text = 'paused'
text = f'{pause_text} - {text}'
return text

View File

@ -667,18 +667,23 @@ class GalleryImport( HydrusSerialisable.SerialisableBase ):
gallery_go = gallery_work_to_do and not self._gallery_paused
files_go = files_work_to_do and not self._files_paused
if gallery_go and not self._gallery_working_lock.locked():
if gallery_go and not self._gallery_working_lock.locked() and self._gallery_repeating_job is not None and self._gallery_repeating_job.WaitingOnWorkSlot():
self._gallery_status = 'waiting for a work slot'
if files_go and not self._files_working_lock.locked():
if files_go and not self._files_working_lock.locked() and self._files_repeating_job is not None and self._files_repeating_job.WaitingOnWorkSlot():
self._files_status = 'waiting for a work slot'
gallery_text = ClientImportControl.GenerateLiveStatusText( self._gallery_status, self._gallery_paused, self._no_work_until, self._no_work_until_reason )
file_text = ClientImportControl.GenerateLiveStatusText( self._files_status, self._files_paused, self._no_work_until, self._no_work_until_reason )
currently_working = self._gallery_repeating_job is not None and self._gallery_repeating_job.CurrentlyWorking()
gallery_text = ClientImportControl.GenerateLiveStatusText( self._gallery_status, self._gallery_paused, currently_working, self._no_work_until, self._no_work_until_reason )
currently_working = self._files_repeating_job is not None and self._files_repeating_job.CurrentlyWorking()
file_text = ClientImportControl.GenerateLiveStatusText( self._files_status, self._files_paused, currently_working, self._no_work_until, self._no_work_until_reason )
return ( gallery_text, file_text, self._files_paused, self._gallery_paused )

View File

@ -322,7 +322,9 @@ class HDDImport( HydrusSerialisable.SerialisableBase ):
with self._lock:
text = ClientImportControl.GenerateLiveStatusText( self._files_status, self._paused, 0, '' )
currently_working = self._files_repeating_job is not None and self._files_repeating_job.CurrentlyWorking()
text = ClientImportControl.GenerateLiveStatusText( self._files_status, self._paused, currently_working, 0, '' )
return ( text, self._paused )

View File

@ -553,8 +553,13 @@ class SimpleDownloaderImport( HydrusSerialisable.SerialisableBase ):
with self._lock:
gallery_text = ClientImportControl.GenerateLiveStatusText( self._gallery_status, self._gallery_paused, self._no_work_until, self._no_work_until_reason )
file_text = ClientImportControl.GenerateLiveStatusText( self._files_status, self._files_paused, self._no_work_until, self._no_work_until_reason )
currently_working = self._gallery_repeating_job is not None and self._gallery_repeating_job.CurrentlyWorking()
gallery_text = ClientImportControl.GenerateLiveStatusText( self._gallery_status, self._gallery_paused, currently_working, self._no_work_until, self._no_work_until_reason )
currently_working = self._files_repeating_job is not None and self._files_repeating_job.CurrentlyWorking()
file_text = ClientImportControl.GenerateLiveStatusText( self._files_status, self._files_paused, currently_working, self._no_work_until, self._no_work_until_reason )
return ( list( self._pending_jobs ), gallery_text, file_text, self._gallery_paused, self._files_paused )

View File

@ -1488,18 +1488,23 @@ class WatcherImport( HydrusSerialisable.SerialisableBase ):
checker_go = HydrusTime.TimeHasPassed( self._next_check_time ) and not self._checking_paused
files_go = files_work_to_do and not self._files_paused
if checker_go and not self._checker_working_lock.locked():
if checker_go and not self._checker_working_lock.locked() and self._checker_repeating_job is not None and self._checker_repeating_job.WaitingOnWorkSlot():
self._watcher_status = 'waiting for a work slot'
if files_go and not self._files_working_lock.locked():
if files_go and not self._files_working_lock.locked() and self._files_repeating_job is not None and self._files_repeating_job.WaitingOnWorkSlot():
self._files_status = 'waiting for a work slot'
files_status = ClientImportControl.GenerateLiveStatusText( self._files_status, self._files_paused, self._no_work_until, self._no_work_until_reason )
watcher_status = ClientImportControl.GenerateLiveStatusText( self._watcher_status, self._checking_paused, self._no_work_until, self._no_work_until_reason )
currently_working = self._files_repeating_job is not None and self._files_repeating_job.CurrentlyWorking()
files_status = ClientImportControl.GenerateLiveStatusText( self._files_status, self._files_paused, currently_working, self._no_work_until, self._no_work_until_reason )
currently_working = self._checker_repeating_job is not None and self._checker_repeating_job.CurrentlyWorking()
watcher_status = ClientImportControl.GenerateLiveStatusText( self._watcher_status, self._checking_paused, currently_working, self._no_work_until, self._no_work_until_reason )
return ( files_status, self._files_paused, self._file_velocity_status, self._next_check_time, watcher_status, self._subject, self._checking_status, self._check_now, self._checking_paused )

View File

@ -64,7 +64,7 @@ LOCAL_BOORU_JSON_BYTE_LIST_PARAMS = set()
CLIENT_API_INT_PARAMS = { 'file_id', 'file_sort_type', 'potentials_search_type', 'pixel_duplicates', 'max_hamming_distance', 'max_num_pairs' }
CLIENT_API_BYTE_PARAMS = { 'hash', 'destination_page_key', 'page_key', 'service_key', 'Hydrus-Client-API-Access-Key', 'Hydrus-Client-API-Session-Key', 'file_service_key', 'deleted_file_service_key', 'tag_service_key', 'tag_service_key_1', 'tag_service_key_2' }
CLIENT_API_STRING_PARAMS = { 'name', 'url', 'domain', 'search', 'service_name', 'reason', 'tag_display_type', 'source_hash_type', 'desired_hash_type' }
CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'tags', 'tags_1', 'tags_2', 'file_ids', 'only_return_identifiers', 'only_return_basic_information', 'create_new_file_ids', 'detailed_url_information', 'hide_service_keys_tags', 'simple', 'file_sort_asc', 'return_hashes', 'return_file_ids', 'include_notes', 'notes', 'note_names', 'doublecheck_file_system' }
CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'tags', 'tags_1', 'tags_2', 'file_ids', 'only_return_identifiers', 'only_return_basic_information', 'create_new_file_ids', 'detailed_url_information', 'hide_service_keys_tags', 'simple', 'file_sort_asc', 'return_hashes', 'return_file_ids', 'include_notes', 'include_services_object', 'notes', 'note_names', 'doublecheck_file_system' }
CLIENT_API_JSON_BYTE_LIST_PARAMS = { 'file_service_keys', 'deleted_file_service_keys', 'hashes' }
CLIENT_API_JSON_BYTE_DICT_PARAMS = { 'service_keys_to_tags', 'service_keys_to_actions_to_tags', 'service_keys_to_additional_tags' }
@ -166,6 +166,40 @@ def CheckTagService( tag_service_key: bytes ):
return service
def GetServicesDict():
service_types = [
HC.LOCAL_TAG,
HC.TAG_REPOSITORY,
HC.LOCAL_FILE_DOMAIN,
HC.LOCAL_FILE_UPDATE_DOMAIN,
HC.FILE_REPOSITORY,
HC.COMBINED_LOCAL_FILE,
HC.COMBINED_LOCAL_MEDIA,
HC.COMBINED_FILE,
HC.COMBINED_TAG,
HC.LOCAL_RATING_LIKE,
HC.LOCAL_RATING_NUMERICAL,
HC.LOCAL_RATING_INCDEC,
HC.LOCAL_FILE_TRASH_DOMAIN
]
services = HG.client_controller.services_manager.GetServices( service_types )
service_dict = {}
for service in services:
service_dict[ service.GetServiceKey().hex() ] = {
'name' : service.GetName(),
'type' : service.GetServiceType(),
'type_pretty' : HC.service_string_lookup[ service.GetServiceType() ]
}
return service_dict
def GetServiceKeyFromName( service_name: str ):
try:
@ -1458,6 +1492,9 @@ class HydrusResourceClientAPIRestrictedGetService( HydrusResourceClientAPIRestri
HC.COMBINED_LOCAL_MEDIA,
HC.COMBINED_FILE,
HC.COMBINED_TAG,
HC.LOCAL_RATING_LIKE,
HC.LOCAL_RATING_NUMERICAL,
HC.LOCAL_RATING_INCDEC,
HC.LOCAL_FILE_TRASH_DOMAIN
}
@ -1568,6 +1605,8 @@ class HydrusResourceClientAPIRestrictedGetServices( HydrusResourceClientAPIRestr
body_dict[ name ] = services_list
body_dict[ 'services' ] = GetServicesDict()
body = Dumps( body_dict, request.preferred_mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
@ -2641,6 +2680,7 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
hide_service_keys_tags = request.parsed_request_args.GetValue( 'hide_service_keys_tags', bool, default_value = True )
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 )
include_services_object = request.parsed_request_args.GetValue( 'include_services_object', bool, default_value = True )
create_new_file_ids = request.parsed_request_args.GetValue( 'create_new_file_ids', bool, default_value = False )
hashes = ParseHashes( request )
@ -2945,6 +2985,11 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
body_dict[ 'metadata' ] = metadata
if include_services_object:
body_dict[ 'services' ] = GetServicesDict()
mime = request.preferred_mime
body = Dumps( body_dict, mime )

View File

@ -443,7 +443,14 @@ def ParseURL( url: str ) -> urllib.parse.ParseResult:
url = UnicodeNormaliseURL( url )
return urllib.parse.urlparse( url )
try:
return urllib.parse.urlparse( url )
except Exception as e:
raise HydrusExceptions.URLClassException( str( e ) )
OH_NO_NO_NETLOC_CHARACTERS = '?#'

View File

@ -1711,15 +1711,20 @@ class NetworkJob( object ):
self._WaitOnConnectionError( 'connection broke mid-request' )
except requests.exceptions.SSLError as e:
except ( requests.exceptions.SSLError, requests.exceptions.ConnectionError, requests.exceptions.ConnectTimeout ) as e:
# note a requests SSLError is a ConnectionError, so careful about catching order here
# note a requests SSLError is a ConnectionError, so be careful if you extract this again
self.engine.domain_manager.ReportNetworkInfrastructureError( self._url )
raise HydrusExceptions.ConnectionException( 'Problem with SSL: {}'.format( repr( e ) ) )
except ( requests.exceptions.ConnectionError, requests.exceptions.ConnectTimeout ):
if isinstance( e, requests.exceptions.SSLError ):
fail_text = 'Problem with SSL: {}'.format( repr( e ) )
delay_text = 'SSL connection failed'
else:
fail_text = 'Could not connect!'
delay_text = 'connection failed'
self._ResetForAnotherConnectionAttempt()
@ -1729,10 +1734,10 @@ class NetworkJob( object ):
else:
raise HydrusExceptions.ConnectionException( 'Could not connect!' )
raise HydrusExceptions.ConnectionException( fail_text )
self._WaitOnConnectionError( 'connection failed' )
self._WaitOnConnectionError( delay_text )
except requests.exceptions.ReadTimeout:

View File

@ -100,8 +100,8 @@ options = {}
# Misc
NETWORK_VERSION = 20
SOFTWARE_VERSION = 530
CLIENT_API_VERSION = 45
SOFTWARE_VERSION = 531
CLIENT_API_VERSION = 46
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -290,6 +290,21 @@ class HydrusController( object ):
def ThreadSlotsAreAvailable( self, thread_type ) -> bool:
with self._thread_slot_lock:
if thread_type not in self._thread_slots:
return True # assume no max if no max set
( current_threads, max_threads ) = self._thread_slots[ thread_type ]
return current_threads < max_threads
def CallLater( self, initial_delay, func, *args, **kwargs ) -> HydrusThreading.SingleJob:
job_scheduler = self._GetAppropriateJobScheduler( initial_delay )

View File

@ -882,6 +882,19 @@ class SchedulableJob( object ):
HG.controller.sub( self, 'PubSubWake', topic )
def WaitingOnWorkSlot( self ):
if self._thread_slot_type is not None:
if not self._currently_working.set() and self.IsDue() and not HG.controller.ThreadSlotsAreAvailable( self._thread_slot_type ):
return True
return False
def Work( self ) -> None:
try:

View File

@ -45,6 +45,84 @@ try:
except:
pass
def GetExampleServicesDict():
services_dict = {
'6c6f63616c2074616773': {
'name' : 'my tags',
'type' : 5,
'type_pretty' : 'local tag service'
},
HG.test_controller.example_tag_repo_service_key.hex() : {
'name' : 'example tag repo',
'type' : 0,
'type_pretty' : 'hydrus tag repository'
},
'6c6f63616c2066696c6573' : {
'name' : 'my files',
'type' : 2,
'type_pretty' : 'local file domain'
},
'7265706f7369746f72792075706461746573' : {
'name' : 'repository updates',
'type' : 20,
'type_pretty' : 'local update file domain'
},
HG.test_controller.example_file_repo_service_key_1.hex() : {
'name' : 'example file repo 1',
'type' : 1,
'type_pretty' : 'hydrus file repository'
},
HG.test_controller.example_file_repo_service_key_2.hex() : {
'name' : 'example file repo 2',
'type' : 1,
'type_pretty' : 'hydrus file repository'
},
'616c6c206c6f63616c2066696c6573' : {
'name' : 'all local files',
'type' : 15,
'type_pretty' : 'virtual combined local file service'
},
'616c6c206c6f63616c206d65646961' : {
'name' : 'all my files',
'type' : 21,
'type_pretty' : 'virtual combined local media service'
},
'616c6c206b6e6f776e2066696c6573' : {
'name' : 'all known files',
'type' : 11,
'type_pretty' : 'virtual combined file service'
},
'616c6c206b6e6f776e2074616773' : {
'name' : 'all known tags',
'type' : 10,
'type_pretty' : 'virtual combined tag service'
},
HG.test_controller.example_like_rating_service_key.hex() : {
'name' : 'example local rating like service',
'type' : 7,
'type_pretty' : 'local like/dislike rating service'
},
HG.test_controller.example_numerical_rating_service_key.hex() : {
'name' : 'example local rating numerical service',
'type' : 6,
'type_pretty' : 'local numerical rating service'
},
HG.test_controller.example_incdec_rating_service_key.hex() : {
'name' : 'example local rating inc/dec service',
'type' : 22,
'type_pretty' : 'local inc/dec rating service'
},
'7472617368' : {
'name' : 'trash',
'type' : 14,
'type_pretty' : 'local trash file domain'
}
}
return services_dict
class TestClientAPI( unittest.TestCase ):
@classmethod
@ -726,7 +804,8 @@ class TestClientAPI( unittest.TestCase ):
'type': 14,
'type_pretty': 'local trash file domain'
}
]
],
'services' : GetExampleServicesDict()
}
get_service_expected_result = {
@ -4056,7 +4135,7 @@ class TestClientAPI( unittest.TestCase ):
metadata.append( metadata_row )
expected_identifier_result = { 'metadata' : metadata }
expected_identifier_result = { 'metadata' : metadata, 'services' : GetExampleServicesDict() }
media_results = []
file_info_managers = []
@ -4287,10 +4366,10 @@ class TestClientAPI( unittest.TestCase ):
with_notes_metadata.append( with_notes_metadata_row )
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 }
expected_metadata_result = { 'metadata' : metadata, 'services' : GetExampleServicesDict() }
expected_detailed_known_urls_metadata_result = { 'metadata' : detailed_known_urls_metadata, 'services' : GetExampleServicesDict() }
expected_notes_metadata_result = { 'metadata' : with_notes_metadata, 'services' : GetExampleServicesDict() }
expected_only_return_basic_information_result = { 'metadata' : only_return_basic_information_metadata, 'services' : GetExampleServicesDict() }
HG.test_controller.SetRead( 'hash_ids_to_hashes', file_ids_to_hashes )
HG.test_controller.SetRead( 'media_results', media_results )
@ -4386,7 +4465,7 @@ class TestClientAPI( unittest.TestCase ):
d = json.loads( text )
expected_result = { 'metadata' : list( expected_only_return_basic_information_result[ 'metadata' ] ) }
expected_result = { 'metadata' : list( expected_only_return_basic_information_result[ 'metadata' ] ), 'services' : GetExampleServicesDict() }
expected_result[ 'metadata' ].sort( key = lambda basic: expected_order.index( basic[ 'file_id' ] ) )
@ -4478,7 +4557,7 @@ class TestClientAPI( unittest.TestCase ):
self.maxDiff = None
'''
expected_result = { 'metadata' : list( expected_metadata_result[ 'metadata' ] ) }
expected_result = { 'metadata' : list( expected_metadata_result[ 'metadata' ] ), 'services' : GetExampleServicesDict() }
expected_result[ 'metadata' ].sort( key = lambda basic: expected_order.index( basic[ 'file_id' ] ) )
@ -4538,7 +4617,7 @@ class TestClientAPI( unittest.TestCase ):
d = json.loads( text )
expected_result = { 'metadata' : list( expected_identifier_result[ 'metadata' ] ) }
expected_result = { 'metadata' : list( expected_identifier_result[ 'metadata' ] ), 'services' : GetExampleServicesDict() }
expected_result[ 'metadata' ].sort( key = lambda basic: expected_order.index( basic[ 'file_id' ] ) )
@ -4580,7 +4659,7 @@ class TestClientAPI( unittest.TestCase ):
d = json.loads( text )
expected_result = { 'metadata' : list( expected_only_return_basic_information_result[ 'metadata' ] ) }
expected_result = { 'metadata' : list( expected_only_return_basic_information_result[ 'metadata' ] ), 'services' : GetExampleServicesDict() }
expected_result[ 'metadata' ].sort( key = lambda basic: expected_order.index( basic[ 'file_id' ] ) )
@ -4620,7 +4699,7 @@ class TestClientAPI( unittest.TestCase ):
d = json.loads( text )
expected_result = { 'metadata' : list( expected_metadata_result[ 'metadata' ] ) }
expected_result = { 'metadata' : list( expected_metadata_result[ 'metadata' ] ), 'services' : GetExampleServicesDict() }
expected_result[ 'metadata' ].sort( key = lambda basic: expected_order.index( basic[ 'file_id' ] ) )
@ -4721,7 +4800,7 @@ class TestClientAPI( unittest.TestCase ):
expected_result[ 'metadata' ].sort( key = lambda m_dict: hashes_in_test.index( bytes.fromhex( m_dict[ 'hash' ] ) ) )
path = '/get_files/file_metadata?hashes={}'.format( urllib.parse.quote( json.dumps( [ hash.hex() for hash in hashes_in_test ] ) ) )
path = '/get_files/file_metadata?hashes={}&include_services_object=false'.format( urllib.parse.quote( json.dumps( [ hash.hex() for hash in hashes_in_test ] ) ) )
connection.request( 'GET', path, headers = headers )

View File

@ -232,6 +232,10 @@ class Controller( object ):
self._param_read_responses = {}
self.example_like_rating_service_key = LOCAL_RATING_LIKE_SERVICE_KEY
self.example_numerical_rating_service_key = LOCAL_RATING_NUMERICAL_SERVICE_KEY
self.example_incdec_rating_service_key = LOCAL_RATING_INCDEC_SERVICE_KEY
self.example_file_repo_service_key_1 = HydrusData.GenerateKey()
self.example_file_repo_service_key_2 = HydrusData.GenerateKey()
self.example_tag_repo_service_key = HydrusData.GenerateKey()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB