Version 521

This commit is contained in:
Hydrus Network Developer 2023-03-22 15:28:10 -05:00
parent f079db3fa3
commit 7bd42868c5
No known key found for this signature in database
GPG Key ID: 76249F053212133C
45 changed files with 2973 additions and 2786 deletions

View File

@ -7,6 +7,46 @@ title: Changelog
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
## [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'
## [Version 520](https://github.com/hydrusnetwork/hydrus/releases/tag/v520)
### autocomplete
@ -361,83 +401,3 @@ title: Changelog
* I added a couple unit tests for the new .txt sidecar separator
* fixed a bad sidecar unit test
* 'client_running' and 'server_running' are now in the .gitignore
## [Version 511](https://github.com/hydrusnetwork/hydrus/releases/tag/v511)
### thumbnail UI scaling
* thumbnails can finally look good at high UI scales! a new setting in _options->thumbnails_, 'Thumbnail UI scale supersampling %', lets you tell hydrus to generate thumbnails at a particular UI scale. match it to your monitor, and your thumbnails should regenerate to look crisp
* some users have complicated multi-monitor setups, or they change their UI scale regularly, so I'm not auto-setting this _yet_. let me know how it goes
* sadly <100% for super-crunchy-mode doesn't work
### unnamespaced search tags
* _I am not really happy with this solution, since it doesn't neatly restore the old behaviour, but it does make things easier in the new system and I've fixed a related bug_
* a new option in _services->manage tag display and search_, 'Unnamespaced input gives (any namespace) wildcard results', now lets you quickly search `*:sam*` by typing `sam`
* fixed an issue where an autocomplete input with a total wildcard namespace, like `*:sam` was not matching to unnamespaced tags when preparing the list of tag results
* wildcards with `*` namespace now have a special `(any namespace)` suffix, and they show with unnamespaced namespace colour
### misc
* fixed the client-server communication problem related to last week's SerialisableDictionary update. I messed up and forgot this object is used in network comms, which meant >=v510 clients couldn't talk to a <=509 server and _vice versa_ version swaps. now the server always kicks out an old SerialisableDictionary serialisation. I plan to remove the patch in 26 weeks, giving us more buffer time for users to update naturally
* the recent option to turn off mouse-scroll-changes-menu-button-value is improved--now the wheel event is correctly passed up to the parent panel, so you'll scroll right through one of these buttons, not halt on it. the file sort control now also obeys this option
* if you try to zoom a media in so that its virtual size would be >32,000px on a side, the canvas now zooms to 32k exactly. this is the max allowed zoom for technical reasons atm (I'll fix it in a future rewrite). this also fixes the 'zoom max' command, which previously would make no action if the max zoom created a virtual canvas bigger than this. also, 'zoom max' is now shown on the media viewer right-click menu
* the 'max zoom' dimension for mpv windows and my native animation window is now 8k. seems like there are smaller technical limits for mpv, and my animation window isn't tiled, so this is to be extra safe for now
* fixed a bug where it was possible to send the 'undelete file' signal to a file that was physically deleted (and therefore viewed in a special 'deleted files' domain). the file would obediently return to its original local file service and then throw 'missing file' warnings when the thumb tried to show. now these files are discarded from undelete consideration
* if you are looking at physically deleted files, the thumbnail view now provides a 'clear deletion record' menu action! this is the same command as the button in _services->review services->all local files_, but just on the selection
* fixed several taglists across the program that were displaying tags in the wrong display context and/or not sorting correctly. this mostly went wrong by setting sorted storage taglists (which normally show sibling/parent flare) as unsorted display taglists
* file lookup script tag suggestions (as fetched from some external source) are now set to be sorted
### file import options pre-import checking
* _this stuff is advanced users only. normal users can rest assured that the way the client skips downloads for 'already in db/previously deleted' files now has fewer false negatives and false positives_
* the awkwardly named advanced 'do not check url/hash to see if file already in db/previously deleted' checkboxes in file import options have been overhauled. now they are phrased in the positive ("check x to determine aid/pd?") and offer 'do not check', 'check', and the new 'check - and matches are dispositive'. the tooltip has been updated to talk about what they do. 'dispositive' basically means 'if this one hits, trust it over the other', and by default the 'hash' check remains dispositive over the URLs (this was previously hardcoded, now you can choose urls to rule in some cases).
* there is also a new checkbox to optionally disable a component of the url checking that looks at neighbouring urls on the same file to determine url-mapping trustworthiness. this will solve or help explore some weird multi-url-mapping situations
* also, novel SHA256 hashes no longer count as 'matches', just like a novel MD5 hash would not. this helps keep useful dispositive behaviour for known hashes but also automatically defers to urls when a site is being CDN-optimised and transfer hashes are different to api-reported ones. this fixes some watchers that have been using excess bandwidth on repeated downloads
* fixed several problems with the url-lookup logic, particularly with the method that checks for 'file-neighbour' urls (simply, when a file-url match should be distrusted because that file has multiple urls of the same url class). it was also too aggressive on file/unknown url classes, which can legitimately have tokenised neighbours, and getting confused by http/https dupes
* the neighbour test now remembers untrustworthy domains across different url checks for a file, which helps some subsequent direct-file-url checks where neighbours aren't a marker of file-url mapping reliability
* the overall logic behind the hash and url lookup is cleaned up significantly
* if you are an advanced user who has been working with me on this stuff, let me know how it goes. we erected this rats' nest through years of patches, and now I have cleaned it out. I'm confident it works better overall, but I may have missed one of your complicated situations. at the least, these new options should help us figure out quicker fixes in future
### boring code cleanup
* removed some old 'subject_identifier' arg parsing from various account-modification calls in the server code. as previously planned, for simplicity and security, the only identifier for these actions is now 'subject_account_key', and subject_identifier is only used for account lookups
* improved the error handling around serialised object loading. the messages explain what happened and state object type and the versions involved
* cleaned up some tag sort code
* cleaned up how advanced file delete content updates work
* fixed yet another duplicate potentials count unit test that was sometimes failing due to complex count perspective
## [Version 510](https://github.com/hydrusnetwork/hydrus/releases/tag/v510)
### notes
* duplicate metadata merge options now supports note merging. you can copy from worse to better or in both directions, with a couple extra conflict-resolution options that are a subset of note import options and have reasonable defaults.
* the default note merge options are to go from worse to better for 'set as better' and both directions for 'they are the same', renaming notes on conflicts. **your existing duplicate metadata merge options will receive these settings on update, so if you don't want this, update your settings from the duplicate filter page**
* the manage notes dialog gets copy and paste buttons. these will copy all the current notes and paste them to another instance of the panel, using the default (extend if possible, otherwise rename) conflict resolution rules
* if an automatic system like a parser gives a note text that already exists on the file, the Note Import Options now discards it in all cases, no matter the names involved. no more automatic dupes!
* ADVANCED: note import options (and related note add/merge operations that use it) now scan all prefix-matching note names for 'new note is already in file' and 'new note is an extension of a note already in file' tests. this improves a former fix to the 'successive parses of two sites with the same note name but different note text cause one of them to be dupe-added as (2), (3), (4), renames etc...' bug. the initial (1) rename will be scanned and recognised as 'already in file' and ignored or now extended as the settings say, just as if the desired name were hit. thanks to the reports here--I missed the logic the first time around
* it would be nice to have 'manage notes' for multiple files at once--this is still a future goal
### notes client api
* the `/add_notes/set_notes` now takes some new parameters if you want to apply the adapted Note Import Options merge logic rather than figure out renames and extensions yourself
* `/add_notes/set_notes` now returns the changes it made, which in the new mode may not be exactly what you instructed
* added unit tests and help to reflect the above
* client api version is now 38
### misc
* I fixed up how shift/ctrl/drag selection works on taglists. like with the recent thumbnail selection update, you can now 'undo' a shift-select with subsequent clicks or 'drag undo', and the list remembers what _was_ selected beforehand. ctrl-shift-select is also a more reliable 'deselect range'. both mouse drag selection and ctrl-drag selection use this logic, have fewer index bugs, and the ctrl-drag now chooses at the start whether this drag will be selection or deselection based on your initial click that started the drag. have a play with it--overall it just feels better now
* the 'file log' menu now shows a 'reverse' command, which reverses all the imports in the log. if you want to import from oldest to newest with a typical booru, just start your downloader with file imports paused (check the cog icon), and then allow the gallery search to fully populate the list as normaly. once done, hit this new reverse and then unpause the files, and you should be good
* any image files or thumbnails that are completely transparent and have a non-completely-black image now have their alpha channel stripped, just like files that are completely opaque. I believe the instances where this is a mistake outweigh the instances where it is legit, but let me know how we get on--maybe there are some weird mid-gif thumbs or something where this misfires. in the same thing, I reverted the 'psd thumbnails now have no transparency' change from last week. the issue where ffmpeg was sometimes being confused about psd layer masks from earlier should be fixed while letting legit transparency work correctly. the ultimate fix here will be to roll imagemagick into the program, which I am now planning and will start 'running from source' experiments with soon
* the three 'additional fixed time...' settings in _options->downloading_ now have a max value of 3600, for extreme situation testing
### boring code cleanup
* updated my serialisabledict/list objects again--they can now handle bytes objects in any position. I will slowly migrate my existing hardcoded bytes serialisation and the old serialisablebytesdict to these freshly flexible classes
* for clarity, across the code, renamed 'duplicate action options' to 'duplicate content merge options'
* refactored duplicate content merge options initialisation, clearing the stuffed init and totuple to nicer get/set
* broke apart how NoteImportOptions does its main note filtering for easier low-level access
* cleaned a ton of note import options code up. the logic here was not great, now it is a bit tidier
* undid whatever nonsense I was doing with taglist ctrl-drag-selection and cleaned up the main click and drag event handling along with its index calculation and 'what was clicked last time' record
* fixed numerous weird logical/position index issues with the taglist and clicking/dragging

View File

@ -0,0 +1,60 @@
---
title: More Tags
---
# Tags Can Get Complicated
Tags are powerful, and there are many tools within hydrus to customise how they apply and display. I recommend you play around with the basics before making your own new local tag services or jumping right into the PTR, so take it slow.
## Tag services
Hydrus lets you organise tags across multiple separate 'services'. By default there are two, but you can have however many you want (`services->manage services`). You might like to add more for different sets of siblings/parents, tags you don't want to see but still search by, parsing tags into different services based on reliability of the source or the source itself. You could for example parse all tags from Pixiv into one service, Danbooru tags into another, Deviantart etc. and so on as you chose. You must always have at least one local tag service.
Local tag services are stored only on your hard drive--they are completely private. No tags, siblings, or parents will accidentally leak, so feel free to go wild with whatever odd scheme you want to try out.
Each tag service comes with its own tags, siblings and parents.
### My tags
The intent is to use this service for tags you yourself want to add.
### Downloader tags
The default [tag parse target](getting_started_downloading.md#parsing). Tags of things you download will end up here unless you change the settings. It's probably a good idea to set up some tag blacklists for tags you don't want.
## 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.
Tag repos store many file->tag relationships. Anyone who has an access key to the repository can sync with it and hence download all these relationships. If any of their own files match up, they will get those tags. Access keys will also usually have permission to upload new tags and ask for incorrect ones to be deleted.
Anyone can run a tag repository, but it is a bit complicated for new users. I ran a public tag repository for a long time, and now this large central store is run by users. It has over a billion tags and is free to access and contribute to.
To connect with it, please check [here](access_keys.md). **Please read that page if you want to try out the PTR. It is only appropriate for someone on an SSD!**
If you add it, your client will download updates from the repository over time and, usually when it is idle or shutting down, 'process' them into its database until it is fully synchronised. The processing step is CPU and HDD heavy, and you can customise when it happens in _file->options->maintenance and processing_. As the repository synchronises, you should see some new tags appear, particularly on famous files that lots of people have.
You can watch more detailed synchronisation progress in the _services->review services_ window.
![](images/review_repos_public_tag_repo.png)
Your new service should now be listed on the left of the manage tags dialog. Adding tags to a repository works very similarly to the 'my tags' service except hitting 'apply' will not immediately confirm your changes--it will put them in a queue to be uploaded. These 'pending' tags will be counted with a plus '+' or minus '-' sign:
[![](images/rlm_pending.png)](images/rlm_pending.png)
Notice that a 'pending' menu has appeared on the main window. This lets you start the upload when you are ready and happy with everything that you have queued.
When you upload your pending tags, they will commit and look to you like any other tag. The tag repository will anonymously bundle them into the next update, which everyone else will download in a day or so. They will see your tags just like you saw theirs.
If you attempt to remove a tag that has been uploaded, you may be prompted to give a reason, creating a petition that a janitor for the repository will review.
I recommend you not spam tags to the public tag repo until you get a rough feel for the [guidelines](https://github.com/CuddleBear92/Hydrus-Presets-and-Scripts/blob/master/tag%20guidelines), and my original [tag schema](tagging_schema.html) thoughts, or just lurk until you get the idea. It roughly follows what you will see on a typical booru. The general rule is to only add factual tags--no subjective opinion.
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)
## Siblings and parents
For more in-depth information, see [siblings](advanced_siblings.md) and [parents](advanced_parents.md).
tl;dr: Siblings rename/alias tags in an undoable way. Parents virtually add/imply one or more tags (parents) if the 'child' tag is present. The PTR has a _lot_ of them.
### Display rules
If you go to `tags -> manage where siblings and parents apply` you'll get a window where you can customise where and in what order siblings and parents apply. The service at the top of the list has precedence over all else, then second, and so on depending on how many you have. If you for example have PTR you can use a tag service to overwrite tags/siblings for cases where you disagree with the PTR standards.

View File

@ -3,10 +3,10 @@ title: Searching and Sorting
---
# Searching and sorting
The primary purpose of tags is to be able to find what you've tagged again. For this we have the search feature of Hydrus.
The primary purpose of tags is to be able to find what you've tagged again. Let's see more how it works.
## Searching
Just open a new search page (`pages > new file search page` or <kbd>Ctrl+T</kbd> `> file search`) and start typing away in the search field which should be focused when you first open the page.
Just open a new search page (`pages > new file search page` or <kbd>Ctrl+T</kbd> `> file search`) and start typing in the search field which should be focused when you first open the page.
### The dropdown controls
@ -14,17 +14,17 @@ Let's look at the tag autocomplete dropdown:
![](images/ac_dropdown.png)
* **favourite searches star**
* **system predicates**
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.
Hydrus calls search terms _predicates_. 'system predicates', which search metadata other than simple tags, show on any search page with an empty autocomplete input. You can mix them into any search alongside tags. They are very useful, so try them out!
* **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`.
Turn these on and off to control whether tag _predicates_ apply to tags that exist, or 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.
This controls whether a change to the list of current search predicates 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](getting_started_searching.md#or_searching)**
@ -32,15 +32,20 @@ Let's look at the tag autocomplete dropdown:
* **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 combination is fine, but as you use the client more, you may want to access different search 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 known tag service. For most purposes, this combination is fine, but as you use the client more, you will sometimes 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'.
Turning on 'advanced mode' gives access to more search domains. Some of them are subtly complicated, run extremely slowly, and only useful for clever jobs--most of the time, you still want 'my files' and 'all known tags'.
* **favourite searches star**
Once you are more experienced, have a play with this. It lets you save your common searches for future, so you don't have to either keep re-entering them or keep them open all the time. If you close big things down when you aren't using them, you will keep your client lightweight and save time.
Hydrus will treat a space the same way as an underscore when searching so the query `character:samus aran` will find files tagged with `character:samus aran` and `character:samus_aran`.
When you type a tag in a search page, Hydrus will treat a space the same way as an underscore. Searching `character:samus aran` will find files tagged with `character:samus aran` and `character:samus_aran`. This is true of some other syntax characters, `[](){}/\"'-`, too.
Tags will be searchable by all their [siblings](/advanced_siblings.md). If there's a sibling for `large` -> `huge` then the query `large` will find files tagged with either and so will a search for `huge`. This goes for the whole sibling chain, no matter how deep or a tag's position in it.
Tags will be searchable by all their [siblings](advanced_siblings.md). If there's a sibling for `large` -> `huge` then typing `large` will provide `huge` as a suggestion. This goes for the whole sibling chain, no matter how deep or a tag's position in it.
### Wildcards
@ -62,9 +67,6 @@ 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`.
### System predicates
Tags are intended to tell you about content in the file while system predicates on the other hand deals with the files themselves for the most part: How big a file is, resolution, number of pixels, sound or no sound, number of tags assigned to the file, time imported, and quite a few other things. System predicates are the things prefixed with `system:` in the window that appear when you click in the search box.
## 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. For example the query `red eyes` **AND** `green eyes` (aka what you get if you enter each tag by itself) will only find files that has both tags. While the query `red eyes` **OR** `green eyes` will present you with files that are tagged with red eyes or green eyes, or both.

View File

@ -7,15 +7,17 @@ A _tag_ is a small bit of text describing a single property of something. They m
## How do we find files? { id="intro" }
So, you have stored some media in your database. Everything is hashed and cached. You can search by inbox and resolution and size and so on, but if you really want to find what we are looking for, you will have to use _tags_.
So, you have some files imported. Let's give them some tags so we can find them again later.
[FAQ: what is a tag?](faq.md#tags)
Your client starts with two [local tags services](getting_started_tags.md#tag_services), called 'my tags' and 'downloader tags' which keeps all of its file->tag mappings in your client's database where only you can see them. It is a good place to practise. So, select a file and press F3:
Your client starts with two [local tags services](getting_started_tags.md#tag_services), called 'my tags' and 'downloader tags' which keep all of their file->tag mappings in your client's database where only you can see them. 'my tags' is a good place to practise.
Select a file and press F3:
[![](images/sororitas_local.png)](images/sororitas_local.png)
The autocomplete dropdown in the manage tags dialog works very like the one in a normal search page--you type part of a tag, and matching results will appear below. You select the tag you want with the arrow keys and hit enter. Since your 'my tags' service doesn't have any tags in it yet, you won't get any results here except the exact match of what you typed. If you want to remove a tag, enter the exact same thing again or double-click it in the box above.
The area below where you type is the 'autocomplete dropdown'. You will see this on normal search pages too. Type part of a tag, and matching results will appear below. Since you are starting out, your 'my tags' service won't have many tags in it yet, but things will populate fast! Select the tag you want with the arrow keys and hit enter. If you want to remove a tag, enter the exact same thing again or double-click it in the box above.
Prefixing a tag with a category and a colon will create a [_namespaced_ tag](faq.md#namespaces). This helps inform the software and other users about what the tag is. Examples of namespaced tags are:
@ -26,71 +28,16 @@ Prefixing a tag with a category and a colon will create a [_namespaced_ tag](faq
The client is set up to draw common namespaces in different colours, just like boorus do. You can change these colours in the options.
Once you are happy with your tags, hit 'apply' or just press enter on the text box if it is empty.
Once you are happy with your tags, hit 'apply' or just press enter on the text box when it is empty.
[![](images/sororitas_local_done.png)](images/sororitas_local_done.png)
The tags are now saved to your database. Searching for any of them will return this file and anything else so tagged:
The tags are now saved to your database. On any search page, you can now type one in and be able to find everything with that tag::
[![](images/sororitas_search.png)](images/sororitas_search.png)
If you add more tags or system predicates to a search, you will limit the results to those files that match every single one:
If you add more 'search predicates' to a search, you will limit the results to those files that match every single one:
[![](images/sororitas_hanako.png)](images/sororitas_hanako.png)
You can also exclude a tag by prefixing it with a hyphen (e.g. `-heresy`).
## Siblings and parents
For more in-depth information about them see [siblings](advanced_siblings.md) and [parents](advanced_parents.md).
tl;dr is that siblings lets you chain together different tags that mean the same thing, top sibling in the chain decides what it will look like in lists.
Parents lets you virtually add one or more tags (parents) if the 'child' tag is present.
## Tag services
Hydrus uses services to let you organise tags into various groups as you chose. By default there are two, but you can have however many you want. Some uses are different sets of siblings/parents, tags you don't want to see but still search by, parsing tags into different services based on reliability of the source or the source itself. You could for example parse all tags from Pixiv into one service, Danbooru tags into another, Deviantart etc. and so on as you chose.
You are however unable to have less than one.
Services are always local. No tags will accidentally leak or anything like that nor will siblings and parents. So feel free to go wild with whatever odd scheme you want to try out.
Each tag service comes with its own tags, siblings and parents.
### Display rules
If you go to `tags -> manage where siblings and parents apply` you'll get a window where you can customise where and in what order siblings and parents apply. The service at the top of the list has precedence over all else, then second, and so on depending on how many you have. If you for example have PTR you can use a tag service to overwrite tags/siblings for cases where you disagree with the PTR standards.
### My tags
The intent is to use this service for tags you yourself want to add.
### Downloader tags
The default [tag parse target](getting_started_downloading.md#parsing). Tags of things you download will end up here unless you change the settings. It's probably a good idea to set up some tag blacklists for tags you don't want.
## 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.
Tag repos store many file->tag relationships. Anyone who has an access key to the repository can sync with it and hence download all these relationships. If any of their own files match up, they will get those tags. Access keys will also usually have permission to upload new tags and ask for incorrect ones to be deleted.
Anyone can run a tag repository, but it is a bit complicated for new users. I ran a public tag repository for a long time, and now this large central store is run by users. It has over a billion tags and is free to access and contribute to.
To connect with it, please check [here](access_keys.md). **Please read that page if you want to try out the PTR. It is only appropriate for someone on an SSD!**
If you add it, your client will download updates from the repository over time and, usually when it is idle or shutting down, 'process' them into its database until it is fully synchronised. The processing step is CPU and HDD heavy, and you can customise when it happens in _file->options->maintenance and processing_. As the repository synchronises, you should see some new tags appear, particularly on famous files that lots of people have.
You can watch more detailed synchronisation progress in the _services->review services_ window.
![](images/review_repos_public_tag_repo.png)
Your new service should now be listed on the left of the manage tags dialog. Adding tags to a repository works very similarly to the 'my tags' service except hitting 'apply' will not immediately confirm your changes--it will put them in a queue to be uploaded. These 'pending' tags will be counted with a plus '+' or minus '-' sign:
[![](images/rlm_pending.png)](images/rlm_pending.png)
Notice that a 'pending' menu has appeared on the main window. This lets you start the upload when you are ready and happy with everything that you have queued.
When you upload your pending tags, they will commit and look to you like any other tag. The tag repository will anonymously bundle them into the next update, which everyone else will download in a day or so. They will see your tags just like you saw theirs.
If you attempt to remove a tag that has been uploaded, you may be prompted to give a reason, creating a petition that a janitor for the repository will review.
I recommend you not spam tags to the public tag repo until you get a rough feel for the [guidelines](https://github.com/CuddleBear92/Hydrus-Presets-and-Scripts/blob/master/tag%20guidelines), and my original [tag schema](tagging_schema.html) thoughts, or just lurk until you get the idea. It roughly follows what you will see on a typical booru. The general rule is to only add factual tags--no subjective opinion.
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)

View File

@ -34,6 +34,39 @@
<div class="content">
<h1 id="changelog"><a href="#changelog">changelog</a></h1>
<ul>
<li>
<h2 id="version_521"><a href="#version_521">version 521</a></h2>
<ul>
<li><h3>some tag presentation</h3></li>
<li>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</li>
<li>you can also set the ' OR ' connector text</li>
<li>and you can set the OR connector's 'namespace' colour. it was 'system' before</li>
<li>also turned off the new namespace colour fading for OR predicates, where it was unintentionally kicking in and looking horrible lol</li>
<li><h3>misc</h3></li>
<li>added a checkbox to 'file viewing statistcs' to turn off tracking for the archive/delete filter, if you don't like that</li>
<li>file viewing statistics now maxes out at five times a duration-having media's duration, if that is more than your max view time</li>
<li>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) </li>
<li>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</li>
<li>I might have fixed a height-layout bug in the petition management page</li>
<li><h3>advanced change to unnamespaced tags and their parsing</h3></li>
<li>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</li>
<li>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'</li>
<li>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</li>
<li>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</li>
<li>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'</li>
<li><h3>advanced file domain and file import options stuff</h3></li>
<li>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)</li>
<li>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)</li>
<li>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</li>
<li>also fixed the file domain button getting stuck on 'initialising' if it starts with an empty file domain</li>
<li>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</li>
<li><h3>boring cleanup</h3></li>
<li>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</li>
<li>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</li>
<li>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</li>
<li>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'</li>
</ul>
</li>
<li>
<h2 id="version_520"><a href="#version_520">version 520</a></h2>
<ul>

View File

@ -13,13 +13,15 @@ CAN_HIDE_MOUSE = True
CANVAS_MEDIA_VIEWER = 0
CANVAS_PREVIEW = 1
CANVAS_MEDIA_VIEWER_DUPLICATES = 2
CANVAS_MEDIA_VIEWER_ARCHIVE_DELETE = 3
CANVAS_MEDIA_VIEWER_TYPES = { CANVAS_MEDIA_VIEWER, CANVAS_MEDIA_VIEWER_DUPLICATES }
CANVAS_MEDIA_VIEWER_TYPES = { CANVAS_MEDIA_VIEWER, CANVAS_MEDIA_VIEWER_DUPLICATES, CANVAS_MEDIA_VIEWER_ARCHIVE_DELETE }
canvas_type_str_lookup = {
CANVAS_MEDIA_VIEWER : 'media viewer',
CANVAS_PREVIEW : 'preview',
CANVAS_MEDIA_VIEWER_DUPLICATES : 'duplicates filter'
CANVAS_MEDIA_VIEWER_DUPLICATES : 'duplicates filter',
CANVAS_MEDIA_VIEWER_ARCHIVE_DELETE : 'archive/delete filter'
}
# Hue is generally 200, Sat and Lum changes based on need

View File

@ -589,7 +589,7 @@ class ClientFilesManager( object ):
hash = media.GetHash()
mime = media.GetMime()
( width, height ) = media.GetResolution()
duration = media.GetDuration()
duration = media.GetDurationMS()
num_frames = media.GetNumFrames()
bounding_dimensions = self._controller.options[ 'thumbnail_dimensions' ]
@ -2124,7 +2124,7 @@ class FilesMaintenanceManager( object ):
return None
duration = media_result.GetDuration()
duration = media_result.GetDurationMS()
if duration is not None:

View File

@ -10,6 +10,7 @@ from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
from hydrus.client.media import ClientMedia
# now let's fill out grandparents
def BuildServiceKeysToChildrenToParents( service_keys_to_simple_children_to_parents ):
@ -253,10 +254,13 @@ class FileViewingStatsManager( object ):
self._my_flush_job = self._controller.CallRepeating( 5, 60, self.REPEATINGFlush )
def _GenerateViewsRow( self, canvas_type, view_timestamp, viewtime_delta ):
def _GenerateViewsRow( self, media: ClientMedia.Media, canvas_type: int, view_timestamp: int, viewtime_delta: int ):
new_options = HG.client_controller.new_options
viewtime_min = None
viewtime_max = None
result_views_delta = 0
result_viewtime_delta = 0
@ -274,11 +278,21 @@ class FileViewingStatsManager( object ):
if canvas_type == CC.CANVAS_MEDIA_VIEWER_DUPLICATES and not new_options.GetBoolean( 'file_viewing_statistics_active_on_dupe_filter' ):
canvas_type = CC.CANVAS_MEDIA_VIEWER
do_it = False
elif canvas_type == CC.CANVAS_MEDIA_VIEWER_ARCHIVE_DELETE and not new_options.GetBoolean( 'file_viewing_statistics_active_on_archive_delete_filter' ):
do_it = False
canvas_type = CC.CANVAS_MEDIA_VIEWER
if media.HasDuration() and viewtime_max is not None:
# if user is watching a long vid, save that whole time mate
viewtime_max = max( viewtime_max, ( media.GetDurationMS() / 1000 ) * 5 )
if do_it:
@ -347,16 +361,18 @@ class FileViewingStatsManager( object ):
def FinishViewing( self, hash, canvas_type, view_timestamp, viewtime_delta ):
def FinishViewing( self, media: ClientMedia.MediaSingleton, canvas_type, view_timestamp, viewtime_delta ):
if not HG.client_controller.new_options.GetBoolean( 'file_viewing_statistics_active' ):
return
hash = media.GetHash()
with self._lock:
( canvas_type, row ) = self._GenerateViewsRow( canvas_type, view_timestamp, viewtime_delta )
( canvas_type, row ) = self._GenerateViewsRow( media, canvas_type, view_timestamp, viewtime_delta )
if not self._RowMakesChanges( row ):

View File

@ -212,6 +212,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'booleans' ][ 'delete_files_after_export' ] = False
self._dictionary[ 'booleans' ][ 'file_viewing_statistics_active' ] = True
self._dictionary[ 'booleans' ][ 'file_viewing_statistics_active_on_archive_delete_filter' ] = True
self._dictionary[ 'booleans' ][ 'file_viewing_statistics_active_on_dupe_filter' ] = False
self._dictionary[ 'booleans' ][ 'prefix_hash_when_copying' ] = False
@ -279,6 +280,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'booleans' ][ 'focus_preview_on_shift_click_only_static' ] = False
self._dictionary[ 'booleans' ][ 'fade_sibling_connector' ] = True
self._dictionary[ 'booleans' ][ 'use_custom_sibling_connector_colour' ] = False
from hydrus.client.gui.canvas import ClientGUIMPV
@ -555,12 +557,15 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'noneable_strings' ][ 'qt_stylesheet_name' ] = None
self._dictionary[ 'noneable_strings' ][ 'last_advanced_file_deletion_reason' ] = None
self._dictionary[ 'noneable_strings' ][ 'last_advanced_file_deletion_special_action' ] = None
self._dictionary[ 'noneable_strings' ][ 'sibling_connector_custom_namespace_colour' ] = 'system'
self._dictionary[ 'noneable_strings' ][ 'or_connector_custom_namespace_colour' ] = 'system'
self._dictionary[ 'strings' ] = {}
self._dictionary[ 'strings' ][ 'app_display_name' ] = 'hydrus client'
self._dictionary[ 'strings' ][ 'namespace_connector' ] = ':'
self._dictionary[ 'strings' ][ 'sibling_connector' ] = ' \u2192 '
self._dictionary[ 'strings' ][ 'or_connector' ] = ' OR '
self._dictionary[ 'strings' ][ 'export_phrase' ] = '{hash}'
self._dictionary[ 'strings' ][ 'current_colourset' ] = 'default'
self._dictionary[ 'strings' ][ 'favourite_simple_downloader_formula' ] = 'all files linked by images in page'
@ -723,6 +728,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
quiet_file_import_options.SetPreImportURLCheckLooksForNeighbours( preimport_url_check_looks_for_neighbours )
quiet_file_import_options.SetPostImportOptions( automatic_archive, associate_primary_urls, associate_source_urls )
quiet_file_import_options.SetPresentationImportOptions( presentation_import_options )
quiet_file_import_options.SetDestinationLocationContext( ClientLocation.LocationContext.STATICCreateSimple( CC.LOCAL_FILE_SERVICE_KEY ) )
self._dictionary[ 'default_file_import_options' ][ 'quiet' ] = quiet_file_import_options
@ -731,6 +737,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
loud_file_import_options.SetPreImportOptions( exclude_deleted, preimport_hash_check_type, preimport_url_check_type, allow_decompression_bombs, min_size, max_size, max_gif_size, min_resolution, max_resolution )
loud_file_import_options.SetPreImportURLCheckLooksForNeighbours( preimport_url_check_looks_for_neighbours )
loud_file_import_options.SetPostImportOptions( automatic_archive, associate_primary_urls, associate_source_urls )
loud_file_import_options.SetDestinationLocationContext( ClientLocation.LocationContext.STATICCreateSimple( CC.LOCAL_FILE_SERVICE_KEY ) )
self._dictionary[ 'default_file_import_options' ][ 'loud' ] = loud_file_import_options

View File

@ -71,13 +71,31 @@ def ConvertParseResultToPrettyString( result ):
try:
tag = HydrusTags.CleanTag( HydrusTags.CombineTag( additional_info, parsed_text ) )
if additional_info is None:
combined_tag = parsed_text
else:
combined_tag = HydrusTags.CombineTag( additional_info, parsed_text, do_not_double_namespace = True )
tag = HydrusTags.CleanTag( combined_tag )
except:
tag = 'unparsable tag, will likely be discarded'
try:
HydrusTags.CheckTagNotEmpty( tag )
except HydrusExceptions.TagSizeException:
tag = 'empty tag, will be discarded'
return 'tag: ' + tag
elif content_type == HC.CONTENT_TYPE_NOTES:
@ -179,13 +197,18 @@ def ConvertParsableContentToPrettyString( parsable_content, include_veto = False
elif content_type == HC.CONTENT_TYPE_MAPPINGS:
namespaces = [ namespace for namespace in additional_infos if namespace != '' ]
namespaces = [ namespace for namespace in additional_infos if namespace not in ( '', None ) ]
if '' in additional_infos:
namespaces.append( 'unnamespaced' )
if None in additional_infos:
namespaces.append( 'any namespace' )
pretty_strings.append( 'tags: ' + ', '.join( namespaces ) )
elif content_type == HC.CONTENT_TYPE_NOTES:
@ -381,7 +404,16 @@ def GetNamespacesFromParsableContent( parsable_content ):
content_type_to_additional_infos = HydrusData.BuildKeyToSetDict( ( ( content_type, additional_infos ) for ( name, content_type, additional_infos ) in parsable_content ) )
namespaces = content_type_to_additional_infos[ HC.CONTENT_TYPE_MAPPINGS ] # additional_infos is a set of namespaces
namespaces = set( content_type_to_additional_infos[ HC.CONTENT_TYPE_MAPPINGS ] ) # additional_infos is a set of namespaces
if None in namespaces:
namespaces.discard( None )
namespaces.add( '' )
namespaces = sorted( namespaces )
return namespaces
@ -445,7 +477,20 @@ def GetTagsFromParseResults( results ):
if content_type == HC.CONTENT_TYPE_MAPPINGS:
tag_results.append( HydrusTags.CombineTag( additional_info, parsed_text ) )
namespace = additional_info
make_no_namespace_changes = namespace is None
if make_no_namespace_changes:
combined_tag = parsed_text
else:
combined_tag = HydrusTags.CombineTag( namespace, parsed_text, do_not_double_namespace = True )
tag_results.append( combined_tag )
@ -1965,7 +2010,7 @@ class ContentParser( HydrusSerialisable.SerialisableBase ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_CONTENT_PARSER
SERIALISABLE_NAME = 'Content Parser'
SERIALISABLE_VERSION = 6
SERIALISABLE_VERSION = 7
def __init__( self, name = None, content_type = None, formula = None, additional_info = None ):
@ -1984,14 +2029,6 @@ class ContentParser( HydrusSerialisable.SerialisableBase ):
formula = ParseFormulaHTML()
if additional_info is None:
if content_type == HC.CONTENT_TYPE_MAPPINGS:
additional_info = ''
self._name = name
self._content_type = content_type
self._formula = formula
@ -2173,6 +2210,27 @@ class ContentParser( HydrusSerialisable.SerialisableBase ):
return ( 6, new_serialisable_info )
if version == 6:
( name, content_type, serialisable_formula, additional_info ) = old_serialisable_info
if content_type == HC.CONTENT_TYPE_MAPPINGS:
namespace = additional_info
if namespace == '':
namespace = None
additional_info = namespace
new_serialisable_info = ( name, content_type, serialisable_formula, additional_info )
return ( 7, new_serialisable_info )
def GetName( self ):

View File

@ -498,7 +498,7 @@ class RasterContainerVideo( RasterContainer ):
video_buffer_size = new_options.GetInteger( 'video_buffer_size' )
duration = self._media.GetDuration()
duration = self._media.GetDurationMS()
num_frames_in_video = self._media.GetNumFrames()
if duration is None or duration == 0:
@ -572,7 +572,7 @@ class RasterContainerVideo( RasterContainer ):
def THREADRender( self ):
mime = self._media.GetMime()
duration = self._media.GetDuration()
duration = self._media.GetDurationMS()
num_frames_in_video = self._media.GetNumFrames()
time.sleep( 0.00001 )
@ -748,7 +748,7 @@ class RasterContainerVideo( RasterContainer ):
def GetDuration( self, index ):
def GetDurationMS( self, index ):
if self._media.GetMime() == HC.IMAGE_GIF:

View File

@ -2050,25 +2050,26 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
if self._predicate_type == PREDICATE_TYPE_OR_CONTAINER:
texts_and_namespaces = []
or_connector = HG.client_controller.new_options.GetString( 'or_connector' )
or_connector_namespace = HG.client_controller.new_options.GetNoneableString( 'or_connector_custom_namespace_colour' )
if or_under_construction:
texts_and_namespaces.append( ( 'OR: ', 'system' ) )
texts_and_namespaces = []
for or_predicate in self._value:
texts_and_namespaces.append( ( or_predicate.ToString(), or_predicate.GetNamespace() ) )
texts_and_namespaces.append( ( or_predicate.ToString(), 'namespace', or_predicate.GetNamespace() ) )
texts_and_namespaces.append( ( ' OR ', 'system' ) )
texts_and_namespaces.append( ( or_connector, 'or', or_connector_namespace ) )
texts_and_namespaces = texts_and_namespaces[ : -1 ]
if not or_under_construction:
texts_and_namespaces = texts_and_namespaces[ : -1 ]
else:
texts_and_namespaces = [ ( self.ToString( render_for_user = render_for_user ), self.GetNamespace() ) ]
texts_and_namespaces = [ ( self.ToString( render_for_user = render_for_user ), 'namespace', self.GetNamespace() ) ]
return texts_and_namespaces
@ -2147,6 +2148,11 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
return False
def IsORPredicate( self ):
return self._predicate_type == PREDICATE_TYPE_OR_CONTAINER
def IsUIEditable( self, ideal_predicate: "Predicate" ) -> bool:
# bleh

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -552,6 +552,8 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
self._default_reason = default_reason
local_file_services = list( HG.client_controller.services_manager.GetServices( ( HC.LOCAL_FILE_DOMAIN, ) ) )
if suggested_file_service_key is None:
@ -649,6 +651,8 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
permitted_reason_choices.append( ( 'keep existing reason: {}'.format( self._existing_shared_file_deletion_reason ), self._existing_shared_file_deletion_reason ) )
selection_index = len( permitted_reason_choices ) - 1
custom_index = len( permitted_reason_choices )
@ -658,6 +662,8 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
permitted_reason_choices.append( ( '(all files have existing file deletion reasons and they differ): do not alter them.', self.SPECIAL_CHOICE_NO_REASON ) )
selection_index = len( permitted_reason_choices ) - 1
self._reason_radio = ClientGUICommon.BetterRadioBox( self._reason_panel, choices = permitted_reason_choices, vertical = True )
@ -767,7 +773,7 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
def _GetReason( self ):
if self._reason_panel.isEnabled():
if self._reason_panel.isVisible() and self._reason_panel.isEnabled():
reason = self._reason_radio.GetValue()
@ -782,7 +788,15 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
else:
reason = None
if self._all_files_have_existing_file_deletion_reasons or self._existing_shared_file_deletion_reason is not None:
# do not overwrite
reason = None
else:
reason = self._default_reason
return reason
@ -813,7 +827,7 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
if trashed_key in keys_to_hashes and combined_key in keys_to_hashes and keys_to_hashes[ trashed_key ] == keys_to_hashes[ combined_key ]:
del keys_to_hashes[ trashed_key ]
del keys_to_hashes[ combined_key ]
possible_file_service_keys_and_hashes = [ ( fsk, keys_to_hashes[ fsk ] ) for fsk in possible_file_service_keys if fsk in keys_to_hashes and len( keys_to_hashes[ fsk ] ) > 0 ]
@ -1150,16 +1164,23 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
if save_reason and HG.client_controller.new_options.GetBoolean( 'remember_last_advanced_file_deletion_reason' ):
if self._reason_radio.GetCurrentIndex() <= 0:
last_advanced_file_deletion_reason = None
else:
last_advanced_file_deletion_reason = reason
reasons_ok = self._reason_radio.isVisible() and self._reason_radio.isEnabled()
HG.client_controller.new_options.SetNoneableString( 'last_advanced_file_deletion_reason', last_advanced_file_deletion_reason )
user_selected_existing_or_make_no_change = reason == self._existing_shared_file_deletion_reason or reason is None
if reasons_ok and not user_selected_existing_or_make_no_change:
if self._reason_radio.GetCurrentIndex() <= 0:
last_advanced_file_deletion_reason = None
else:
last_advanced_file_deletion_reason = reason
HG.client_controller.new_options.SetNoneableString( 'last_advanced_file_deletion_reason', last_advanced_file_deletion_reason )
return ( involves_physical_delete, list_of_service_keys_to_content_updates )

View File

@ -969,13 +969,6 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._export_location = QP.DirPickerCtrl( self )
location_context = self._new_options.GetDefaultLocalLocationContext()
self._default_local_location_context = ClientGUILocation.LocationSearchContextButton( self, location_context )
self._default_local_location_context.setToolTip( 'This initialised into a bunch of dialogs across the program as a fallback. You can probably leave it alone forever, but if you delete or move away from \'my files\' as your main place to do work, please update it here.' )
self._default_local_location_context.SetOnlyImportableDomainsAllowed( True )
self._prefix_hash_when_copying = QW.QCheckBox( self )
self._prefix_hash_when_copying.setToolTip( 'If you often paste hashes into boorus, check this to automatically prefix with the type, like "md5:2496dabcbd69e3c56a5d8caabb7acde5".' )
@ -1067,7 +1060,6 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
rows = []
rows.append( ( 'Fallback local file search location: ', self._default_local_location_context ) )
rows.append( ( 'When copying file hashes, prefix with booru-friendly hash type: ', self._prefix_hash_when_copying ) )
rows.append( ( 'Confirm sending files to trash: ', self._confirm_trash ) )
rows.append( ( 'Confirm sending more than one file to archive or inbox: ', self._confirm_archive ) )
@ -1154,8 +1146,6 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
HC.options[ 'export_path' ] = HydrusPaths.ConvertAbsPathToPortablePath( self._export_location.GetPath() )
self._new_options.SetDefaultLocalLocationContext( self._default_local_location_context.GetValue() )
self._new_options.SetBoolean( 'prefix_hash_when_copying', self._prefix_hash_when_copying.isChecked() )
HC.options[ 'delete_to_recycle_bin' ] = self._delete_to_recycle_bin.isChecked()
@ -1191,11 +1181,16 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._new_options = HG.client_controller.new_options
self._file_viewing_statistics_active = QW.QCheckBox( self )
self._file_viewing_statistics_active_on_archive_delete_filter = QW.QCheckBox( self )
self._file_viewing_statistics_active_on_dupe_filter = QW.QCheckBox( self )
self._file_viewing_statistics_media_min_time = ClientGUICommon.NoneableSpinCtrl( self )
self._file_viewing_statistics_media_max_time = ClientGUICommon.NoneableSpinCtrl( self )
max_tt = 'If you view a file for a very long time, the amount of viewtime recorded is clipped to this. This stops an outrageous viewtime being saved because you left something open in the background. If the media you view has duration, like a video, the max viewtime is five times its length or this, whichever is larger.'
self._file_viewing_statistics_media_max_time.setToolTip( max_tt )
self._file_viewing_statistics_preview_min_time = ClientGUICommon.NoneableSpinCtrl( self )
self._file_viewing_statistics_preview_max_time = ClientGUICommon.NoneableSpinCtrl( self )
self._file_viewing_statistics_preview_max_time.setToolTip( max_tt )
self._file_viewing_stats_menu_display = ClientGUICommon.BetterChoice( self )
@ -1208,6 +1203,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
#
self._file_viewing_statistics_active.setChecked( self._new_options.GetBoolean( 'file_viewing_statistics_active' ) )
self._file_viewing_statistics_active_on_archive_delete_filter.setChecked( self._new_options.GetBoolean( 'file_viewing_statistics_active_on_archive_delete_filter' ) )
self._file_viewing_statistics_active_on_dupe_filter.setChecked( self._new_options.GetBoolean( 'file_viewing_statistics_active_on_dupe_filter' ) )
self._file_viewing_statistics_media_min_time.SetValue( self._new_options.GetNoneableInteger( 'file_viewing_statistics_media_min_time' ) )
self._file_viewing_statistics_media_max_time.SetValue( self._new_options.GetNoneableInteger( 'file_viewing_statistics_media_max_time' ) )
@ -1223,7 +1219,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
rows = []
rows.append( ( 'Enable file viewing statistics tracking?:', self._file_viewing_statistics_active ) )
rows.append( ( 'Enable file viewing statistics tracking on the duplicate filter?:', self._file_viewing_statistics_active_on_dupe_filter ) )
rows.append( ( 'Enable file viewing statistics tracking in the archive/delete filter?:', self._file_viewing_statistics_active_on_archive_delete_filter ) )
rows.append( ( 'Enable file viewing statistics tracking in the duplicate filter?:', self._file_viewing_statistics_active_on_dupe_filter ) )
rows.append( ( 'Min time to view on media viewer to count as a view (seconds):', self._file_viewing_statistics_media_min_time ) )
rows.append( ( 'Cap any view on the media viewer to this maximum time (seconds):', self._file_viewing_statistics_media_max_time ) )
rows.append( ( 'Min time to view on preview viewer to count as a view (seconds):', self._file_viewing_statistics_preview_min_time ) )
@ -1242,6 +1239,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
def UpdateOptions( self ):
self._new_options.SetBoolean( 'file_viewing_statistics_active', self._file_viewing_statistics_active.isChecked() )
self._new_options.SetBoolean( 'file_viewing_statistics_active_on_archive_delete_filter', self._file_viewing_statistics_active_on_archive_delete_filter.isChecked() )
self._new_options.SetBoolean( 'file_viewing_statistics_active_on_dupe_filter', self._file_viewing_statistics_active_on_dupe_filter.isChecked() )
self._new_options.SetNoneableInteger( 'file_viewing_statistics_media_min_time', self._file_viewing_statistics_media_min_time.GetValue() )
self._new_options.SetNoneableInteger( 'file_viewing_statistics_media_max_time', self._file_viewing_statistics_media_max_time.GetValue() )
@ -2583,41 +2581,71 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
#
self._autocomplete_panel = ClientGUICommon.StaticBox( self, 'autocomplete' )
self._read_autocomplete_panel = ClientGUICommon.StaticBox( self, 'file search autocomplete' )
self._default_search_synchronised = QW.QCheckBox( self._autocomplete_panel )
location_context = self._new_options.GetDefaultLocalLocationContext()
self._default_local_location_context = ClientGUILocation.LocationSearchContextButton( self._read_autocomplete_panel, location_context )
self._default_local_location_context.setToolTip( 'This initialised into a bunch of dialogs across the program as a fallback. You can probably leave it alone forever, but if you delete or move away from \'my files\' as your main place to do work, please update it here.' )
self._default_local_location_context.SetOnlyImportableDomainsAllowed( True )
self._default_tag_service_search_page = ClientGUICommon.BetterChoice( self._read_autocomplete_panel )
self._default_search_synchronised = QW.QCheckBox( self._read_autocomplete_panel )
tt = 'This refers to the button on the autocomplete dropdown that enables new searches to start. If this is on, then new search pages will search as soon as you enter the first search predicate. If off, no search will happen until you switch it back on.'
self._default_search_synchronised.setToolTip( tt )
self._autocomplete_float_main_gui = QW.QCheckBox( self._autocomplete_panel )
self._autocomplete_float_main_gui = QW.QCheckBox( self._read_autocomplete_panel )
tt = 'The autocomplete dropdown can either \'float\' on top of the main window, or if that does not work well for you, it can embed into the parent page panel.'
self._autocomplete_float_main_gui.setToolTip( tt )
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_read_list_height_num_chars = ClientGUICommon.BetterSpinBox( self._read_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 )
self._always_show_system_everything = QW.QCheckBox( self._autocomplete_panel )
self._always_show_system_everything = QW.QCheckBox( self._read_autocomplete_panel )
tt = 'After users get some experience with the program and a larger collection, they tend to have less use for system:everything.'
self._always_show_system_everything.setToolTip( tt )
self._filter_inbox_and_archive_predicates = QW.QCheckBox( self._autocomplete_panel )
self._filter_inbox_and_archive_predicates = QW.QCheckBox( self._read_autocomplete_panel )
tt = 'If everything is current in the inbox (or archive), then there is no use listing it or its opposite--it either does not change the search or it produces nothing. If you find it jarring though, turn it off here!'
self._filter_inbox_and_archive_predicates.setToolTip( tt )
#
misc_panel = ClientGUICommon.StaticBox( self, 'misc' )
self._write_autocomplete_panel = ClientGUICommon.StaticBox( self, 'tag edit autocomplete' )
self._default_tag_service_tab = ClientGUICommon.BetterChoice( self._write_autocomplete_panel )
self._save_default_tag_service_tab_on_change = QW.QCheckBox( self._write_autocomplete_panel )
self._ac_write_list_height_num_chars = ClientGUICommon.BetterSpinBox( self._write_autocomplete_panel, min = 1, max = 128 )
#
misc_panel = ClientGUICommon.StaticBox( self, 'file search' )
self._forced_search_limit = ClientGUICommon.NoneableSpinCtrl( misc_panel, '', min = 1, max = 100000 )
self._forced_search_limit.setToolTip( 'This is overruled if you set an explicit system:limit larger than it.' )
#
self._default_tag_service_search_page.addItem( 'all known tags', CC.COMBINED_TAG_SERVICE_KEY )
services = HG.client_controller.services_manager.GetServices( HC.REAL_TAG_SERVICES )
for service in services:
self._default_tag_service_tab.addItem( service.GetName(), service.GetServiceKey() )
self._default_tag_service_search_page.addItem( service.GetName(), service.GetServiceKey() )
self._default_tag_service_tab.SetValue( self._new_options.GetKey( 'default_tag_service_tab' ) )
self._save_default_tag_service_tab_on_change.setChecked( self._new_options.GetBoolean( 'save_default_tag_service_tab_on_change' ) )
self._default_tag_service_search_page.SetValue( self._new_options.GetKey( 'default_tag_service_search_page' ) )
self._default_search_synchronised.setChecked( self._new_options.GetBoolean( 'default_search_synchronised' ) )
self._autocomplete_float_main_gui.setChecked( self._new_options.GetBoolean( 'autocomplete_float_main_gui' ) )
@ -2633,28 +2661,43 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
#
vbox = QP.VBoxLayout()
message = 'This tag autocomplete appears in file search pages and other places where you use tags and system predicates to search for files.'
message = 'The autocomplete dropdown list is the panel that hangs below the tag input text box on search pages.'
st = ClientGUICommon.BetterStaticText( self._read_autocomplete_panel, label = message )
st = ClientGUICommon.BetterStaticText( self._autocomplete_panel, label = message )
self._autocomplete_panel.Add( st, CC.FLAGS_CENTER )
self._read_autocomplete_panel.Add( st, CC.FLAGS_CENTER )
rows = []
#
rows.append( ( 'Default/Fallback local file search location: ', self._default_local_location_context ) )
rows.append( ( 'Default tag service in search pages: ', self._default_tag_service_search_page ) )
rows.append( ( 'Autocomplete dropdown floats over file search pages: ', self._autocomplete_float_main_gui ) )
rows.append( ( 'Autocomplete list height: ', self._ac_read_list_height_num_chars ) )
rows.append( ( 'Start new search pages in \'searching immediately\': ', self._default_search_synchronised ) )
rows.append( ( 'Autocomplete results float in file search pages: ', self._autocomplete_float_main_gui ) )
rows.append( ( '\'Read\' autocomplete list height: ', self._ac_read_list_height_num_chars ) )
rows.append( ( '\'Write\' autocomplete list height: ', self._ac_write_list_height_num_chars ) )
rows.append( ( 'show system:everything even if total files is over 10,000: ', self._always_show_system_everything ) )
rows.append( ( 'hide inbox and archive system predicates if either has no files: ', self._filter_inbox_and_archive_predicates ) )
gridbox = ClientGUICommon.WrapInGrid( self._autocomplete_panel, rows )
gridbox = ClientGUICommon.WrapInGrid( self._read_autocomplete_panel, rows )
self._autocomplete_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._read_autocomplete_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
#
message = 'This tag autocomplete appears in the manage tags dialog and other places where you edit a list of tags.'
st = ClientGUICommon.BetterStaticText( self._write_autocomplete_panel, label = message )
self._write_autocomplete_panel.Add( st, CC.FLAGS_CENTER )
rows = []
rows.append( ( 'Remember last used default tag service in manage tag dialogs: ', self._save_default_tag_service_tab_on_change ) )
rows.append( ( 'Default tag service in manage tag dialogs: ', self._default_tag_service_tab ) )
rows.append( ( 'Autocomplete list height: ', self._ac_write_list_height_num_chars ) )
gridbox = ClientGUICommon.WrapInGrid( self._write_autocomplete_panel, rows )
self._write_autocomplete_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
#
@ -2668,18 +2711,32 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
#
#
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._autocomplete_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._read_autocomplete_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, misc_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._write_autocomplete_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.addStretch( 1 )
self.setLayout( vbox )
self._UpdateDefaultTagServiceControl()
self._save_default_tag_service_tab_on_change.clicked.connect( self._UpdateDefaultTagServiceControl )
def _UpdateDefaultTagServiceControl( self ):
enabled = not self._save_default_tag_service_tab_on_change.isChecked()
self._default_tag_service_tab.setEnabled( enabled )
def UpdateOptions( self ):
self._new_options.SetDefaultLocalLocationContext( self._default_local_location_context.GetValue() )
self._new_options.SetBoolean( 'default_search_synchronised', self._default_search_synchronised.isChecked() )
self._new_options.SetBoolean( 'autocomplete_float_main_gui', self._autocomplete_float_main_gui.isChecked() )
@ -3504,12 +3561,6 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
general_panel = ClientGUICommon.StaticBox( self, 'general tag options' )
self._default_tag_service_tab = ClientGUICommon.BetterChoice( general_panel )
self._save_default_tag_service_tab_on_change = QW.QCheckBox( general_panel )
self._default_tag_service_search_page = ClientGUICommon.BetterChoice( general_panel )
self._expand_parents_on_storage_taglists = QW.QCheckBox( general_panel )
self._expand_parents_on_storage_autocomplete_taglists = QW.QCheckBox( general_panel )
@ -3536,23 +3587,6 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
#
self._default_tag_service_search_page.addItem( 'all known tags', CC.COMBINED_TAG_SERVICE_KEY )
services = HG.client_controller.services_manager.GetServices( HC.REAL_TAG_SERVICES )
for service in services:
self._default_tag_service_tab.addItem( service.GetName(), service.GetServiceKey() )
self._default_tag_service_search_page.addItem( service.GetName(), service.GetServiceKey() )
self._default_tag_service_tab.SetValue( self._new_options.GetKey( 'default_tag_service_tab' ) )
self._save_default_tag_service_tab_on_change.setChecked( self._new_options.GetBoolean( 'save_default_tag_service_tab_on_change' ) )
self._default_tag_service_search_page.SetValue( self._new_options.GetKey( 'default_tag_service_search_page' ) )
self._expand_parents_on_storage_taglists.setChecked( self._new_options.GetBoolean( 'expand_parents_on_storage_taglists' ) )
self._expand_parents_on_storage_taglists.setToolTip( 'This affects taglists in places like the manage tags dialog, where you edit tags as they actually are, and implied parents hang below tags.' )
@ -3583,9 +3617,6 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
rows = []
rows.append( ( 'Default tag service in manage tag dialogs: ', self._default_tag_service_tab ) )
rows.append( ( 'Remember last used default tag service in manage tag dialogs: ', self._save_default_tag_service_tab_on_change ) )
rows.append( ( 'Default tag service in search pages: ', self._default_tag_service_search_page ) )
rows.append( ( 'Show parent info by default on edit/write taglists: ', self._show_parent_decorators_on_storage_taglists ) )
rows.append( ( 'Show parent info by default on edit/write autocomplete taglists: ', self._show_parent_decorators_on_storage_autocomplete_taglists ) )
rows.append( ( 'Show parents expanded by default on edit/write taglists: ', self._expand_parents_on_storage_taglists ) )
@ -3614,20 +3645,9 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
#
self._save_default_tag_service_tab_on_change.clicked.connect( self._UpdateDefaultTagServiceControl )
self._UpdateDefaultTagServiceControl()
self._favourites_input.tagsPasted.connect( self.AddTagsOnlyAdd )
def _UpdateDefaultTagServiceControl( self ):
enabled = not self._save_default_tag_service_tab_on_change.isChecked()
self._default_tag_service_tab.setEnabled( enabled )
def AddTagsOnlyAdd( self, tags ):
current_tags = self._favourites.GetTags()
@ -3642,11 +3662,6 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
def UpdateOptions( self ):
self._new_options.SetKey( 'default_tag_service_tab', self._default_tag_service_tab.GetValue() )
self._new_options.SetBoolean( 'save_default_tag_service_tab_on_change', self._save_default_tag_service_tab_on_change.isChecked() )
self._new_options.SetKey( 'default_tag_service_search_page', self._default_tag_service_search_page.GetValue() )
self._new_options.SetBoolean( 'show_parent_decorators_on_storage_taglists', self._show_parent_decorators_on_storage_taglists.isChecked() )
self._new_options.SetBoolean( 'show_parent_decorators_on_storage_autocomplete_taglists', self._show_parent_decorators_on_storage_autocomplete_taglists.isChecked() )
self._new_options.SetBoolean( 'expand_parents_on_storage_taglists', self._expand_parents_on_storage_taglists.isChecked() )
@ -3695,7 +3710,22 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._show_number_namespaces.setToolTip( 'This lets unnamespaced "16:9" show as that, not hiding the "16".' )
self._namespace_connector = QW.QLineEdit( render_panel )
self._sibling_connector = QW.QLineEdit( render_panel )
self._fade_sibling_connector = QW.QCheckBox( render_panel )
tt = 'If set, then if the sibling goes from one namespace to another, that colour will fade across the distance of the sibling connector. Just a bit of fun.'
self._fade_sibling_connector.setToolTip( tt )
self._sibling_connector_custom_namespace_colour = ClientGUICommon.NoneableTextCtrl( render_panel, none_phrase = 'use ideal tag colour' )
tt = 'The sibling connector can use a particular namespace\'s colour.'
self._sibling_connector_custom_namespace_colour.setToolTip( tt )
self._or_connector = QW.QLineEdit( render_panel )
tt = 'When an OR predicate is rendered, it splits the components by this text.'
self._or_connector.setToolTip( tt )
self._or_connector_custom_namespace_colour = QW.QLineEdit( render_panel )
tt = 'The OR connector can use a particular namespace\'s colour.'
self._or_connector_custom_namespace_colour.setToolTip( tt )
self._replace_tag_underscores_with_spaces = QW.QCheckBox( render_panel )
@ -3712,11 +3742,14 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
#
self._show_namespaces.setChecked( new_options.GetBoolean( 'show_namespaces' ) )
self._show_number_namespaces.setChecked(( new_options.GetBoolean( 'show_number_namespaces' ) ) )
self._show_number_namespaces.setChecked( new_options.GetBoolean( 'show_number_namespaces' ) )
self._namespace_connector.setText( new_options.GetString( 'namespace_connector' ) )
self._replace_tag_underscores_with_spaces.setChecked( new_options.GetBoolean( 'replace_tag_underscores_with_spaces' ) )
self._sibling_connector.setText( new_options.GetString( 'sibling_connector' ) )
self._fade_sibling_connector.setChecked( new_options.GetBoolean( 'fade_sibling_connector' ) )
self._replace_tag_underscores_with_spaces.setChecked( new_options.GetBoolean( 'replace_tag_underscores_with_spaces' ) )
self._sibling_connector_custom_namespace_colour.SetValue( new_options.GetNoneableString( 'sibling_connector_custom_namespace_colour' ) )
self._or_connector.setText( new_options.GetString( 'or_connector' ) )
self._or_connector_custom_namespace_colour.setText( new_options.GetNoneableString( 'or_connector_custom_namespace_colour' ) )
#
@ -3754,7 +3787,10 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
rows.append( ( 'Unless namespace is a number: ', self._show_number_namespaces ) )
rows.append( ( 'If shown, namespace connecting string: ', self._namespace_connector ) )
rows.append( ( 'Sibling connecting string: ', self._sibling_connector ) )
rows.append( ( 'Fade the namespace colour when showing siblings on Qt6: ', self._fade_sibling_connector ) )
rows.append( ( 'Fade the colour of the sibling connector string on Qt6: ', self._fade_sibling_connector ) )
rows.append( ( 'Namespace for the colour of the sibling connecting string: ', self._sibling_connector_custom_namespace_colour ) )
rows.append( ( 'OR connecting string: ', self._or_connector ) )
rows.append( ( 'Namespace for the colour of the OR connecting string: ', self._or_connector_custom_namespace_colour ) )
rows.append( ( 'EXPERIMENTAL: Replace all underscores with spaces: ', self._replace_tag_underscores_with_spaces ) )
gridbox = ClientGUICommon.WrapInGrid( render_panel, rows )
@ -3771,8 +3807,10 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
#
self._NamespacesUpdated()
self._SiblingColourStuffClicked()
self._show_namespaces.clicked.connect( self._NamespacesUpdated )
self._fade_sibling_connector.clicked.connect( self._SiblingColourStuffClicked )
self.setLayout( vbox )
@ -3837,6 +3875,13 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
def _SiblingColourStuffClicked( self ):
choice_available = not self._fade_sibling_connector.isChecked()
self._sibling_connector_custom_namespace_colour.setEnabled( choice_available )
def _NamespacesUpdated( self ):
self._show_number_namespaces.setEnabled( not self._show_namespaces.isChecked() )
@ -3851,9 +3896,14 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._new_options.SetBoolean( 'show_namespaces', self._show_namespaces.isChecked() )
self._new_options.SetBoolean( 'show_number_namespaces', self._show_number_namespaces.isChecked() )
self._new_options.SetString( 'namespace_connector', self._namespace_connector.text() )
self._new_options.SetBoolean( 'replace_tag_underscores_with_spaces', self._replace_tag_underscores_with_spaces.isChecked() )
self._new_options.SetString( 'sibling_connector', self._sibling_connector.text() )
self._new_options.SetBoolean( 'fade_sibling_connector', self._fade_sibling_connector.isChecked() )
self._new_options.SetBoolean( 'replace_tag_underscores_with_spaces', self._replace_tag_underscores_with_spaces.isChecked() )
self._new_options.SetNoneableString( 'sibling_connector_custom_namespace_colour', self._sibling_connector_custom_namespace_colour.GetValue() )
self._new_options.SetString( 'or_connector', self._or_connector.text() )
self._new_options.SetNoneableString( 'or_connector_custom_namespace_colour', self._or_connector_custom_namespace_colour.text() )
HC.options[ 'namespace_colours' ] = self._namespace_colours.GetNamespaceColours()

View File

@ -690,7 +690,7 @@ class Canvas( QW.QWidget, CAC.ApplicationCommandProcessorMixin ):
hash = self._current_media.GetHash()
HG.client_controller.file_viewing_stats_manager.FinishViewing( hash, self.CANVAS_TYPE, view_timestamp, viewtime_delta )
HG.client_controller.file_viewing_stats_manager.FinishViewing( self._current_media, self.CANVAS_TYPE, view_timestamp, viewtime_delta )
def _SeekDeltaCurrentMedia( self, direction, duration_ms ):
@ -3675,6 +3675,8 @@ def CommitArchiveDelete( page_key: bytes, location_context: ClientLocation.Locat
class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
CANVAS_TYPE = CC.CANVAS_MEDIA_VIEWER_ARCHIVE_DELETE
def __init__( self, parent, page_key, location_context: ClientLocation.LocationContext, media_results ):
CanvasMediaList.__init__( self, parent, page_key, location_context, media_results )

View File

@ -507,7 +507,7 @@ class Animation( QW.QWidget ):
self._current_frame_drawn = True
next_frame_time_s = self._video_container.GetDuration( self._current_frame_index ) / 1000.0
next_frame_time_s = self._video_container.GetDurationMS( self._current_frame_index ) / 1000.0
next_frame_ideally_due = self._next_frame_due_at + next_frame_time_s
@ -895,7 +895,7 @@ class Animation( QW.QWidget ):
if self._current_timestamp_ms is not None and self._video_container is not None and self._video_container.IsInitialised():
duration_ms = self._video_container.GetDuration( self._current_frame_index - 1 )
duration_ms = self._video_container.GetDurationMS( self._current_frame_index - 1 )
self._current_timestamp_ms += duration_ms
@ -1261,7 +1261,7 @@ class AnimationBar( QW.QWidget ):
def SetMediaAndWindow( self, media, media_window ):
self._media_window = media_window
self._duration_ms = max( media.GetDuration(), 1 )
self._duration_ms = max( media.GetDurationMS(), 1 )
num_frames = media.GetNumFrames()

View File

@ -339,12 +339,12 @@ class MPVWidget( QW.QWidget, CAC.ApplicationCommandProcessorMixin ):
else:
current_frame_index = int( round( ( current_timestamp_ms / self._media.GetDuration() ) * num_frames ) )
current_frame_index = int( round( ( current_timestamp_ms / self._media.GetDurationMS() ) * num_frames ) )
current_frame_index = min( current_frame_index, num_frames - 1 )
current_timestamp_ms = min( current_timestamp_ms, self._media.GetDuration() )
current_timestamp_ms = min( current_timestamp_ms, self._media.GetDurationMS() )
paused = self._player.pause
@ -492,7 +492,7 @@ class MPVWidget( QW.QWidget, CAC.ApplicationCommandProcessorMixin ):
new_timestamp_ms = max( 0, ( current_timestamp_s * 1000 ) + ( direction * duration_ms ) )
if new_timestamp_ms > self._media.GetDuration():
if new_timestamp_ms > self._media.GetDurationMS():
new_timestamp_ms = 0

View File

@ -44,6 +44,15 @@ class EditFileImportOptionsPanel( ClientGUIScrolledPanels.EditPanel ):
help_hbox = ClientGUICommon.WrapInText( help_button, self, 'help for this panel -->', object_name = 'HydrusIndeterminate' )
#
if file_import_options.IsDefault():
file_import_options = HG.client_controller.new_options.GetDefaultFileImportOptions( FileImportOptions.IMPORT_TYPE_LOUD ).Duplicate()
file_import_options.SetIsDefault( True )
#
default_panel = ClientGUICommon.StaticBox( self, 'default options' )

View File

@ -1714,6 +1714,8 @@ class ListBox( QW.QScrollArea ):
rows_of_texts_and_colours = self._GetRowsOfTextsAndColours( term )
term_ok_with_fade = term.CanFadeColours()
for texts_and_colours in rows_of_texts_and_colours:
x_start = self.TEXT_X_PADDING
@ -1743,7 +1745,7 @@ class ListBox( QW.QScrollArea ):
text_colour = namespace_colour
do_a_fade = fades_can_ever_happen and last_used_namespace_colour is not None and last_used_namespace_colour != namespace_colour
do_a_fade = fades_can_ever_happen and term_ok_with_fade and last_used_namespace_colour is not None and last_used_namespace_colour != namespace_colour
if term in self._selected_terms:
@ -2291,7 +2293,13 @@ class ListBoxTags( ListBox ):
self._page_key = None # placeholder. if a subclass sets this, it changes menu behaviour to allow 'select this tag' menu pubsubs
self._sibling_connection_string = HG.client_controller.new_options.GetString( 'sibling_connector' )
self._sibling_connector_string = HG.client_controller.new_options.GetString( 'sibling_connector' )
self._sibling_connector_namespace = None
if not HG.client_controller.new_options.GetBoolean( 'fade_sibling_connector' ):
self._sibling_connector_namespace = HG.client_controller.new_options.GetNoneableString( 'sibling_connector_custom_namespace_colour' )
self._UpdateBackgroundColour()
@ -2365,7 +2373,7 @@ class ListBoxTags( ListBox ):
show_parent_rows = self._show_parent_decorators and self._extra_parent_rows_allowed
rows_of_texts_and_namespaces = term.GetRowsOfPresentationTextsWithNamespaces( self._render_for_user, self._show_sibling_decorators, self._sibling_connection_string, self._show_parent_decorators, show_parent_rows )
rows_of_texts_and_namespaces = term.GetRowsOfPresentationTextsWithNamespaces( self._render_for_user, self._show_sibling_decorators, self._sibling_connector_string, self._sibling_connector_namespace, self._show_parent_decorators, show_parent_rows )
rows_of_texts_and_colours = []
@ -2373,7 +2381,7 @@ class ListBoxTags( ListBox ):
texts_and_colours = []
for ( text, namespace ) in texts_and_namespaces:
for ( text, colour_type, namespace ) in texts_and_namespaces:
if namespace in namespace_colours:
@ -2620,11 +2628,18 @@ class ListBoxTags( ListBox ):
def NotifyNewOptions( self ):
new_sibling_connection_string = HG.client_controller.new_options.GetString( 'sibling_connector' )
new_sibling_connector_string = HG.client_controller.new_options.GetString( 'sibling_connector' )
new_sibling_connector_namespace = None
if new_sibling_connection_string != self._sibling_connection_string:
if not HG.client_controller.new_options.GetBoolean( 'fade_sibling_connector' ):
self._sibling_connection_string = new_sibling_connection_string
new_sibling_connector_namespace = HG.client_controller.new_options.GetNoneableString( 'sibling_connector_custom_namespace_colour' )
if new_sibling_connector_string != self._sibling_connector_string or new_sibling_connector_namespace != self._sibling_connector_namespace:
self._sibling_connector_string = new_sibling_connector_string
self._sibling_connector_namespace = new_sibling_connector_namespace
self.widget().update()

View File

@ -39,6 +39,11 @@ class ListBoxItem( object ):
return NotImplemented
def CanFadeColours( self ):
return False
def GetCopyableText( self, with_counts: bool = False ) -> str:
raise NotImplementedError()
@ -49,7 +54,7 @@ class ListBoxItem( object ):
raise NotImplementedError()
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, sibling_connection_string: str, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, sibling_connector_string: str, sibling_connector_namespace: typing.Optional[ str ], parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str, str ] ] ]:
raise NotImplementedError()
@ -88,7 +93,7 @@ class ListBoxItemTagSlice( ListBoxItem ):
return []
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, sibling_connection_string: str, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.Tuple[ str, str ] ]:
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, sibling_connector_string: str, sibling_connector_namespace: typing.Optional[ str ], parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str, str ] ] ]:
presentation_text = self.GetCopyableText()
@ -101,7 +106,7 @@ class ListBoxItemTagSlice( ListBoxItem ):
( namespace, subtag ) = HydrusTags.SplitTag( self._tag_slice )
return [ [ ( presentation_text, namespace ) ] ]
return [ [ ( presentation_text, 'namespace', namespace ) ] ]
def GetTags( self ) -> typing.Set[ str ]:
@ -160,9 +165,9 @@ class ListBoxItemNamespaceColour( ListBoxItem ):
return []
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, sibling_connection_string: str, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, sibling_connector_string: str, sibling_connector_namespace: typing.Optional[ str ], parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str, str ] ] ]:
return [ [ ( self.GetCopyableText(), self._namespace ) ] ]
return [ [ ( self.GetCopyableText(), 'namespace', self._namespace ) ] ]
def GetTags( self ) -> typing.Set[ str ]:
@ -196,12 +201,17 @@ class ListBoxItemTextTag( ListBoxItem ):
return NotImplemented
def _AppendIdealTagTextWithNamespace( self, texts_with_namespaces, sibling_connection_string: str, render_for_user ):
def _AppendIdealTagTextWithNamespace( self, texts_with_namespaces, sibling_connector_string: str, sibling_connector_namespace: typing.Optional[ str ], render_for_user ):
( namespace, subtag ) = HydrusTags.SplitTag( self._ideal_tag )
texts_with_namespaces.append( ( sibling_connection_string, namespace ) )
texts_with_namespaces.append( ( ClientTags.RenderTag( self._ideal_tag, render_for_user ), namespace ) )
if sibling_connector_namespace is None:
sibling_connector_namespace = namespace
texts_with_namespaces.append( ( sibling_connector_string, 'sibling_connector', sibling_connector_namespace ) )
texts_with_namespaces.append( ( ClientTags.RenderTag( self._ideal_tag, render_for_user ), 'namespace', namespace ) )
def _AppendParentsTextWithNamespaces( self, rows_of_texts_with_namespaces, render_for_user ):
@ -214,7 +224,7 @@ class ListBoxItemTextTag( ListBoxItem ):
tag_text = ClientTags.RenderTag( parent, render_for_user )
texts_with_namespaces = [ ( indent + tag_text, namespace ) ]
texts_with_namespaces = [ ( indent + tag_text, 'namespace', namespace ) ]
rows_of_texts_with_namespaces.append( texts_with_namespaces )
@ -224,7 +234,7 @@ class ListBoxItemTextTag( ListBoxItem ):
parents_text = ' ({} parents)'.format( HydrusData.ToHumanInt( len( self._parent_tags ) ) )
texts_with_namespaces.append( ( parents_text, '' ) )
texts_with_namespaces.append( ( parents_text, 'namespace', '' ) )
def GetBestTag( self ) -> str:
@ -259,7 +269,7 @@ class ListBoxItemTextTag( ListBoxItem ):
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, sibling_connection_string: str, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, sibling_connector_string: str, sibling_connector_namespace: typing.Optional[ str ], parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str, str ] ] ]:
# this should be with counts or whatever, but we need to think about this more lad
@ -267,11 +277,11 @@ class ListBoxItemTextTag( ListBoxItem ):
tag_text = ClientTags.RenderTag( self._tag, render_for_user )
first_row_of_texts_with_namespaces = [ ( tag_text, namespace ) ]
first_row_of_texts_with_namespaces = [ ( tag_text, 'namespace', namespace ) ]
if sibling_decoration_allowed and self._ideal_tag is not None:
self._AppendIdealTagTextWithNamespace( first_row_of_texts_with_namespaces, sibling_connection_string, render_for_user )
self._AppendIdealTagTextWithNamespace( first_row_of_texts_with_namespaces, sibling_connector_string, sibling_connector_namespace, render_for_user )
rows_of_texts_with_namespaces = [ first_row_of_texts_with_namespaces ]
@ -350,9 +360,10 @@ class ListBoxItemTextTagWithCounts( ListBoxItemTextTag ):
if with_counts:
sibling_connection_string = ''
sibling_connector_string = ''
sibling_connector_namespace = ''
return ''.join( ( text for ( text, namespace ) in self.GetRowsOfPresentationTextsWithNamespaces( False, False, sibling_connection_string, False, False )[0] ) )
return ''.join( ( text for ( text, colour_type, data ) in self.GetRowsOfPresentationTextsWithNamespaces( False, False, sibling_connector_string, sibling_connector_namespace, False, False )[0] ) )
else:
@ -367,7 +378,7 @@ class ListBoxItemTextTagWithCounts( ListBoxItemTextTag ):
return [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, value = self._tag ) ]
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, sibling_connection_string: str, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, sibling_connector_string: str, sibling_connector_namespace: typing.Optional[ str ], parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str, str ] ] ]:
# this should be with counts or whatever, but we need to think about this more lad
@ -415,11 +426,11 @@ class ListBoxItemTextTagWithCounts( ListBoxItemTextTag ):
first_row_of_texts_with_namespaces = [ ( tag_text, namespace ) ]
first_row_of_texts_with_namespaces = [ ( tag_text, 'namespace', namespace ) ]
if sibling_decoration_allowed and self._ideal_tag is not None:
self._AppendIdealTagTextWithNamespace( first_row_of_texts_with_namespaces, sibling_connection_string, render_for_user )
self._AppendIdealTagTextWithNamespace( first_row_of_texts_with_namespaces, sibling_connector_string, sibling_connector_namespace, render_for_user )
rows_of_texts_with_namespaces = [ first_row_of_texts_with_namespaces ]
@ -464,6 +475,11 @@ class ListBoxItemPredicate( ListBoxItem ):
return NotImplemented
def CanFadeColours( self ):
return not self._predicate.IsORPredicate()
def GetCopyableText( self, with_counts: bool = False ) -> str:
if self._predicate.GetType() == ClientSearch.PREDICATE_TYPE_NAMESPACE:
@ -508,7 +524,7 @@ class ListBoxItemPredicate( ListBoxItem ):
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, sibling_connection_string: str, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, sibling_connector_string: str, sibling_connector_namespace: typing.Optional[ str ], parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str, str ] ] ]:
rows_of_texts_and_namespaces = []
@ -520,8 +536,13 @@ class ListBoxItemPredicate( ListBoxItem ):
( ideal_namespace, ideal_subtag ) = HydrusTags.SplitTag( ideal_sibling )
first_row_of_texts_and_namespaces.append( ( sibling_connection_string, ideal_namespace ) )
first_row_of_texts_and_namespaces.append( ( ClientTags.RenderTag( ideal_sibling, render_for_user ), ideal_namespace ) )
if sibling_connector_namespace is None:
sibling_connector_namespace = ideal_namespace
first_row_of_texts_and_namespaces.append( ( sibling_connector_string, 'sibling_connector', sibling_connector_namespace ) )
first_row_of_texts_and_namespaces.append( ( ClientTags.RenderTag( ideal_sibling, render_for_user ), 'namespace', ideal_namespace ) )
rows_of_texts_and_namespaces.append( first_row_of_texts_and_namespaces )
@ -541,7 +562,7 @@ class ListBoxItemPredicate( ListBoxItem ):
parents_text = ' ({} parents)'.format( HydrusData.ToHumanInt( len( parent_preds ) ) )
first_row_of_texts_and_namespaces.append( ( parents_text, '' ) )
first_row_of_texts_and_namespaces.append( ( parents_text, 'namespace', '' ) )

View File

@ -128,12 +128,7 @@ def AddPresentationSubmenu( menu: QW.QMenu, importer_name: str, single_selected_
ClientGUIMenus.AppendMenu( menu, submenu, 'show files' )
def CreateManagementController( page_name, management_type, location_context = None ):
if location_context is None:
location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY )
def CreateManagementController( page_name, management_type ):
new_options = HG.client_controller.new_options
@ -142,7 +137,6 @@ def CreateManagementController( page_name, management_type, location_context = N
management_controller.SetType( management_type )
management_controller.SetVariable( 'media_sort', new_options.GetDefaultSort() )
management_controller.SetVariable( 'media_collect', new_options.GetDefaultCollect() )
management_controller.SetVariable( 'location_context', location_context )
return management_controller
@ -151,7 +145,7 @@ def CreateManagementControllerDuplicateFilter():
default_location_context = HG.client_controller.new_options.GetDefaultLocalLocationContext()
management_controller = CreateManagementController( 'duplicates', MANAGEMENT_TYPE_DUPLICATE_FILTER, location_context = default_location_context )
management_controller = CreateManagementController( 'duplicates', MANAGEMENT_TYPE_DUPLICATE_FILTER )
file_search_context = ClientSearch.FileSearchContext( location_context = default_location_context, predicates = [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_EVERYTHING ) ] )
@ -167,6 +161,7 @@ def CreateManagementControllerDuplicateFilter():
return management_controller
def CreateManagementControllerImportGallery( page_name = None ):
if page_name is None:
@ -178,14 +173,13 @@ def CreateManagementControllerImportGallery( page_name = None ):
multiple_gallery_import = ClientImportGallery.MultipleGalleryImport( gug_key_and_name = gug_key_and_name )
location_context = multiple_gallery_import.GetFileImportOptions().GetDestinationLocationContext()
management_controller = CreateManagementController( page_name, MANAGEMENT_TYPE_IMPORT_MULTIPLE_GALLERY, location_context = location_context )
management_controller = CreateManagementController( page_name, MANAGEMENT_TYPE_IMPORT_MULTIPLE_GALLERY )
management_controller.SetVariable( 'multiple_gallery_import', multiple_gallery_import )
return management_controller
def CreateManagementControllerImportSimpleDownloader():
simple_downloader_import = ClientImportSimpleURLs.SimpleDownloaderImport()
@ -194,19 +188,16 @@ def CreateManagementControllerImportSimpleDownloader():
simple_downloader_import.SetFormulaName( formula_name )
location_context = simple_downloader_import.GetFileImportOptions().GetDestinationLocationContext()
management_controller = CreateManagementController( 'simple downloader', MANAGEMENT_TYPE_IMPORT_SIMPLE_DOWNLOADER, location_context = location_context )
management_controller = CreateManagementController( 'simple downloader', MANAGEMENT_TYPE_IMPORT_SIMPLE_DOWNLOADER )
management_controller.SetVariable( 'simple_downloader_import', simple_downloader_import )
return management_controller
def CreateManagementControllerImportHDD( paths, file_import_options: FileImportOptions.FileImportOptions, metadata_routers: typing.Collection[ ClientMetadataMigration.SingleFileMetadataRouter ], paths_to_additional_service_keys_to_tags, delete_after_success ):
location_context = file_import_options.GetDestinationLocationContext()
management_controller = CreateManagementController( 'import', MANAGEMENT_TYPE_IMPORT_HDD, location_context = location_context )
management_controller = CreateManagementController( 'import', MANAGEMENT_TYPE_IMPORT_HDD )
hdd_import = ClientImportLocal.HDDImport( paths = paths, file_import_options = file_import_options, metadata_routers = metadata_routers, paths_to_additional_service_keys_to_tags = paths_to_additional_service_keys_to_tags, delete_after_success = delete_after_success )
@ -214,6 +205,7 @@ def CreateManagementControllerImportHDD( paths, file_import_options: FileImportO
return management_controller
def CreateManagementControllerImportMultipleWatcher( page_name = None, url = None ):
if page_name is None:
@ -223,14 +215,13 @@ def CreateManagementControllerImportMultipleWatcher( page_name = None, url = Non
multiple_watcher_import = ClientImportWatchers.MultipleWatcherImport( url = url )
location_context = multiple_watcher_import.GetFileImportOptions().GetDestinationLocationContext()
management_controller = CreateManagementController( page_name, MANAGEMENT_TYPE_IMPORT_MULTIPLE_WATCHER, location_context = location_context )
management_controller = CreateManagementController( page_name, MANAGEMENT_TYPE_IMPORT_MULTIPLE_WATCHER )
management_controller.SetVariable( 'multiple_watcher_import', multiple_watcher_import )
return management_controller
def CreateManagementControllerImportURLs( page_name = None ):
if page_name is None:
@ -240,42 +231,31 @@ def CreateManagementControllerImportURLs( page_name = None ):
urls_import = ClientImportSimpleURLs.URLsImport()
location_context = urls_import.GetFileImportOptions().GetDestinationLocationContext()
management_controller = CreateManagementController( page_name, MANAGEMENT_TYPE_IMPORT_URLS, location_context = location_context )
management_controller = CreateManagementController( page_name, MANAGEMENT_TYPE_IMPORT_URLS )
management_controller.SetVariable( 'urls_import', urls_import )
return management_controller
def CreateManagementControllerPetitions( petition_service_key ):
petition_service = HG.client_controller.services_manager.GetService( petition_service_key )
page_name = petition_service.GetName() + ' petitions'
petition_service_type = petition_service.GetServiceType()
if petition_service_type in HC.LOCAL_FILE_SERVICES or petition_service_type == HC.FILE_REPOSITORY:
location_context = ClientLocation.LocationContext.STATICCreateSimple( petition_service_key )
else:
location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_FILE_SERVICE_KEY )
management_controller = CreateManagementController( page_name, MANAGEMENT_TYPE_PETITIONS, location_context = location_context )
management_controller = CreateManagementController( page_name, MANAGEMENT_TYPE_PETITIONS )
management_controller.SetVariable( 'petition_service_key', petition_service_key )
return management_controller
def CreateManagementControllerQuery( page_name, file_search_context: ClientSearch.FileSearchContext, search_enabled ):
location_context = file_search_context.GetLocationContext()
management_controller = CreateManagementController( page_name, MANAGEMENT_TYPE_QUERY, location_context = location_context )
management_controller = CreateManagementController( page_name, MANAGEMENT_TYPE_QUERY )
synchronised = HG.client_controller.new_options.GetBoolean( 'default_search_synchronised' )
@ -285,6 +265,7 @@ def CreateManagementControllerQuery( page_name, file_search_context: ClientSearc
return management_controller
class ManagementController( HydrusSerialisable.SerialisableBase ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_MANAGEMENT_CONTROLLER
@ -675,6 +656,77 @@ class ManagementController( HydrusSerialisable.SerialisableBase ):
return d
def GetLocationContext( self ) -> ClientLocation.LocationContext:
# this is hacky, but it has a decent backstop and it is easier than keeping the management controller updated with this oft-changing thing all the time when we nearly always have it duped somewhere else anyway
source_names = [
'file_search_context',
'file_search_context_1',
'multiple_gallery_import',
'multiple_watcher_import',
'hdd_import',
'simple_downloader_import',
'urls_import'
]
for source_name in source_names:
if source_name in self._variables:
source = self._variables[ source_name ]
if hasattr( source, 'GetFileImportOptions' ):
file_import_options = source.GetFileImportOptions()
location_context = FileImportOptions.GetRealFileImportOptions( file_import_options, FileImportOptions.IMPORT_TYPE_LOUD ).GetDestinationLocationContext()
return location_context
elif hasattr( source, 'GetLocationContext' ):
location_context = source.GetLocationContext()
return location_context
if 'petition_service_key' in self._variables:
petition_service_key = self._variables[ 'petition_service_key' ]
try:
petition_service = HG.client_controller.services_manager.GetService( petition_service_key )
petition_service_type = petition_service.GetServiceType()
if petition_service_type in HC.LOCAL_FILE_SERVICES or petition_service_type == HC.FILE_REPOSITORY:
location_context = ClientLocation.LocationContext.STATICCreateSimple( petition_service_key )
else:
location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_FILE_SERVICE_KEY )
return location_context
except HydrusExceptions.DataMissing:
pass
if 'location_context' in self._variables:
return self._variables[ 'location_context' ]
return ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY )
def GetNumSeeds( self ):
try:
@ -912,7 +964,7 @@ class ListBoxTagsMediaManagementPanel( ClientGUIListBoxes.ListBoxTagsMedia ):
def _GetCurrentLocationContext( self ):
return self._management_controller.GetVariable( 'location_context' )
return self._management_controller.GetLocationContext()
def _GetCurrentPagePredicates( self ) -> typing.Set[ ClientSearch.Predicate ]:
@ -993,6 +1045,8 @@ class ManagementPanel( QW.QScrollArea ):
self._controller = controller
self._management_controller = management_controller
self._last_seen_location_context = self._management_controller.GetLocationContext()
self._page = page
self._page_key = self._management_controller.GetVariable( 'page_key' )
@ -1019,13 +1073,22 @@ class ManagementPanel( QW.QScrollArea ):
return 'empty page'
def _MakeCurrentSelectionTagsBox( self, sizer, tag_display_type = ClientTags.TAG_DISPLAY_SELECTION_LIST ):
self._current_selection_tags_box = ClientGUIListBoxes.StaticBoxSorterForListBoxTags( self, 'selection tags' )
self._current_selection_tags_list = ListBoxTagsMediaManagementPanel( self._current_selection_tags_box, self._management_controller, self._page_key, tag_display_type = tag_display_type )
self._current_selection_tags_box.SetTagsBox( self._current_selection_tags_list )
QP.AddToLayout( sizer, self._current_selection_tags_box, CC.FLAGS_EXPAND_BOTH_WAYS )
def _SetLocationContext( self, location_context: ClientLocation.LocationContext ):
current_location_context = self._management_controller.GetVariable( 'location_context' )
if location_context != current_location_context:
if location_context != self._last_seen_location_context:
self._management_controller.SetVariable( 'location_context', location_context )
self._last_seen_location_context = location_context
self.locationChanged.emit( location_context )
@ -1043,34 +1106,6 @@ class ManagementPanel( QW.QScrollArea ):
def GetMediaCollect( self ):
if self.SHOW_COLLECT:
return self._media_collect.GetValue()
else:
return ClientMedia.MediaCollect()
def GetMediaSort( self ):
return self._media_sort.GetSort()
def _MakeCurrentSelectionTagsBox( self, sizer, tag_display_type = ClientTags.TAG_DISPLAY_SELECTION_LIST ):
self._current_selection_tags_box = ClientGUIListBoxes.StaticBoxSorterForListBoxTags( self, 'selection tags' )
self._current_selection_tags_list = ListBoxTagsMediaManagementPanel( self._current_selection_tags_box, self._management_controller, self._page_key, tag_display_type = tag_display_type )
self._current_selection_tags_box.SetTagsBox( self._current_selection_tags_list )
QP.AddToLayout( sizer, self._current_selection_tags_box, CC.FLAGS_EXPAND_BOTH_WAYS )
def CheckAbleToClose( self ):
pass
@ -1088,7 +1123,7 @@ class ManagementPanel( QW.QScrollArea ):
def GetDefaultEmptyMediaPanel( self ) -> ClientGUIResults.MediaPanel:
location_context = self._management_controller.GetVariable( 'location_context' )
location_context = self._management_controller.GetLocationContext()
media_panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, location_context, [] )
@ -1099,6 +1134,23 @@ class ManagementPanel( QW.QScrollArea ):
return media_panel
def GetMediaCollect( self ):
if self.SHOW_COLLECT:
return self._media_collect.GetValue()
else:
return ClientMedia.MediaCollect()
def GetMediaSort( self ):
return self._media_sort.GetSort()
def GetPageState( self ) -> int:
return self._page_state
@ -2387,7 +2439,7 @@ class ManagementPanelImporterMultipleGallery( ManagementPanelImporter ):
media_results = []
location_context = self._highlighted_gallery_import.GetFileImportOptions().GetDestinationLocationContext()
location_context = FileImportOptions.GetRealFileImportOptions( self._highlighted_gallery_import.GetFileImportOptions(), FileImportOptions.IMPORT_TYPE_LOUD ).GetDestinationLocationContext()
self._SetLocationContext( location_context )
@ -3272,7 +3324,7 @@ class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ):
media_results = []
location_context = self._highlighted_watcher.GetFileImportOptions().GetDestinationLocationContext()
location_context = FileImportOptions.GetRealFileImportOptions( self._highlighted_watcher.GetFileImportOptions(), FileImportOptions.IMPORT_TYPE_LOUD ).GetDestinationLocationContext()
self._SetLocationContext( location_context )
@ -5030,19 +5082,24 @@ class ManagementPanelPetitions( ManagementPanel ):
if contents.count() > 0:
ideal_height_in_rows = min( 20, len( contents_and_checks ) )
ideal_height_in_rows = max( 1, min( 20, len( contents_and_checks ) ) )
pixels_per_row = contents.sizeHintForRow( 0 )
ideal_height_in_pixels = ( ideal_height_in_rows * pixels_per_row ) + ( contents.frameWidth() * 2 )
else:
contents.setFixedHeight( ideal_height_in_pixels )
ideal_height_in_rows = 1
pixels_per_row = 16
ideal_height_in_pixels = ( ideal_height_in_rows * pixels_per_row ) + ( contents.frameWidth() * 2 )
contents.setFixedHeight( ideal_height_in_pixels )
def _ShowHashes( self, hashes ):
location_context = self._management_controller.GetVariable( 'location_context' )
location_context = self._management_controller.GetLocationContext()
with ClientGUICommon.BusyCursor():
@ -5752,7 +5809,7 @@ class ManagementPanelQuery( ManagementPanel ):
if query_job_key == self._query_job_key:
location_context = self._management_controller.GetVariable( 'location_context' )
location_context = self._management_controller.GetLocationContext()
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, location_context, media_results )

View File

@ -456,7 +456,7 @@ class Page( QW.QWidget ):
self._preview_panel.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Sunken )
self._preview_panel.setLineWidth( 2 )
self._preview_canvas = ClientGUICanvas.CanvasPanel( self._preview_panel, self._page_key, self._management_controller.GetVariable( 'location_context' ) )
self._preview_canvas = ClientGUICanvas.CanvasPanel( self._preview_panel, self._page_key, self._management_controller.GetLocationContext() )
self._management_panel.locationChanged.connect( self._preview_canvas.SetLocationContext )
@ -1078,7 +1078,7 @@ class Page( QW.QWidget ):
self._SetPrettyStatus( '' )
location_context = self._management_controller.GetVariable( 'location_context' )
location_context = self._management_controller.GetLocationContext()
media_panel = ClientGUIResults.MediaPanelThumbnails( self, self._page_key, location_context, media_results )
@ -2934,7 +2934,7 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
source_management_controller = source_page.GetManagementController()
location_context = source_management_controller.GetVariable( 'location_context' )
location_context = source_management_controller.GetLocationContext()
screen_position = QG.QCursor.pos()

View File

@ -584,7 +584,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea, CAC.Applicatio
return ''
total_duration = sum( ( media.GetDuration() for media in media_source ) )
total_duration = sum( ( media.GetDurationMS() for media in media_source ) )
return HydrusData.ConvertMillisecondsToPrettyTime( total_duration )
@ -856,7 +856,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea, CAC.Applicatio
if HG.client_controller.new_options.GetBoolean( 'focus_preview_on_ctrl_click_only_static' ):
focus_it = media.GetDuration() is None
focus_it = media.GetDurationMS() is None
else:
@ -905,7 +905,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea, CAC.Applicatio
if HG.client_controller.new_options.GetBoolean( 'focus_preview_on_shift_click_only_static' ):
focus_it = media.GetDuration() is None
focus_it = media.GetDurationMS() is None
else:

View File

@ -557,7 +557,9 @@ class EditContentParserPanel( ClientGUIScrolledPanels.EditPanel ):
self._mappings_panel = QW.QWidget( self._content_panel )
self._namespace = QW.QLineEdit( self._mappings_panel )
self._namespace = ClientGUICommon.NoneableTextCtrl( self._mappings_panel, none_phrase = 'any namespace' )
tt = 'The difference between "any namespace" and setting an empty input for "unnamespaced" is "unnamespaced" will force unnamespaced, even if the parsed tag includes a colon. If you are parsing hydrus content and expect to see "namespace:subtag", hit "any namespace", and if you are parsing normal boorus that might have a colon in for weird reasons, try "unnamespaced".'
self._namespace.setToolTip( tt )
#
@ -634,7 +636,7 @@ class EditContentParserPanel( ClientGUIScrolledPanels.EditPanel ):
namespace = additional_info
self._namespace.setText( namespace )
self._namespace.SetValue( namespace )
elif content_type == HC.CONTENT_TYPE_NOTES:
@ -901,7 +903,7 @@ class EditContentParserPanel( ClientGUIScrolledPanels.EditPanel ):
elif content_type == HC.CONTENT_TYPE_MAPPINGS:
namespace = self._namespace.text()
namespace = self._namespace.GetValue()
additional_info = namespace

View File

@ -135,7 +135,7 @@ class LocationSearchContextButton( ClientGUICommon.BetterButton ):
self._all_known_files_allowed_only_in_advanced_mode = False
self._only_importable_domains_allowed = False
self.SetValue( location_context )
self.SetValue( location_context, force_label = True )
def _EditLocation( self ):
@ -237,15 +237,18 @@ class LocationSearchContextButton( ClientGUICommon.BetterButton ):
self._all_known_files_allowed_only_in_advanced_mode = all_known_files_allowed_only_in_advanced_mode
def SetValue( self, location_context: ClientLocation.LocationContext ):
def SetValue( self, location_context: ClientLocation.LocationContext, force_label = False ):
location_context = location_context.Duplicate()
location_context.FixMissingServices( HG.client_controller.services_manager.FilterValidServiceKeys )
if location_context == self._location_context:
if not force_label:
return
if location_context == self._location_context:
return
self._location_context = location_context

View File

@ -912,6 +912,11 @@ class FileSeed( HydrusSerialisable.SerialisableBase ):
def Import( self, temp_path: str, file_import_options: FileImportOptions.FileImportOptions, status_hook = None ):
if file_import_options.IsDefault():
file_import_options = FileImportOptions.GetRealFileImportOptions( file_import_options, FileImportOptions.IMPORT_TYPE_LOUD )
file_import_job = ClientImportFiles.FileImportJob( temp_path, file_import_options )
file_import_status = file_import_job.DoWork( status_hook = status_hook )

View File

@ -107,6 +107,11 @@ class FileImportJob( object ):
HydrusData.ShowText( 'File import job created for path {}.'.format( temp_path ) )
if file_import_options.IsDefault():
file_import_options = FileImportOptions.GetRealFileImportOptions( file_import_options, FileImportOptions.IMPORT_TYPE_LOUD )
self._temp_path = temp_path
self._file_import_options = file_import_options

View File

@ -61,7 +61,17 @@ class FileImportOptions( HydrusSerialisable.SerialisableBase ):
self._associate_primary_urls = True
self._associate_source_urls = True
self._presentation_import_options = PresentationImportOptions.PresentationImportOptions()
self._import_destination_location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.LOCAL_FILE_SERVICE_KEY )
try:
fallback = HG.client_controller.services_manager.GetLocalMediaFileServices()[0].GetServiceKey()
except:
fallback = CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY
self._import_destination_location_context = ClientLocation.LocationContext.STATICCreateSimple( fallback )
self._is_default = False

View File

@ -202,7 +202,7 @@ class Media( object ):
raise NotImplementedError()
def GetDuration( self ) -> typing.Optional[ int ]:
def GetDurationMS( self ) -> typing.Optional[ int ]:
raise NotImplementedError()
@ -1768,7 +1768,7 @@ class MediaCollection( MediaList, Media ):
self._size = sum( [ media.GetSize() for media in self._sorted_media ] )
self._size_definite = not False in ( media.IsSizeDefinite() for media in self._sorted_media )
duration_sum = sum( [ media.GetDuration() for media in self._sorted_media if media.HasDuration() ] )
duration_sum = sum( [ media.GetDurationMS() for media in self._sorted_media if media.HasDuration() ] )
if duration_sum > 0: self._duration = duration_sum
else: self._duration = None
@ -1839,7 +1839,7 @@ class MediaCollection( MediaList, Media ):
def GetDuration( self ):
def GetDurationMS( self ):
return self._duration
@ -2037,9 +2037,9 @@ class MediaSingleton( Media ):
return self
def GetDuration( self ):
def GetDurationMS( self ):
return self._media_result.GetDuration()
return self._media_result.GetDurationMS()
def GetEarliestHashId( self ):
@ -2388,7 +2388,7 @@ class MediaSingleton( Media ):
def HasDuration( self ):
duration = self._media_result.GetDuration()
duration = self._media_result.GetDurationMS()
return duration is not None and duration > 0
@ -2672,7 +2672,7 @@ class MediaSort( HydrusSerialisable.SerialisableBase ):
# videos > images > pdfs
# heavy vids first, heavy images first
duration = x.GetDuration()
duration = x.GetDurationMS()
num_frames = x.GetNumFrames()
size = x.GetSize()
resolution = x.GetResolution()
@ -2745,7 +2745,7 @@ class MediaSort( HydrusSerialisable.SerialisableBase ):
def sort_key( x ):
return deal_with_none( x.GetDuration() )
return deal_with_none( x.GetDurationMS() )
elif sort_data == CC.SORT_FILES_BY_FRAMERATE:
@ -2759,7 +2759,7 @@ class MediaSort( HydrusSerialisable.SerialisableBase ):
return -1
duration = x.GetDuration()
duration = x.GetDurationMS()
if duration is None or duration == 0:

View File

@ -61,7 +61,7 @@ class MediaResult( object ):
return MediaResult( file_info_manager, tags_manager, locations_manager, ratings_manager, notes_manager, file_viewing_stats_manager )
def GetDuration( self ):
def GetDurationMS( self ):
return self._file_info_manager.duration

View File

@ -84,7 +84,7 @@ options = {}
# Misc
NETWORK_VERSION = 20
SOFTWARE_VERSION = 520
SOFTWARE_VERSION = 521
CLIENT_API_VERSION = 42
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -327,14 +327,6 @@ class HydrusDB( HydrusDBBase.DBBase ):
def _AnalyzeTempTable( self, temp_table_name ):
# this is useful to do after populating a temp table so the query planner can decide which index to use in a big join that uses it
self._Execute( 'ANALYZE {};'.format( temp_table_name ) )
self._Execute( 'ANALYZE mem.sqlite_master;' ) # this reloads the current stats into the query planner, may no longer be needed
def _AttachExternalDatabases( self ):
for ( name, filename ) in self._db_filenames.items():

View File

@ -175,6 +175,14 @@ class DBBase( object ):
self._c = None
def _AnalyzeTempTable( self, temp_table_name ):
# this is useful to do after populating a temp table so the query planner can decide which index to use in a big join that uses it
self._Execute( 'ANALYZE {};'.format( temp_table_name ) )
self._Execute( 'ANALYZE mem.sqlite_master;' ) # this reloads the current stats into the query planner, may no longer be needed
def _CloseCursor( self ):
if self._c is not None:

View File

@ -167,6 +167,7 @@ def FilterNamespaces( tags, namespaces ):
return result
def SortNumericTags( tags ):
tags = list( tags )
@ -175,6 +176,7 @@ def SortNumericTags( tags ):
return tags
def CheckTagNotEmpty( tag ):
( namespace, subtag ) = SplitTag( tag )
@ -184,6 +186,7 @@ def CheckTagNotEmpty( tag ):
raise HydrusExceptions.TagSizeException( 'Received a zero-length tag!' )
def CleanTag( tag ):
try:
@ -197,7 +200,14 @@ def CleanTag( tag ):
tag = tag.lower()
tag = HydrusText.re_leading_single_colon.sub( '::', tag ) # Convert anything starting with one colon to start with two i.e. :D -> ::D
tag = HydrusText.re_leading_colons.sub( ':', tag )
if HydrusText.re_leading_single_colon_and_no_more_colons.match( tag ) is not None:
# Convert anything starting with one colon to start with two i.e. :D -> ::D
# but don't do :weird:stuff, which is a forced unnamespaced tag that includes a colon in the subtag
tag = ':' + tag
if ':' in tag:
@ -225,6 +235,7 @@ def CleanTag( tag ):
return tag
def CleanTags( tags ):
@ -253,11 +264,12 @@ def CleanTags( tags ):
return clean_tags
def CombineTag( namespace, subtag ):
def CombineTag( namespace, subtag, do_not_double_namespace = False ):
if namespace == '':
if HydrusText.re_leading_single_colon.search( subtag ) is not None:
if ':' in subtag and HydrusText.re_leading_double_colon.match( subtag ) is None:
return ':' + subtag
@ -268,9 +280,17 @@ def CombineTag( namespace, subtag ):
else:
return namespace + ':' + subtag
if do_not_double_namespace and subtag.startswith( namespace + ':' ):
return subtag
else:
return namespace + ':' + subtag
def ConvertTagSliceToString( tag_slice ):
if tag_slice == '':
@ -292,10 +312,12 @@ def ConvertTagSliceToString( tag_slice ):
return tag_slice
def IsUnnamespaced( tag ):
return SplitTag( tag )[0] == ''
def SplitTag( tag ):
if ':' in tag:

View File

@ -18,7 +18,10 @@ re_one_or_more_whitespace = re.compile( r'\s+' ) # this does \t and friends too
# want to keep the 'leading space' part here, despite tag.strip() elsewhere, in case of some crazy '- test' tag
re_leading_garbage = re.compile( r'^(-|system:)+' )
re_leading_single_colon = re.compile( '^:(?!:)' )
re_leading_single_colon_and_no_more_colons = re.compile( '^:(?=[^:]+$)' )
re_leading_single_colon_and_later_colon = re.compile( '^:(?=[^:]+:[^:]+$)' )
re_leading_double_colon = re.compile( '^::(?!:)' )
re_leading_colons = re.compile( '^:+' )
re_leading_byte_order_mark = re.compile( '^\ufeff' ) # unicode .txt files prepend with this, wew
HYDRUS_NOTE_NEWLINE = '\n'

View File

@ -107,7 +107,6 @@ class TestClientDBDuplicates( unittest.TestCase ):
fake_file_import_job._file_info = ( size, mime, width, height, duration, num_frames, has_audio, num_words )
fake_file_import_job._extra_hashes = ( b'abcd', b'abcd', b'abcd' )
fake_file_import_job._perceptual_hashes = [ perceptual_hash ]
fake_file_import_job._file_import_options = FileImportOptions.FileImportOptions()
self._write( 'import_file', fake_file_import_job )

View File

@ -198,7 +198,6 @@ class TestMigration( unittest.TestCase ):
fake_file_import_job._file_info = ( size, mime, width, height, duration, num_frames, has_audio, num_words )
fake_file_import_job._extra_hashes = ( md5, sha1, sha512 )
fake_file_import_job._perceptual_hashes = [ os.urandom( 8 ) ]
fake_file_import_job._file_import_options = FileImportOptions.FileImportOptions()
self.WriteSynchronous( 'import_file', fake_file_import_job )

View File

@ -1,12 +1,94 @@
import os
import random
import typing
import unittest
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusExceptions
from hydrus.client import ClientParsing
from hydrus.client import ClientStrings
class DummyFormula( ClientParsing.ParseFormula ):
def __init__( self, result: typing.List[ str ] ):
ClientParsing.ParseFormula.__init__( self )
self._result = result
def _GetSerialisableInfo( self ):
return None
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
pass
def _ParseRawTexts( self, parsing_context, parsing_text, collapse_newlines: bool ):
return self._result
def ToPrettyString( self ):
return 'test dummy formula'
def ToPrettyMultilineString( self ):
return 'test dummy formula' + os.linesep + 'returns what you give it'
class TestContentParser( unittest.TestCase ):
def test_mappings( self ):
parsing_context = {}
parsing_text = 'test parsing text'
name = 'test content parser'
# none
dummy_formula = DummyFormula( [ 'character:lara croft', 'double pistols' ] )
additional_info = None
content_parser = ClientParsing.ContentParser( name = name, content_type = HC.CONTENT_TYPE_MAPPINGS, formula = dummy_formula, additional_info = additional_info )
self.assertEqual( ClientParsing.GetTagsFromParseResults( content_parser.Parse( parsing_context, parsing_text ) ), { 'character:lara croft', 'double pistols' } )
# ''
additional_info = ''
content_parser = ClientParsing.ContentParser( name = name, content_type = HC.CONTENT_TYPE_MAPPINGS, formula = dummy_formula, additional_info = additional_info )
self.assertEqual( ClientParsing.GetTagsFromParseResults( content_parser.Parse( parsing_context, parsing_text ) ), { ':character:lara croft', 'double pistols' } )
# character
additional_info = 'character'
content_parser = ClientParsing.ContentParser( name = name, content_type = HC.CONTENT_TYPE_MAPPINGS, formula = dummy_formula, additional_info = additional_info )
self.assertEqual( ClientParsing.GetTagsFromParseResults( content_parser.Parse( parsing_context, parsing_text ) ), { 'character:lara croft', 'character:double pistols' } )
# series
additional_info = 'series'
content_parser = ClientParsing.ContentParser( name = name, content_type = HC.CONTENT_TYPE_MAPPINGS, formula = dummy_formula, additional_info = additional_info )
self.assertEqual( ClientParsing.GetTagsFromParseResults( content_parser.Parse( parsing_context, parsing_text ) ), { 'series:character:lara croft', 'series:double pistols' } )
class TestStringConverter( unittest.TestCase ):
def test_basics( self ):

View File

@ -1687,27 +1687,27 @@ class TestTagObjects( unittest.TestCase ):
self.assertEqual( p.ToString(), 'tag' )
self.assertEqual( p.GetNamespace(), '' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, 'tag', True, count = ClientSearch.PredicateCount.STATICCreateStaticCount( 1, 2 ) )
self.assertEqual( p.ToString( with_count = False ), 'tag' )
self.assertEqual( p.ToString( with_count = True ), 'tag (1) (+2)' )
self.assertEqual( p.GetNamespace(), '' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, 'tag', False )
self.assertEqual( p.ToString(), '-tag' )
self.assertEqual( p.GetNamespace(), '' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, 'tag', False, count = ClientSearch.PredicateCount.STATICCreateStaticCount( 1, 2 ) )
self.assertEqual( p.ToString( with_count = False ), '-tag' )
self.assertEqual( p.ToString( with_count = True ), '-tag (1) (+2)' )
self.assertEqual( p.GetNamespace(), '' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
#
@ -1715,187 +1715,187 @@ class TestTagObjects( unittest.TestCase ):
self.assertEqual( p.ToString(), 'system:import time: since 1 year 2 months ago' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ( CC.UNICODE_ALMOST_EQUAL_TO, 'delta', ( 1, 2, 3, 4 ) ) )
self.assertEqual( p.ToString(), 'system:import time: around 1 year 2 months ago' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ( '>', 'delta', ( 1, 2, 3, 4 ) ) )
self.assertEqual( p.ToString(), 'system:import time: before 1 year 2 months ago' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_ARCHIVE, count = ClientSearch.PredicateCount.STATICCreateCurrentCount( 1000 ) )
self.assertEqual( p.ToString(), 'system:archive (1,000)' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( '<', 200 ) )
self.assertEqual( p.ToString(), 'system:duration < 200 milliseconds' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_EVERYTHING, count = ClientSearch.PredicateCount.STATICCreateCurrentCount( 2000 ) )
self.assertEqual( p.ToString(), 'system:everything (2,000)' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_SERVICE, ( True, HC.CONTENT_STATUS_CURRENT, CC.LOCAL_FILE_SERVICE_KEY ) )
self.assertEqual( p.ToString(), 'system:is currently in my files' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_SERVICE, ( True, HC.CONTENT_STATUS_DELETED, CC.LOCAL_FILE_SERVICE_KEY ) )
self.assertEqual( p.ToString(), 'system:is deleted from my files' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_SERVICE, ( False, HC.CONTENT_STATUS_PENDING, CC.LOCAL_FILE_SERVICE_KEY ) )
self.assertEqual( p.ToString(), 'system:is not pending to my files' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_SERVICE, ( False, HC.CONTENT_STATUS_PETITIONED, CC.LOCAL_FILE_SERVICE_KEY ) )
self.assertEqual( p.ToString(), 'system:is not petitioned from my files' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_AUDIO, True )
self.assertEqual( p.ToString(), 'system:has audio' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_AUDIO, False )
self.assertEqual( p.ToString(), 'system:no audio' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_EXIF, True )
self.assertEqual( p.ToString(), 'system:image has exif' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_EXIF, False )
self.assertEqual( p.ToString(), 'system:no exif' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_HUMAN_READABLE_EMBEDDED_METADATA, True )
self.assertEqual( p.ToString(), 'system:image has human-readable embedded metadata' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_HUMAN_READABLE_EMBEDDED_METADATA, False )
self.assertEqual( p.ToString(), 'system:no human-readable embedded metadata' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_ICC_PROFILE, True )
self.assertEqual( p.ToString(), 'system:image has icc profile' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_ICC_PROFILE, False )
self.assertEqual( p.ToString(), 'system:no icc profile' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HASH, ( ( bytes.fromhex( 'abcd' ), ), 'sha256' ) )
self.assertEqual( p.ToString(), 'system:sha256 hash is abcd' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '<', 2000 ) )
self.assertEqual( p.ToString(), 'system:height < 2,000' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_INBOX, count = ClientSearch.PredicateCount.STATICCreateCurrentCount( 1000 ) )
self.assertEqual( p.ToString(), 'system:inbox (1,000)' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT, 2000 )
self.assertEqual( p.ToString(), 'system:limit is 2,000' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_LOCAL, count = ClientSearch.PredicateCount.STATICCreateCurrentCount( 100 ) )
self.assertEqual( p.ToString(), 'system:local (100)' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_MIME, set( HC.IMAGES ).intersection( HC.SEARCHABLE_MIMES ) )
self.assertEqual( p.ToString(), 'system:filetype is image' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_MIME, ( HC.VIDEO_WEBM, ) )
self.assertEqual( p.ToString(), 'system:filetype is webm' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_MIME, ( HC.VIDEO_WEBM, HC.IMAGE_GIF ) )
self.assertEqual( p.ToString(), 'system:filetype is gif, webm' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_MIME, ( HC.GENERAL_AUDIO, HC.GENERAL_VIDEO ) )
self.assertEqual( p.ToString(), 'system:filetype is audio, video' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NOT_LOCAL, count = ClientSearch.PredicateCount.STATICCreateCurrentCount( 100 ) )
self.assertEqual( p.ToString(), 'system:not local (100)' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ( '*', '<', 2 ) )
self.assertEqual( p.ToString(), 'system:number of tags < 2' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ( 'character', '<', 2 ) )
self.assertEqual( p.ToString(), 'system:number of character tags < 2' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ( '<', 5000 ) )
self.assertEqual( p.ToString(), 'system:number of words < 5,000' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
from hydrus.test import TestController
@ -1903,37 +1903,37 @@ class TestTagObjects( unittest.TestCase ):
self.assertEqual( p.ToString(), 'system:rating for example local rating numerical service > 1/5' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_RATING, ( '>', 3, TestController.LOCAL_RATING_INCDEC_SERVICE_KEY ) )
self.assertEqual( p.ToString(), 'system:rating for example local rating inc/dec service > 3' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO, ( '=', 16, 9 ) )
self.assertEqual( p.ToString(), 'system:ratio = 16:9' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_SIMILAR_TO, ( ( bytes.fromhex( 'abcd' ), ), 5 ) )
self.assertEqual( p.ToString(), 'system:similar to 1 files using max hamming of 5' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE, ( '>', 5, 1048576 ) )
self.assertEqual( p.ToString(), 'system:filesize > 5MB' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( '=', 1920 ) )
self.assertEqual( p.ToString(), 'system:width = 1,920' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
#
@ -1941,13 +1941,13 @@ class TestTagObjects( unittest.TestCase ):
self.assertEqual( p.ToString(), 'series:*anything*' )
self.assertEqual( p.GetNamespace(), 'series' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, 'series', False )
self.assertEqual( p.ToString(), '-series' )
self.assertEqual( p.GetNamespace(), '' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
#
@ -1955,13 +1955,13 @@ class TestTagObjects( unittest.TestCase ):
self.assertEqual( p.ToString(), 'a*i:o* (wildcard search)' )
self.assertEqual( p.GetNamespace(), '*' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, 'a*i:o*', False )
self.assertEqual( p.ToString(), '-a*i:o*' )
self.assertEqual( p.GetNamespace(), '*' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
#
@ -1969,7 +1969,7 @@ class TestTagObjects( unittest.TestCase ):
self.assertEqual( p.ToString(), ' series:game of thrones' )
self.assertEqual( p.GetNamespace(), 'series' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), 'namespace', p.GetNamespace() ) ] )
#
@ -1980,11 +1980,11 @@ class TestTagObjects( unittest.TestCase ):
or_texts_and_namespaces = []
or_texts_and_namespaces.append( ( 'blue eyes', '' ) )
or_texts_and_namespaces.append( ( ' OR ', 'system' ) )
or_texts_and_namespaces.append( ( 'character:samus aran', 'character' ) )
or_texts_and_namespaces.append( ( ' OR ', 'system' ) )
or_texts_and_namespaces.append( ( 'system:height < 2,000', 'system' ) )
or_texts_and_namespaces.append( ( 'blue eyes', 'namespace', '' ) )
or_texts_and_namespaces.append( ( ' OR ', 'or', 'system' ) )
or_texts_and_namespaces.append( ( 'character:samus aran', 'namespace', 'character' ) )
or_texts_and_namespaces.append( ( ' OR ', 'or', 'system' ) )
or_texts_and_namespaces.append( ( 'system:height < 2,000', 'namespace', 'system' ) )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), or_texts_and_namespaces )
@ -2125,3 +2125,27 @@ class TestTagObjects( unittest.TestCase ):
self.assertEqual( tag_autocomplete_options.GetExactMatchCharacterThreshold(), 2 )
class TestTagRendering( unittest.TestCase ):
def test_rendering( self ):
self.assertEqual( ClientTags.RenderTag( 'tag', True ), 'tag' )
self.assertEqual( ClientTags.RenderTag( 'test_tag', True ), 'test_tag' )
HG.test_controller.new_options.SetBoolean( 'replace_tag_underscores_with_spaces', True )
self.assertEqual( ClientTags.RenderTag( 'test_tag', True ), 'test tag' )
HG.test_controller.new_options.SetBoolean( 'replace_tag_underscores_with_spaces', False )
self.assertEqual( ClientTags.RenderTag( 'character:lara', True ), 'character:lara' )
HG.test_controller.new_options.SetBoolean( 'show_namespaces', False )
self.assertEqual( ClientTags.RenderTag( 'character:lara', True ), 'lara' )
HG.test_controller.new_options.SetBoolean( 'show_namespaces', True )

View File

@ -66,6 +66,7 @@ from hydrus.test import TestHydrusNetworking
from hydrus.test import TestHydrusSerialisable
from hydrus.test import TestHydrusServer
from hydrus.test import TestHydrusSessions
from hydrus.test import TestHydrusTags
from hydrus.test import TestServerDB
DB_DIR = None
@ -789,6 +790,7 @@ class Controller( object ):
TestClientImageHandling,
TestClientMetadataMigration,
TestClientMigration,
TestHydrusTags,
TestHydrusServer
]
@ -814,6 +816,7 @@ class Controller( object ):
TestClientThreading,
TestFunctions,
TestHydrusData,
TestHydrusTags,
TestHydrusSerialisable,
TestHydrusSessions
]

View File

@ -0,0 +1,22 @@
import unittest
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusTags
from hydrus.core import HydrusGlobals as HG
class TestHydrusTags( unittest.TestCase ):
def test_cleaning_and_combining( self ):
self.assertEqual( HydrusTags.CleanTag( ' test ' ), 'test' )
self.assertEqual( HydrusTags.CleanTag( ' character:test ' ), 'character:test' )
self.assertEqual( HydrusTags.CleanTag( ' character : test ' ), 'character:test' )
self.assertEqual( HydrusTags.CleanTag( ':p' ), '::p' )
self.assertEqual( HydrusTags.CombineTag( '', ':p' ), '::p' )
self.assertEqual( HydrusTags.CombineTag( '', '::p' ), '::p' )
self.assertEqual( HydrusTags.CombineTag( '', 'unnamespace:withcolon' ), ':unnamespace:withcolon' )

View File

@ -12,8 +12,9 @@ nav:
- getting_started_installing.md
- getting_started_files.md
- getting_started_importing.md
- getting_started_searching.md
- getting_started_tags.md
- getting_started_searching.md
- getting_started_more_tags.md
- getting_started_downloading.md
- getting_started_ratings.md
- PTR:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB