Merge branch 'develop'

This commit is contained in:
Hydrus Network Developer 2024-04-17 15:36:49 -05:00
commit ef01ac9bc7
No known key found for this signature in database
GPG Key ID: 76249F053212133C
50 changed files with 934 additions and 577 deletions

View File

@ -17,7 +17,7 @@ jobs:
name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.11'
architecture: x64
-
name: APT Install

View File

@ -15,7 +15,7 @@ jobs:
name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.11'
-
name: Build docs to /help
run: |
@ -71,7 +71,7 @@ jobs:
run: |
cd $GITHUB_WORKSPACE
temp_dmg="$(mktemp).dmg"
hdiutil create "$temp_dmg" -ov -volname "HydrusNetwork" -fs HFS+ -format UDZO -srcfolder "$GITHUB_WORKSPACE/build/$(head -n 1 triple.txt)/release"
hdiutil create "$temp_dmg" -ov -volname "HydrusNetwork" -fs HFS+ -format ULFO -srcfolder "$GITHUB_WORKSPACE/build/$(head -n 1 triple.txt)/release"
mv "$temp_dmg" Hydrus.Network.${{ env.version_short }}.-.macOS.-.App.dmg
-
name: Upload Files

View File

@ -17,7 +17,7 @@ jobs:
name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.11'
architecture: x64
-
name: Pip Install

View File

@ -0,0 +1,60 @@
*** The purpose of this document ***
This will teach you how to figure out what happened if your database seems to have disappeared or reset back to empty.
*** This looks like the first time you have run the program ***
If you boot your normal client and it creates the "This looks like the first time you have run the program" popup, and/or all your files and settings seem to have disappeared, there are some common causes for this, usually hydrus looking in the wrong place.
First off, know that hydrus will never intentionally delete your database. It can lose track of it, but there is no 'wipe this database folder' tech. Similarly, as you work to fix this problem, make sure you do not delete or overwrite anything yourself--it is easy to get mixed up and accidentally delete something important. Get yourself working again, make sure you have backups of what matters, and then clean the mess involved in the cleanup.
A database consists of four files--client.db, client.caches.db, client.mappings.db, and client.master.db, all in the same folder. A new database will have .db files that are a few hundred KB each, but a well-used database will typically be hundreds of MB to dozens of GB. Also, obviously, an older database's files will have much older file creation dates (and modified dates, most likely).
In the following scenarios, you may be uncertain where your real database actually is. Try simply searching your entire system for the fairly unusually named 'client.mappings.db' and/or the other filenames. Don't start with 'client.db', since several other programs use that filename too. Make sure to search your trash/recycle bin too!
*** Scenario One: Hydrus is looking in the wrong place ***
When hydrus boots, it has a default database location. For Windows and Linux, this is "install_dir/db", the same folder where this document is typically located. If you launch the program with the --db_dir launch parameter, you change this.
Hydrus will check that it has 'write access' to the folder it wants to use, which a read-only location (e.g. "C:\Program Files" on Windows) will fail. If hydrus cannot write to its given db folder, or if there is another similar permission problem, the client will fall back to a secondary default, "~/Hydrus", which on Windows will be "C:\Users\[You]\Hydrus", and on Linux will be something like "/home/[You]/Hydrus".
So, if you have a normal client install and after an update or a hard drive migration it suddenly boots empty, it could be that the original location is now read only, or otherwise strangely inaccessible, and the client assumed it was supposed to be making a new database in your home directory. It could also be the reverse--that you were unknowingly using the home directory all along, and your suddenly write-ok db dir now has a fresh database in it!
How do we figure this out? First off, check "help->about" in the client. That will show your install and database directories. Is the database directory what you expect, or has it moved? Open that location ("file->open->database directory"), and compare it to the location you thought you were using. Check your home directory. If you find your old db, think about why hydrus has moved--is the original location now read-only, did you recently alter/add/forget the db_dir launch parameter in a custom shortcut, are you using clever symlinks to host a NAS location locally, and things are a bit tangled?--then try to fix the problem. Simple is always better.
For macOS Apps, the default location is "~/Library/Hydrus", and there are no fallbacks. You likely will not run into this problem unless you are messing around with the source version or the --db_dir launch parameter on the App.
*** Scenario Two: The database has been overwritten ***
Since hydrus stores its database directory inside itself, there is one dangerzone action you can take during an update, especially an unusual update that you want to test for some reason. Let's say you extract the program to your desktop and run it once, to test that it boots; once it works, you then drag that folder on top of your normal install to update. Can you predict what disaster will happen? If you run the program on your desktop, it will create new and empty database files (client.db etc...), and then when you do the overwrite, the empty database will overwrite your real one!
Thus, when you update manually, by overwriting your install with an extract, you must always do a fresh extract. Straight from the archive file is best.
There is no way to fix this problem unless you have a backup.
*** Scenario Three: Hard drive failure ***
You can usually assume that an entirely missing database is not a hardware problem, but in odd cases, it might be. Failing hard drives tend to delete things at random, so you'd probably get a half-way boot and then lots of errors from one .db file disappearing, rather than all four at once. However you might, rarely, lose the whole 'db' directory in one go, or if you host on an external partition and that fails, then hydrus might fall back to "~/Hydrus". Same deal if your NAS goes weird, or if a drive gets a 'dirty bit' set and is now totally read-only. As before, check where you think the database should be and where it might have moved and figure out which is which and why the original is not available.
*** Scenario Four: The database has been moved ***
Hydrus will never move or delete your database, but a crazy anti-virus, live cloud backup, drive maintenance/error handling, or other third-party application may do it. Although it sounds stupid, you, the user, might have misclicked without realising or deleted the wrong copy of something as you were migrating something. Simply search your whole system for 'client.mappings.db', and don't forget to search your trash--it happens!
If you can figure out what moved the db, add whatever 'exclude' directory to that software you need to to stop it happening again.
*** Repairing the database ***
The ideal answer to your problem is you restore the database to where it should be, or fix your shortcut, or fix your permission problem, and everything works again. If your situation is more complicated, for instance you had hard drive damage and were able to recover a good client.db, client.caches.db, and client.master.db but not a good client.mappings.db, then you should check the 'help my db is broke.txt' document, which should be beside this one, and contact me, hydev, if that doesn't cover it!
*** The importance of backups ***
I talk about backups a lot. It sounds silly to worry about overwriting something by accident until it happens to you. Hydrus databases are huge stores of information that contain hundreds or thousands of hours of work. Losing all that does not feel great, so it is worth putting the effort in. Do a backup before you update, every time. Do a backup every week regardless of whether you update, and not just your hydrus, but all your documents and photos and all else. What might have been wiping out years and years of work just becomes a 'Ah, damn, that was wrong, START RESTORE'.
https://hydrusnetwork.github.io/hydrus/after_disaster.html

View File

@ -249,3 +249,5 @@ I say this to everyone who goes through this: losing data sucks, I know. I lost
If you do not have a backup routine, this is the time to sort it out. If you cannot afford a USB drive, find a friend or family member who can sell you a USB stick for a few bucks. At worst, simply make a copy on the same drive. At the very least, you just need the spare space for the client*.db files. If you have enough money for an ongoing computer budget, and enough IQ to figure out and stick to a schedule, just fold it into your calendar. Rotating in a new WD Passport every few years is not an outrageous expense, and you can protect every document, childhood photo, ancient-and-now-impossible-to-find 32-bit utility, and your hydrus db, and you can sling it in your backpack and no longer have to worry about losing it all in a fire. If everything goes black one day, you still get a heart attack, but the maximum damage is money and stress and a week of work lost, rather than losing everything you have ever done. A backup is an insurance policy.
Check out the hydrus 'getting started with installing/updating/backing up' help for some more info about routines and programs like FreeFileSync.
https://hydrusnetwork.github.io/hydrus/after_disaster.html

View File

@ -56,3 +56,5 @@ Note these files do not have URLs, so if you are an advanced user and try to run
You should now be done! If you get weird file counts on autocomplete results or more missing file errors, let me know.
I recommend you take a breath, pour yourself a drink, and make a job for tomorrow to think about your backup routine.
https://hydrusnetwork.github.io/hydrus/after_disaster.html

View File

@ -7,6 +7,36 @@ title: Changelog
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
## [Version 571](https://github.com/hydrusnetwork/hydrus/releases/tag/v571)
### clean install
* the recent 'future build' test went well, so I am rolling these updates into the normal release for everyone. on Windows and Linux, the built program is now running Python 3.11, and, on all platforms, updated versions of Qt (UI) and OpenCV (image-processing). there's nothing earth-shattering about these changes, but some things will work better and faster
* **because of the jump, v570 and v571 have dll conflicts! if you are on Windows or Linux and use the .zip or .tar.zst "Extract" release, you will need to a clean install as here**: https://hydrusnetwork.github.io/hydrus/getting_started_installing.html#clean_installs
* **if you are a Windows installer/macOS App/source user, you do not need to do a clean install, just update as normal**
### misc
* when you finish an archive/delete filter and there are several domains you could delete from, the 'commit' buttons are now disabled for 1.2 seconds. this catches you from accidentally spamming enter through a surprise complicated decision
* under _options->files and trash_, you can now say 'when finishing filtering, always delete from all possible domains', which makes the above decision always single domain. hit this if you do want to spam through this and are fine always deleting from everywhere
* the client will now, by default, attempt to load truncated images. this was previously off until you set it per-session-on in a debug menu, but is now a checkbox under _options->media_. some weird damaged jpegs and pngs should now load, fingers crossed
* the 'load images with PIL' setting is now default on for new users and no longer IN TESTING
* every normal single column text list across the program now copies text better if you explicitly hit ctrl+c/ctrl+insert. they now copy all selected rows (rather than just one), and when the display text differs from the underlying data/sort text, you'll now get the sort text (e.g. on manage urls launched on multiple files, you might see 'site.com/123456 (2)', but now, when it copies, that ' (1)' display cruft is omitted). I spammed this to 22 locations and tested 2 so there are definitely no weird string copy bugs anywhere
* fixed an issue opening/closing manage parsers, url classes, or url class links if you have url classes with invalid example urls or critically missing default values in your storage
* the server has a new 'restart_services' command, only triggerable by an admin with service modification ability, which tells all the services on all ports to stop and restart. if there's a new ssl cert, they load the new one
### client api
* the 'associate urls' command has a new 'normalise_urls' parameter (default true, which was the behaviour before) to let you force-add un-normalised URLs or URIs or whatever
* added some unit tests to test this new param
* client api version is now 64
### help docs
* wrote a new help document, 'help my db disappeared.txt' for the db directory that tells you what to do if you boot one day and suddenly get the 'this looks like the first time you ran this program' popup
* clarified the Windows 'running from source' help a little around 'git' and added a 'here is the Python version you want' link for Win 7 users
* gave the install help a very light pass, just fixing and updating a few things here and there. I also warn Linux users that the AUR package may throw errors if Arch updates a Qt library or something before we have had a chance to test it (as we have seen a couple times recently), and I generally suggest AUR people run from source manually if they can
## [Version 570](https://github.com/hydrusnetwork/hydrus/releases/tag/v570)
### UI stuff
@ -369,100 +399,3 @@ title: Changelog
* did a scattering of the clientinterface typing, getting a feel for where I want to take this
* deleted the old in-client server-test's 'boot' variant; this is no longer used and was always super hacky to maintain
* I removed an old basic error raising routine that would sometimes kick in when a hash definition is missing. this routine now always fills in the missing data with garbage and does its best to recover the invalid situation automatically, with decent logging, while still informing the user that things are well busted m8. it isn't the user's job to fix this, and there is no good fix anyway, so no point halting work and giving it to the user to figure out!
## [Version 561](https://github.com/hydrusnetwork/hydrus/releases/tag/v561)
### rearranging thumbnails
* on the thumbnail menu, there is a new 'move' submenu. you can move the current selection of files to the start or end of the media list, or to one before or after the earliest selected file, or to the file you right-clicked on to create the menu, or to the first file's position if the selection is not contiguous. if the selection is non-contiguous, it will be made so in the move
* added these rearrange commands to the shortcuts system, as 'move thumbnails' under the 'thumbnails' set. I wasn't sure whether to add some default shortcuts, like ctrl+numpad 7/3/4/6 for home/end/left/right or something--let me know what you think
### misc
* thanks to user help, fixed a stupid typo from last week that caused some bad errors (including crashes, in some cases) when doing non-simple duplicate filtering (issue #1514). this is the issue the v560a hotfix was made for
* fixed another stupid content update typo that was causing 'already in db' results to not get metadata updates
* as a hardcoded shortcut, Ctrl+C or Ctrl+Insert now copies the currently selected tags in any taglist. it'll output the full tag/predicate text, with namespace, no counts
* I've shortened some thumbnail/media-viewer menu labels, made the 'delete' line into a submenu, and ensured the top info line is always a short variant, with detailed info bumped off to the submenu off the top line. I hate how these menus are often super-wide and thus a pain to navigate to the submenus, so let me know what situations still make them wide
* the file log arrow button menu now has entries for 'delete already in db' and 'delete everything'
* the 'add these tags to the favourites list?' yes/no now only fires if you try to add more than five tags ot once
* the various dialogs in the client that auto-yes or auto-no now show a live countdown in their title string
* the window position saving system is now stricter about what it records. maximised and fullscreen state is only saved if 'remember size' is false, and the last size/position is not saved at all if 'remember size/position' is false (previously, it would save these values but not restore them, but let's try being more precise here)
* fixed a 'omg what happened, closing the window now' error in the duplicate filter if you try to 'go back' while it is loading a new set of pairs to show
* fixed the 'vacuum db' command to correctly save 'last vacuumed time' for all files vacuumed in a job, not just the last!
* whenever a `copy2` file copy (which includes copying file times and permission bits) fails for permission reasons, hydrus now falls back to a normal `copy` and logs the failure, including the modified time that failed to copy (which is the bit we actually care about here)
### db update stuff
* if there is a known bitrot issue on update, you now get a nicer error message. rather than the actual error, you are now told which version is safe to update to. to christen this system, I've added a check for the recent millisecond timestamp conversion, which caused some issues for users updating older clients. **if your client is v551 or older and you try to update to v561 or later, you will be told to update to v558 first.** sorry for the inconvenience here, and thank you for the reports (issue #1512)
* if you try to boot a database more than 50 versions earlier than the code, the client-based version popups now happen in the correct order, with the >50 exception firing before the >15 warning
* when an update asks a not-super-important yes/no question, I will now make it auto-yes or auto-no after ten minutes with the recommended value. this will ensure that automatic updaters will still progress (previously, they were hanging forever!)
### some downloader stuff
* thanks to a user, the derpibooru now fetches the post description as a note and the source as an associable URL. I tweaked the submitted stuff a bit, simplifying the parsing and discluding 'No description provided.' notes
* thanks to a user, the e621 parser can now grab files from posts where the (spicy, I think) content is normally not shown due to a guest login. the posts still won't show up in guest-login gallery searches, so this won't alter your normal results, but if you run into a post like this in your browser and drag-and-drop it onto the client, it now works
* I tried to improve the parsing system's de-newlining. this thing is a long-time hack--I've never liked it and I want to replace it with proper multi-line support--but for now I've made sure the de-newliner strips each line of leading/trailing whitespace and discards empty lines. the mode that _doesn't_ collapse newlines (note parsing, for the most part) now _does_ strip leading/trailing newlines along with other whitespace, meaning you no longer have to try and strip extra `<p>` and `<br>` tags and stuff yourself when grabbing notes. also, the formula UI where it says 'Newlines are collapsed before...' now says when it won't be collapsing newlines due to it being a note parser
* the String Match processing step now explicitly removes newlines before it runs, meaning it can still catch multi-line notes properly. you can now run a proper regex on a multi-line note
### boring cleanup
* optimised some thumbnail handling code, stuff like fetching the current list of sorted selected media
* large collections will be a little faster to select and otherwise do operations on
* sketched out a new `ClientGlobals` and client controller interface and started refactoring various HG.client_controller to the new CG. this makes no important running changes, but it cleans the messy HG file and will help future coding and type checking in the IDE as it is fleshed out
* added some help text to the edit file maintenance panel and fixed some gonk layout in the 'add new work' panel
* fixed some instances of the 'unknown' import status showing as a blank string
* fixed an error message in the export folder export job that fired when a file to be exported is missing--it was just giving blank instead of the file hash, and its direction to file maintenance was old and unclear
## [Version 560](https://github.com/hydrusnetwork/hydrus/releases/tag/v560)
### editing times for multiple files
* the 'edit times' dialog is now available when you select multiple files. it will show and apply time data for all of those files at once. when the files have different times, the various widgets and panels will show ranges and a count of how many files do and don't have that particular time type
* when you open the edit times dialog on more than one file, every time control now has a 'cascade step' section, where you can set a time delta, e.g. 100 milliseconds, and then, on dialog ok, each file in the selection that launched the dialog will be set that much successively later than the previous, obviously in the order they are currently in. this is a way of forcing/normalising file sorts based on time. negative values are allowed!
* when the edit times dialog is set to change more than 100 total times, it now verifies with the user that this is correct on dialog ok
* when the edit times dialog sets a lot of modified dates to files (i.e. actually writing them to your file system), this now happens in a non-gui thread and now makes a cancellable progress popup after a few seconds
### misc
* fixed the 'imported to' timestamp for files migrated to other local file domains, which were one of the ones incorrectly set, as expected, to 54 years ago. in the database update, I also fix all the wrongly saved ones from v559
* mr bones and the file history chart are now under the 'database' menu
* fixed an issue with the file history chart not maintaining the `show_deleted = False` state through search refreshes
* there's a new checkbox under `files and trash`, `Remove files from view when they are moved to another local file domain`. this re-introduces the unintended behaviour that I fixed recently when 'remove when trashed' was set, but now targeted specifically for that situation. if you use multiple local file domans a bunch and want files to disappear when you shoot them to a place you aren't looking at, give it a go and let me know how it works for you
* fixed a regression from my 'remove when trashed' fix where deleting collections with this option on would leave crazy ghost thumbnails behind. collections that are completely emptied should now properly remove themselves in all content update situations
* the gallery downloader page 'cog' icon now has a 'do not allow new duplicates' option, which will discard any (query_text,source) pairs you try to enter if they already exist in the list. this option is remembered through restarts
* added 'sort by pixel hash' to the file sort menu. it isn't super helpful, but it'll show pairs of exact-matching files next to each other amongst a sea of noise. I may expose perceptual hashes in a similar way in future, which would be more useful, but thumbnails don't have their phashes quickly available atm, so maybe only when there are other reasons to add that overhead
* fixed the `setup_venv.sh` and `setup_venv.command` files' custom qtpy and PySide6 (Qt stuff) version installer! there was a dumb typo, sorry for the trouble
* thanks to a user, the derpibooru parser now grabs `fanfic`, `spoiler`, and `error` tags
### boring cleanup
* neatened up how non-thumbnail-generatable files (e.g. rtf) present their default thumbs and refactored the code a little
* when a file's thumbnail is unavailable but the filetype is known (e.g. you are looking at records of deleted files that have no blurhash), hydrus should now deliver that file's default thumb instead
* unified this thumbnail-defaulting code a little more, fixing fetching for some weirder files and deduplicating some messy areas. the client thumbnail cache should be better about delivering the right unusual thumbnail now and as future filetypes are added
* added an 'image.png' to serve as a nicer fallback for various thumbnail-undeliverable but known-image files
* fixed rtf files not providing their rtf thumbnail in the Client API
* fixed up some ancient local booru thumbnail fetching code
* cleaned up some messy dialog launches that were having to navigate single/collected media in an awkward way
* removed the TestFunctions unit test stub, which was of diminishing use
### boring cleanup, time code
* updated the DateTime control and button to handle multiple times at once, and updated the edit timestamps dialog itself similarly throughout (this took a day and a half lol)
* rejiggered the DateTime widgets to handle a nice new object to hold the multiple times' range, since it was all getting messy
* rejiggered the time content update pipeline from top to bottom to take multiple hashes per content update, so applying the same timestamp to a thousand files should still be pretty quick
* fixed up various timestamp_ms->QtDateTime conversions so they all include local timezone info. also fixed the datetime widget so it returns properly local-timezone'd datetimes. I can no longer easily reproduce a particular time that jumps an hour every time you open it (due to retroactive summer-time fun)
* harmonised some older datestring conversions to come out 2023-06-30 instead of 2023/06/30
* fixed some time string calculations to handle our new sub-second times better
* updated the time delta widget to handle negative numbers
### boring cleanup, content updates
* moved all `ContentUpdate` gubbins out of the hydrus module scope; it is now client only
* made a new `ClientContentUpdates.py` to collect all content update code and refactored stuff there
* wrote a new `ContentUpdatePackage` to replace the ancient `service_keys_to_content_updates` structure. various hacky or ad-hoc processing and presentation is now gathered under this new object, and I refactor-spammed it across the program, with too many individual changes to talk about in detail
### client api
* the new `set_time` call has some additional safety rails. you can add (or delete) 'web domain' timestamps any time, but you now cannot add or delete any of the others, only edit when they already exist
* updated the client api unit tests and help to account for this
* the client api is now version 60

View File

@ -886,8 +886,6 @@ Response:
}
```
### **POST `/add_urls/associate_url`** { id="add_urls_associate_url" }
_Manage which URLs the client considers to be associated with which files._
@ -902,23 +900,29 @@ Required Headers:
Arguments (in JSON):
:
* [files](#parameters_files)
* `url_to_add`: (optional, selective A, an url you want to associate with the file(s))
* `urls_to_add`: (optional, selective A, a list of urls you want to associate with the file(s))
* `url_to_delete`: (optional, selective B, an url you want to disassociate from the file(s))
* `urls_to_delete`: (optional, selective B, a list of urls you want to disassociate from the file(s))
* [files](#parameters_files)
* `normalise_urls`: (optional, default true, only affects the 'add' urls)
The single/multiple arguments work the same--just use whatever is convenient for you.
Unless you really know what you are doing, I strongly recommend you stick to associating URLs with just one single 'hash' at a time. Multiple hashes pointing to the same URL is unusual and frequently unhelpful.
By default, anything you throw at the 'add' side will be normalised nicely, but if you need to add some specific/weird URL text, or you need to add a URI, set `normalise_urls` to `false`. Anything you throw at the 'delete' side will not be normalised, so double-check you are deleting exactly what you mean to via [GET /get\_files/file\_metadata](#get_files_file_metadata) etc..
The single/multiple arguments work the same--just use whatever is convenient for you. Unless you really know what you are doing with URL Classes, I strongly recommend you stick to associating URLs with just one single 'hash' at a time. Multiple hashes pointing to the same URL is unusual and frequently unhelpful.
```json title="Example request body"
{
"url_to_add" : "https://rule34.xxx/index.php?id=2588418&page=post&s=view",
"hash" : "3b820114f658d768550e4e3d4f1dced3ff8db77443472b5ad93700647ad2d3ba"
}
```
Response:
: 200 with no content. Like when adding tags, this is safely idempotent--do not worry about re-adding URLs associations that already exist or accidentally trying to delete ones that don't.
## Editing File Tags

View File

@ -61,7 +61,7 @@ I try to release a new version every Wednesday by 8pm EST and write an accompany
* You can also try [running the Windows version in wine](wine.md).
* **Third parties (not maintained by Hydrus Developer)**:
* (These both run from source, so if you have trouble with the built release, they may work better for you!)
* [AUR package](https://aur.archlinux.org/packages/hydrus/)
* [AUR package](https://aur.archlinux.org/packages/hydrus/) - *Although please note that since AUR packages work off your system python, this has been known to cause issues when Arch suddenly updates to the latest Qt or something before we have had a chance to test things and it breaks hydrus. If you can, try just [running from source](running_from_source.md) yourself instead, where we can control things better!*
* [flatpak](https://flathub.org/apps/details/io.github.hydrusnetwork.hydrus)
@ -71,7 +71,7 @@ I try to release a new version every Wednesday by 8pm EST and write an accompany
=== "From Source"
* You can also [run from source](running_from_source.md). This is often the best way to fix compatibility problems.
* You can also [run from source](running_from_source.md). This is often the best way to fix compatibility problems, and it is the most pleasant way to run and update the program (you can update in five seconds!), although it requires a bit more work to set up the first time. It is not too complicated to do, though--my guide will walk you through each step.
By default, hydrus stores all its data—options, files, subscriptions, _everything_—entirely inside its own directory. You can extract it to a usb stick, move it from one place to another, have multiple installs for multiple purposes, wrap it all up inside a truecrypt volume, whatever you like. The .exe installer writes some unavoidable uninstall registry stuff to Windows, but the 'installed' client itself will run fine if you manually move it.
@ -161,7 +161,7 @@ Clients and servers of different versions can usually connect to one another, bu
## Clean installs
**This is usually only relevant if you know you have a dll conflict or otherwise update and cannot boot at all.**
**This is usually only relevant if you know you have a dll conflict or otherwise update and cannot boot at all. It usually only applies to Windows or Linux users who manually update using the 'Extract' releases.**
Very rarely, hydrus needs a clean install. This can be due to a special update like when we moved from 32-bit to 64-bit or needing to otherwise 'reset' a custom install situation. The problem is usually that a library file has been renamed in a new version and hydrus has trouble figuring out whether to use the older one (from a previous version) or the newer.
@ -172,10 +172,12 @@ However, you need to be careful not to delete your database! It sounds silly, bu
* Make a backup if you can!
* Go to your install directory.
* Delete all the files and folders except the 'db' dir (and all of its contents, obviously).
* Reinstall/extract hydrus as you normally do.
* Extract the new version of hydrus as you normally do.
After that, you'll have a 'clean' version of hydrus that only has the latest version's dlls. If hydrus still will not boot, I recommend you roll back to your last working backup and let me, hydrus dev, know what your error is.
*Note that macOS App users will not ever have to do a clean install because every App is self-contained and non-merging with previous Apps. Source users similarly do not have to worry about this issue, although if they update their system python, they'll want to recreate their venv. Windows Installer users basically get a clean install every time, so they don't have to worry about this.*
## Big updates
If you have not updated in some time--say twenty versions or more--doing it all in one jump, like v250->v290, is likely not going to work. I am doing a lot of unusual stuff with hydrus, change my code at a fast pace, and do not have a ton of testing in place. Hydrus update code often falls to [bitrot](https://en.wikipedia.org/wiki/Software_rot), and so some underlying truth I assumed for the v255->v256 code may not still apply six months later. If you try to update more than 50 versions at once (i.e. trying to perform more than a year of updates in one go), the client will give you a polite error rather than even try.
@ -184,7 +186,7 @@ As a result, if you get a failure on trying to do a big update, try cutting the
If you narrow the gap down to just one version and still get an error, please let me know. I am very interested in these sorts of problems and will be happy to help figure out a fix with you (and everyone else who might be affected).
_All that said, and while updating is complex and every client is different, various user reports over the years suggest this route works and is efficient: 204 > 238 > 246 > 291 > 328 > 335 > 376 > 421 > 466 > 474 ? 480 > 521 ? 558_
_All that said, and while updating is complex and every client is different, various user reports over the years suggest this route works and is efficient: 204 > 238 > 246 > 291 > 328 > 335 > 376 > 421 > 466 > 474 ? 480 > 521 > 527 (clean install) ? 558 > 571 (clean install)_
## Backing up

View File

@ -34,6 +34,31 @@
<div class="content">
<h1 id="changelog"><a href="#changelog">changelog</a></h1>
<ul>
<li>
<h2 id="version_571"><a href="#version_571">version 571</a></h2>
<ul>
<li><h3>clean install</h3></li>
<li>the recent 'future build' test went well, so I am rolling these updates into the normal release for everyone. on Windows and Linux, the built program is now running Python 3.11, and, on all platforms, updated versions of Qt (UI) and OpenCV (image-processing). there's nothing earth-shattering about these changes, but some things will work better and faster</li>
<li>**because of the jump, v570 and v571 have dll conflicts! if you are on Windows or Linux and use the .zip or .tar.zst "Extract" release, you will need to a clean install as here**: https://hydrusnetwork.github.io/hydrus/getting_started_installing.html#clean_installs</li>
<li>**if you are a Windows installer/macOS App/source user, you do not need to do a clean install, just update as normal**</li>
<li><h3>misc</h3></li>
<li>when you finish an archive/delete filter and there are several domains you could delete from, the 'commit' buttons are now disabled for 1.2 seconds. this catches you from accidentally spamming enter through a surprise complicated decision</li>
<li>under _options-&gt;files and trash_, you can now say 'when finishing filtering, always delete from all possible domains', which makes the above decision always single domain. hit this if you do want to spam through this and are fine always deleting from everywhere</li>
<li>the client will now, by default, attempt to load truncated images. this was previously off until you set it per-session-on in a debug menu, but is now a checkbox under _options-&gt;media_. some weird damaged jpegs and pngs should now load, fingers crossed</li>
<li>the 'load images with PIL' setting is now default on for new users and no longer IN TESTING</li>
<li>every normal single column text list across the program now copies text better if you explicitly hit ctrl+c/ctrl+insert. they now copy all selected rows (rather than just one), and when the display text differs from the underlying data/sort text, you'll now get the sort text (e.g. on manage urls launched on multiple files, you might see 'site.com/123456 (2)', but now, when it copies, that ' (1)' display cruft is omitted). I spammed this to 22 locations and tested 2 so there are definitely no weird string copy bugs anywhere</li>
<li>fixed an issue opening/closing manage parsers, url classes, or url class links if you have url classes with invalid example urls or critically missing default values in your storage</li>
<li>the server has a new 'restart_services' command, only triggerable by an admin with service modification ability, which tells all the services on all ports to stop and restart. if there's a new ssl cert, they load the new one</li>
<li><h3>client api</h3></li>
<li>the 'associate urls' command has a new 'normalise_urls' parameter (default true, which was the behaviour before) to let you force-add un-normalised URLs or URIs or whatever</li>
<li>added some unit tests to test this new param</li>
<li>client api version is now 64</li>
<li><h3>help docs</h3></li>
<li>wrote a new help document, 'help my db disappeared.txt' for the db directory that tells you what to do if you boot one day and suddenly get the 'this looks like the first time you ran this program' popup</li>
<li>clarified the Windows 'running from source' help a little around 'git' and added a 'here is the Python version you want' link for Win 7 users</li>
<li>gave the install help a very light pass, just fixing and updating a few things here and there. I also warn Linux users that the AUR package may throw errors if Arch updates a Qt library or something before we have had a chance to test it (as we have seen a couple times recently), and I generally suggest AUR people run from source manually if they can</li>
</ul>
</li>
<li>
<h2 id="version_570"><a href="#version_570">version 570</a></h2>
<ul>

View File

@ -25,6 +25,8 @@ There are now setup scripts that make this easy on Windows and Linux. You do not
=== "Windows"
**First of all, you will need git.** If you are just a normal Windows user, you will not have it. Get it:
??? info "Git for Windows"
Git is an excellent tool for synchronising code across platforms. Instead of downloading and extracting the whole .zip every time you want to update, it allows you to just run one line and all the code updates are applied in about three seconds. You can also run special versions of the program, or test out changes I committed two minutes ago without having to wait for me to make a whole build. You don't have to, but I recommend you get it.
@ -45,27 +47,29 @@ There are now setup scripts that make this easy on Windows and Linux. You do not
- Do `Enable file system caching`/Do not `Enable symbolic links`
- Do not enable experimental stuff
Git should now be installed on your system. Any new terminal window (shift+right-click on any folder and hit 'Open in terminal') now has the `git` command!
Git should now be installed on your system. Any new terminal/command line/powershell window (shift+right-click on any folder and hit something like 'Open in terminal') now has the `git` command!
First of all, you will need to install Python. Get 3.10 or 3.11 [here](https://www.python.org/downloads/windows/). During the install process, make sure it has something like 'Add Python to PATH' checked. This makes Python available everywhere in Windows.
Then you will need to install Python. Get 3.10 or 3.11 [here](https://www.python.org/downloads/windows/) (or, if you are Win 7, I think you'll want [this](https://www.python.org/downloads/release/python-3810/)). During the install process, make sure it has something like 'Add Python to PATH' checked. This makes Python available everywhere in Windows.
=== "Linux"
You should already have a fairly new python. Ideally, you want at least 3.9.
You should already have a fairly new python. Ideally, you want at least 3.9. You can find out what version you have just by opening a new terminal and typing 'python'.
=== "macOS"
You should already have python of about the correct version.
You should already have a fairly new python. Ideally, you want at least 3.9. You can find out what version you have just by opening a new terminal and typing 'python'.
If you are already on a very new python, like 3.12+, that's ok--you might need to select the 'advanced' setup later on and choose the '(t)est' options. If you are stuck on a much older version of python, try the same thing, but with the '(o)lder' options (but I can't promise it will work!).
If you are already on newer python, like 3.12+, that's ok--you might need to select the 'advanced' setup later on and choose the '(t)est' options. If you are stuck on a much older version of python, try the same thing, but with the '(o)lder' options (but I can't promise it will work!).
Then, get the hydrus source. It is best to get it with Git: make a new folder somewhere, open a terminal in it, and then enter:
Then, get the hydrus source. It is best to get it with Git: make a new folder somewhere, open a terminal in it, and then paste:
git clone https://github.com/hydrusnetwork/hydrus
The whole repository will be copied to that location. If Git is not available, then just go to the [latest release](https://github.com/hydrusnetwork/hydrus/releases/latest) and download and extract the source code .zip somewhere.
The whole repository will be copied to that location--this is now your install dir. You can move it if you like.
If Git is not available, then just go to the [latest release](https://github.com/hydrusnetwork/hydrus/releases/latest) and download and extract the source code .zip somewhere.
!!! warning "Read-only install locations"
Make sure the install directory has convenient write permissions (e.g. on Windows, don't put it in "Program Files"). Extracting straight to a spare drive, something like "D:\Hydrus Network", is ideal.

View File

@ -1326,6 +1326,10 @@ class Controller( ClientControllerInterface.ClientControllerInterface, HydrusCon
self.CallBlockingToQt( self._splash, qt_code_style )
from hydrus.core.files.images import HydrusImageHandling
HydrusImageHandling.SetEnableLoadTruncatedImages( self.new_options.GetBoolean( 'enable_truncated_images_pil' ) )
def qt_code_pregui():
shortcut_sets = CG.client_controller.Read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUT_SET )
@ -1368,7 +1372,7 @@ class Controller( ClientControllerInterface.ClientControllerInterface, HydrusCon
if self.db.IsFirstStart():
message = 'Hi, this looks like the first time you have started the hydrus client.'
message = 'Hi, this looks like the first time you have started the hydrus client. If this is not the first time you have run this client install, please check the help documentation under "install_dir/db".'
message += '\n' * 2
message += 'Don\'t forget to check out the help if you haven\'t already, by clicking help->help--it has an extensive \'getting started\' section, including how to update and the importance of backing up your database.'
message += '\n' * 2

View File

@ -152,7 +152,9 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'booleans' ][ 'freeze_message_manager_when_mouse_on_other_monitor' ] = False
self._dictionary[ 'booleans' ][ 'freeze_message_manager_when_main_gui_minimised' ] = False
self._dictionary[ 'booleans' ][ 'load_images_with_pil' ] = False
self._dictionary[ 'booleans' ][ 'load_images_with_pil' ] = True
self._dictionary[ 'booleans' ][ 'only_show_delete_from_all_local_domains_when_filtering' ] = False
self._dictionary[ 'booleans' ][ 'use_system_ffmpeg' ] = False
@ -312,6 +314,8 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'booleans' ][ 'slideshow_always_play_duration_media_once_through' ] = False
self._dictionary[ 'booleans' ][ 'enable_truncated_images_pil' ] = True
from hydrus.client.gui.canvas import ClientGUIMPV
self._dictionary[ 'booleans' ][ 'mpv_available_at_start' ] = ClientGUIMPV.MPV_IS_AVAILABLE

View File

@ -1832,16 +1832,6 @@ class FrameGUI( CAC.ApplicationCommandProcessorMixin, ClientGUITopLevelWindows.M
def _EnableLoadTruncatedImages( self ):
result = HydrusImageHandling.EnableLoadTruncatedImages()
if not result:
ClientGUIDialogsMessage.ShowCritical( self, 'Error', 'Could not turn on--perhaps your version of PIL does not support it?' )
def _ExportDownloader( self ):
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'export downloaders' ) as dlg:
@ -3001,6 +2991,7 @@ class FrameGUI( CAC.ApplicationCommandProcessorMixin, ClientGUITopLevelWindows.M
ClientGUIMenus.AppendSeparator( submenu )
ClientGUIMenus.AppendMenuItem( submenu, 'manage services' + HC.UNICODE_ELLIPSIS, 'Add, edit, and delete this server\'s services.', self._ManageServer, service_key )
ClientGUIMenus.AppendMenuItem( submenu, 'restart server services', 'Command the server to disconnect and restart its services.', self._RestartServerServices, service_key )
ClientGUIMenus.AppendSeparator( submenu )
ClientGUIMenus.AppendMenuItem( submenu, 'backup server', 'Command the server to temporarily pause and back up its database.', self._BackupServer, service_key )
ClientGUIMenus.AppendSeparator( submenu )
@ -3479,7 +3470,6 @@ class FrameGUI( CAC.ApplicationCommandProcessorMixin, ClientGUITopLevelWindows.M
ClientGUIMenus.AppendMenuItem( data_actions, 'show scheduled jobs', 'Print some information about the currently scheduled jobs log.', self._DebugShowScheduledJobs )
ClientGUIMenus.AppendMenuItem( data_actions, 'subscription manager snapshot', 'Have the subscription system show what it is doing.', self._controller.subscriptions_manager.ShowSnapshot )
ClientGUIMenus.AppendMenuItem( data_actions, 'flush log', 'Command the log to write any buffered contents to hard drive.', HydrusData.DebugPrint, 'Flushing log' )
ClientGUIMenus.AppendMenuItem( data_actions, 'enable truncated image loading', 'Enable the truncated image loading to test out broken jpegs.', self._EnableLoadTruncatedImages )
ClientGUIMenus.AppendSeparator( data_actions )
ClientGUIMenus.AppendMenuItem( data_actions, 'simulate program exit signal', 'Kill the program via a QApplication exit.', QW.QApplication.instance().exit )
@ -4428,6 +4418,8 @@ class FrameGUI( CAC.ApplicationCommandProcessorMixin, ClientGUITopLevelWindows.M
self._controller.pub( 'notify_new_colourset' )
self._controller.pub( 'notify_new_favourite_tags' )
HydrusImageHandling.SetEnableLoadTruncatedImages( self._controller.new_options.GetBoolean( 'enable_truncated_images_pil' ) )
self._menu_item_help_darkmode.setChecked( CG.client_controller.new_options.GetString( 'current_colourset' ) == 'darkmode' )
self._UpdateSystemTrayIcon()
@ -5547,6 +5539,63 @@ class FrameGUI( CAC.ApplicationCommandProcessorMixin, ClientGUITopLevelWindows.M
def _RestartServerServices( self, service_key ):
def do_it( service ):
started = HydrusTime.GetNow()
service.Request( HC.POST, 'restart_services' )
HydrusData.ShowText( 'Server service restart started!' )
time_started = HydrusTime.GetNowMS()
working_now = False
while not working_now:
if HG.view_shutdown:
return
time.sleep( 5 )
try:
result_bytes = service.Request( HC.GET, 'busy' )
working_now = True
except:
pass
if HydrusTime.TimeHasPassedMS( time_started + ( 60 * 1000 ) ):
HydrusData.ShowText( 'It has been a minute and the server is not back up. Abandoning check--something is super delayed/not working!' )
return
HydrusData.ShowText( 'Server is back up!' )
message = 'This will tell the server to restart its services. If you have swapped in a new ssl cert, this will load that new one.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it', no_label = 'forget it' )
if result == QW.QDialog.Accepted:
service = self._controller.services_manager.GetService( service_key )
self._controller.CallToThread( do_it, service )
def _RestoreSplitterPositions( self ):
self._controller.pub( 'set_splitter_positions', HC.options[ 'hpos' ], HC.options[ 'vpos' ] )
@ -6925,9 +6974,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
HydrusData.ShowText( 'Server vacuum started!' )
time.sleep( 10 )
result_bytes = service.Request( HC.GET, 'busy' )
result_bytes = b'1'
while result_bytes == b'1':
@ -6936,7 +6983,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
return
time.sleep( 10 )
time.sleep( 5 )
result_bytes = service.Request( HC.GET, 'busy' )

View File

@ -7,6 +7,7 @@ from qtpy import QtWidgets as QW
from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientLocation
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIScrolledPanels
@ -114,6 +115,9 @@ class QuestionArchiveDeleteFinishFilteringPanel( ClientGUIScrolledPanels.Resizin
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
delay_delete_buttons = len( deletion_options ) > 0
delayed_delete_buttons = []
for ( location_context, delete_label ) in deletion_options:
label = '{}?'.format( delete_label )
@ -134,6 +138,25 @@ class QuestionArchiveDeleteFinishFilteringPanel( ClientGUIScrolledPanels.Resizin
first_commit = commit
if delay_delete_buttons:
delayed_delete_buttons.append( commit )
commit.setEnabled( False )
def do_it():
for b in delayed_delete_buttons:
b.setEnabled( True )
if len( delayed_delete_buttons ) > 0:
CG.client_controller.CallLaterQtSafe( self, 1.2, 'delayed button enable', do_it )
self._forget = ClientGUICommon.BetterButton( self, 'forget', self.parentWidget().done, QW.QDialog.Rejected )

View File

@ -35,6 +35,7 @@ from hydrus.client.gui import QtInit
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.canvas import ClientGUIMPV
from hydrus.client.gui.importing import ClientGUIImportOptions
from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
from hydrus.client.gui.widgets import ClientGUICommon
@ -3726,13 +3727,14 @@ class EditRegexFavourites( ClientGUIScrolledPanels.EditPanel ):
return self._regexes.GetData()
class EditSelectFromListPanel( ClientGUIScrolledPanels.EditPanel ):
def __init__( self, parent: QW.QWidget, choice_tuples: list, value_to_select = None, sort_tuples = True ):
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
self._list = QW.QListWidget( self )
self._list = ClientGUIListBoxes.BetterQListWidget( self )
self._list.itemDoubleClicked.connect( self.EventSelect )
#

View File

@ -38,6 +38,7 @@ from hydrus.client.gui import ClientGUITopLevelWindowsPanels
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.importing import ClientGUIImport
from hydrus.client.gui.importing import ClientGUIImportOptions
from hydrus.client.gui.lists import ClientGUIListBook
from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
@ -63,7 +64,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._new_options = CG.client_controller.new_options
self._original_new_options = self._new_options.Duplicate()
self._listbook = ClientGUICommon.ListBook( self )
self._listbook = ClientGUIListBook.ListBook( self )
self._listbook.AddPage( 'gui', 'gui', self._GUIPanel( self._listbook ) ) # leave this at the top, to make it default page
self._listbook.AddPage( 'gui pages', 'gui pages', self._GUIPagesPanel( self._listbook, self._new_options ) )
@ -1030,6 +1031,10 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._confirm_multiple_local_file_services_copy = QW.QCheckBox( self )
self._confirm_multiple_local_file_services_move = QW.QCheckBox( self )
self._only_show_delete_from_all_local_domains_when_filtering = QW.QCheckBox( self )
tt = 'When you finish filtering, if the files you chose to delete are in multiple local file domains, you are usually given the option of where you want to delete them from. If you always want to delete them from all locations and do not want the more complicated confirmation dialog, check this.'
self._only_show_delete_from_all_local_domains_when_filtering.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._remove_filtered_files = QW.QCheckBox( self )
self._remove_trashed_files = QW.QCheckBox( self )
self._remove_local_domain_moved_files = QW.QCheckBox( self )
@ -1081,6 +1086,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._confirm_multiple_local_file_services_copy.setChecked( self._new_options.GetBoolean( 'confirm_multiple_local_file_services_copy' ) )
self._confirm_multiple_local_file_services_move.setChecked( self._new_options.GetBoolean( 'confirm_multiple_local_file_services_move' ) )
self._only_show_delete_from_all_local_domains_when_filtering.setChecked( self._new_options.GetBoolean( 'only_show_delete_from_all_local_domains_when_filtering' ) )
self._remove_filtered_files.setChecked( HC.options[ 'remove_filtered_files' ] )
self._remove_trashed_files.setChecked( HC.options[ 'remove_trashed_files' ] )
self._remove_local_domain_moved_files.setChecked( self._new_options.GetBoolean( 'remove_local_domain_moved_files' ) )
@ -1117,6 +1124,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
rows.append( ( 'Confirm when moving files across local file services: ', self._confirm_multiple_local_file_services_move ) )
rows.append( ( 'When physically deleting files or folders, send them to the OS\'s recycle bin: ', self._delete_to_recycle_bin ) )
rows.append( ( 'When maintenance physically deletes files, wait this many ms between each delete: ', self._ms_to_wait_between_physical_file_deletes ) )
rows.append( ( 'When finishing filtering, always delete from all possible domains: ', self._only_show_delete_from_all_local_domains_when_filtering ) )
rows.append( ( 'Remove files from view when they are filtered: ', self._remove_filtered_files ) )
rows.append( ( 'Remove files from view when they are sent to the trash: ', self._remove_trashed_files ) )
rows.append( ( 'Remove files from view when they are moved to another local file domain: ', self._remove_local_domain_moved_files ) )
@ -1208,6 +1216,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
HC.options[ 'trash_max_age' ] = self._trash_max_age.GetValue()
HC.options[ 'trash_max_size' ] = self._trash_max_size.GetValue()
self._new_options.SetBoolean( 'only_show_delete_from_all_local_domains_when_filtering', self._only_show_delete_from_all_local_domains_when_filtering.isChecked() )
self._new_options.SetInteger( 'ms_to_wait_between_physical_file_deletes', self._ms_to_wait_between_physical_file_deletes.value() )
self._new_options.SetBoolean( 'confirm_multiple_local_file_services_copy', self._confirm_multiple_local_file_services_copy.isChecked() )
@ -2402,7 +2412,10 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._use_system_ffmpeg.setToolTip( ClientGUIFunctions.WrapToolTip( 'Check this to always default to the system ffmpeg in your path, rather than using the static ffmpeg in hydrus\'s bin directory. (requires restart)' ) )
self._load_images_with_pil = QW.QCheckBox( system_panel )
self._load_images_with_pil.setToolTip( ClientGUIFunctions.WrapToolTip( 'We are dropping CV and moving to PIL exclusively. If you want to help test, please turn this on and send hydev any images that render wrong!' ) )
self._load_images_with_pil.setToolTip( ClientGUIFunctions.WrapToolTip( 'We are expecting to drop CV and move to PIL exclusively. This used to be a test option but is now default true and may soon be retired.' ) )
self._enable_truncated_images_pil = QW.QCheckBox( system_panel )
self._enable_truncated_images_pil.setToolTip( ClientGUIFunctions.WrapToolTip( 'Should PIL be allowed to load broken images that are missing some data? This is usually fine, but some years ago we had stability problems when this was mixed with OpenCV. Now it is default on, but if you need to, you can disable it here.' ) )
#
@ -2487,6 +2500,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._hide_uninteresting_local_import_time.setChecked( self._new_options.GetBoolean( 'hide_uninteresting_local_import_time' ) )
self._hide_uninteresting_modified_time.setChecked( self._new_options.GetBoolean( 'hide_uninteresting_modified_time' ) )
self._load_images_with_pil.setChecked( self._new_options.GetBoolean( 'load_images_with_pil' ) )
self._enable_truncated_images_pil.setChecked( self._new_options.GetBoolean( 'enable_truncated_images_pil' ) )
self._use_system_ffmpeg.setChecked( self._new_options.GetBoolean( 'use_system_ffmpeg' ) )
self._always_loop_animations.setChecked( self._new_options.GetBoolean( 'always_loop_gifs' ) )
self._draw_transparency_checkerboard_media_canvas.setChecked( self._new_options.GetBoolean( 'draw_transparency_checkerboard_media_canvas' ) )
@ -2588,7 +2602,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
rows.append( ( 'Set a new mpv.conf on dialog ok?:', self._mpv_conf_path ) )
rows.append( ( 'Prefer system FFMPEG:', self._use_system_ffmpeg ) )
rows.append( ( 'IN TESTING: Load images with PIL:', self._load_images_with_pil ) )
rows.append( ( 'Allow loading of truncated images:', self._enable_truncated_images_pil ) )
rows.append( ( 'Load images with PIL:', self._load_images_with_pil ) )
gridbox = ClientGUICommon.WrapInGrid( system_panel, rows )
@ -2827,6 +2842,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._new_options.SetBoolean( 'hide_uninteresting_local_import_time', self._hide_uninteresting_local_import_time.isChecked() )
self._new_options.SetBoolean( 'hide_uninteresting_modified_time', self._hide_uninteresting_modified_time.isChecked() )
self._new_options.SetBoolean( 'load_images_with_pil', self._load_images_with_pil.isChecked() )
self._new_options.SetBoolean( 'enable_truncated_images_pil', self._enable_truncated_images_pil.isChecked() )
self._new_options.SetBoolean( 'use_system_ffmpeg', self._use_system_ffmpeg.isChecked() )
self._new_options.SetBoolean( 'always_loop_gifs', self._always_loop_animations.isChecked() )
self._new_options.SetBoolean( 'draw_transparency_checkerboard_media_canvas', self._draw_transparency_checkerboard_media_canvas.isChecked() )
@ -5142,10 +5158,11 @@ class ManageURLsPanel( CAC.ApplicationCommandProcessorMixin, ClientGUIScrolledPa
self._multiple_files_warning.hide()
self._urls_listbox = QW.QListWidget( self )
self._urls_listbox = ClientGUIListBoxes.BetterQListWidget( self )
self._urls_listbox.setSortingEnabled( True )
self._urls_listbox.setSelectionMode( QW.QAbstractItemView.ExtendedSelection )
self._urls_listbox.itemDoubleClicked.connect( self.EventListDoubleClick )
self._listbox_event_filter = QP.WidgetEventFilter( self._urls_listbox )
self._listbox_event_filter.EVT_KEY_DOWN( self.EventListKeyDown )
@ -5363,9 +5380,11 @@ class ManageURLsPanel( CAC.ApplicationCommandProcessorMixin, ClientGUIScrolledPa
label = '{} ({})'.format( url, count )
item = QW.QListWidgetItem()
item.setText( label )
item.setData( QC.Qt.UserRole, url )
self._urls_listbox.addItem( item )

View File

@ -8,8 +8,6 @@ from qtpy import QtWidgets as QW
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusTime
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
@ -38,11 +36,11 @@ class MultilineStringConversionTestPanel( QW.QWidget ):
self._string_processor = string_processor
self._test_data = QW.QListWidget( self )
self._test_data = ClientGUIListBoxes.BetterQListWidget( self )
self._test_data.setSelectionMode( QW.QListWidget.SingleSelection )
self._result_data = QW.QListWidget( self )
self._result_data = ClientGUIListBoxes.BetterQListWidget( self )
self._result_data.setSelectionMode( QW.QListView.NoSelection )
@ -215,7 +213,7 @@ class SingleStringConversionTestPanel( QW.QWidget ):
stop_now = True
results_list = QW.QListWidget( self._example_results )
results_list = ClientGUIListBoxes.BetterQListWidget( self._example_results )
results_list.setSelectionMode( QW.QListWidget.NoSelection )
if len( results ) == 0:
@ -1193,10 +1191,10 @@ class EditStringJoinerPanel( ClientGUIScrolledPanels.EditPanel ):
self._example_panel = ClientGUICommon.StaticBox( self, 'test results' )
self._example_strings = QW.QListWidget( self._example_panel )
self._example_strings = ClientGUIListBoxes.BetterQListWidget( self._example_panel )
self._example_strings.setSelectionMode( QW.QListWidget.NoSelection )
self._example_strings_joined = QW.QListWidget( self._example_panel )
self._example_strings_joined = ClientGUIListBoxes.BetterQListWidget( self._example_panel )
self._example_strings_joined.setSelectionMode( QW.QListWidget.NoSelection )
#
@ -1571,10 +1569,10 @@ class EditStringSlicerPanel( ClientGUIScrolledPanels.EditPanel ):
self._example_panel = ClientGUICommon.StaticBox( self, 'test results' )
self._example_strings = QW.QListWidget( self._example_panel )
self._example_strings = ClientGUIListBoxes.BetterQListWidget( self._example_panel )
self._example_strings.setSelectionMode( QW.QListWidget.NoSelection )
self._example_strings_sliced = QW.QListWidget( self._example_panel )
self._example_strings_sliced = ClientGUIListBoxes.BetterQListWidget( self._example_panel )
self._example_strings_sliced.setSelectionMode( QW.QListWidget.NoSelection )
#
@ -1767,10 +1765,10 @@ class EditStringSorterPanel( ClientGUIScrolledPanels.EditPanel ):
self._example_panel = ClientGUICommon.StaticBox( self, 'test results' )
self._example_strings = QW.QListWidget( self._example_panel )
self._example_strings = ClientGUIListBoxes.BetterQListWidget( self._example_panel )
self._example_strings.setSelectionMode( QW.QListWidget.NoSelection )
self._example_strings_sorted = QW.QListWidget( self._example_panel )
self._example_strings_sorted = ClientGUIListBoxes.BetterQListWidget( self._example_panel )
self._example_strings_sorted.setSelectionMode( QW.QListWidget.NoSelection )
#
@ -1915,7 +1913,7 @@ class EditStringSplitterPanel( ClientGUIScrolledPanels.EditPanel ):
self._example_string = QW.QLineEdit( self._example_panel )
self._example_string_splits = QW.QListWidget( self._example_panel )
self._example_string_splits = ClientGUIListBoxes.BetterQListWidget( self._example_panel )
self._example_string_splits.setSelectionMode( QW.QListWidget.NoSelection )
#

View File

@ -3839,6 +3839,13 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
location_contexts_to_present_options_for = HydrusData.DedupeList( location_contexts_to_present_options_for )
only_allow_all_media_files = len( location_contexts_to_present_options_for ) > 1 and CG.client_controller.new_options.GetBoolean( 'only_show_delete_from_all_local_domains_when_filtering' ) and True in ( location_context.IsAllMediaFiles() for location_context in location_contexts_to_present_options_for )
if only_allow_all_media_files:
location_contexts_to_present_options_for = [ ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY ) ]
for location_context in location_contexts_to_present_options_for:
file_service_keys = location_context.current_service_keys

View File

@ -205,7 +205,7 @@ class FilenameTaggingOptionsPanel( QW.QWidget ):
self._regexes_panel = ClientGUICommon.StaticBox( self, 'regexes' )
self._regexes = QW.QListWidget( self._regexes_panel )
self._regexes = ClientGUIListBoxes.BetterQListWidget( self._regexes_panel )
self._regexes.itemDoubleClicked.connect( self.EventRemoveRegex )
self._regex_box = QW.QLineEdit()

View File

@ -0,0 +1,378 @@
import collections.abc
import os
import re
import typing
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientPaths
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.networking import ClientNetworkingFunctions
class ListBook( QW.QWidget ):
def __init__( self, *args, **kwargs ):
QW.QWidget.__init__( self, *args, **kwargs )
self._keys_to_active_pages = {}
self._keys_to_proto_pages = {}
self._list_box = ClientGUIListBoxes.BetterQListWidget( self )
self._list_box.setSelectionMode( QW.QListWidget.SingleSelection )
self._empty_panel = QW.QWidget( self )
self._current_key = None
self._current_panel = self._empty_panel
self._panel_sizer = QP.VBoxLayout()
QP.AddToLayout( self._panel_sizer, self._empty_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
hbox = QP.HBoxLayout( margin = 0 )
QP.AddToLayout( hbox, self._list_box, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( hbox, self._panel_sizer, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._list_box.itemSelectionChanged.connect( self.EventSelection )
self.setLayout( hbox )
def _ActivatePage( self, key ):
( classname, args, kwargs ) = self._keys_to_proto_pages[ key ]
page = classname( *args, **kwargs )
page.setVisible( False )
QP.AddToLayout( self._panel_sizer, page, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._keys_to_active_pages[ key ] = page
del self._keys_to_proto_pages[ key ]
def _GetIndex( self, key ):
for i in range( self._list_box.count() ):
i_key = self._list_box.item( i ).data( QC.Qt.UserRole )
if i_key == key:
return i
return -1
def _Select( self, selection ):
if selection == -1:
self._current_key = None
else:
self._current_key = self._list_box.item( selection ).data( QC.Qt.UserRole )
self._current_panel.setVisible( False )
self._list_box.blockSignals( True )
QP.ListWidgetSetSelection( self._list_box, selection )
self._list_box.blockSignals( False )
if selection == -1:
self._current_panel = self._empty_panel
else:
if self._current_key in self._keys_to_proto_pages:
self._ActivatePage( self._current_key )
self._current_panel = self._keys_to_active_pages[ self._current_key ]
self._current_panel.show()
self.update()
def AddPage( self, display_name, key, page, select = False ):
if self._GetIndex( key ) != -1:
raise HydrusExceptions.NameException( 'That entry already exists!' )
if not isinstance( page, tuple ):
page.setVisible( False )
QP.AddToLayout( self._panel_sizer, page, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
# Could call QListWidget.sortItems() here instead of doing it manually
current_display_names = QP.ListWidgetGetStrings( self._list_box )
insertion_index = len( current_display_names )
for ( i, current_display_name ) in enumerate( current_display_names ):
if current_display_name > display_name:
insertion_index = i
break
item = QW.QListWidgetItem()
item.setText( display_name )
item.setData( QC.Qt.UserRole, key )
self._list_box.insertItem( insertion_index, item )
self._keys_to_active_pages[ key ] = page
if self._list_box.count() == 1:
self._Select( 0 )
elif select:
index = self._GetIndex( key )
self._Select( index )
def AddPageArgs( self, display_name, key, classname, args, kwargs ):
if self._GetIndex( key ) != -1:
raise HydrusExceptions.NameException( 'That entry already exists!' )
# Could call QListWidget.sortItems() here instead of doing it manually
current_display_names = QP.ListWidgetGetStrings( self._list_box )
insertion_index = len( current_display_names )
for ( i, current_display_name ) in enumerate( current_display_names ):
if current_display_name > display_name:
insertion_index = i
break
item = QW.QListWidgetItem()
item.setText( display_name )
item.setData( QC.Qt.UserRole, key )
self._list_box.insertItem( insertion_index, item )
self._keys_to_proto_pages[ key ] = ( classname, args, kwargs )
if self._list_box.count() == 1:
self._Select( 0 )
def DeleteAllPages( self ):
self._panel_sizer.removeWidget( self._empty_panel )
QP.ClearLayout( self._panel_sizer, delete_widgets=True )
QP.AddToLayout( self._panel_sizer, self._empty_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._current_key = None
self._current_panel = self._empty_panel
self._keys_to_active_pages = {}
self._keys_to_proto_pages = {}
self._list_box.clear()
def DeleteCurrentPage( self ):
selection = QP.ListWidgetGetSelection( self._list_box )
if selection != -1:
key_to_delete = self._current_key
page_to_delete = self._current_panel
next_selection = selection + 1
previous_selection = selection - 1
if next_selection < self._list_box.count():
self._Select( next_selection )
elif previous_selection >= 0:
self._Select( previous_selection )
else:
self._Select( -1 )
self._panel_sizer.removeWidget( page_to_delete )
page_to_delete.deleteLater()
del self._keys_to_active_pages[ key_to_delete ]
QP.ListWidgetDelete( self._list_box, selection )
def EventSelection( self ):
selection = QP.ListWidgetGetSelection( self._list_box )
if selection != self._GetIndex( self._current_key ):
self._Select( selection )
def GetCurrentKey( self ):
return self._current_key
def GetCurrentPage( self ):
if self._current_panel == self._empty_panel:
return None
else:
return self._current_panel
def GetActivePages( self ):
return list(self._keys_to_active_pages.values())
def GetPage( self, key ):
if key in self._keys_to_proto_pages:
self._ActivatePage( key )
if key in self._keys_to_active_pages:
return self._keys_to_active_pages[ key ]
raise Exception( 'That page not found!' )
def GetPageCount( self ):
return len( self._keys_to_active_pages ) + len( self._keys_to_proto_pages )
def KeyExists( self, key ):
return key in self._keys_to_active_pages or key in self._keys_to_proto_pages
def Select( self, key ):
index = self._GetIndex( key )
if index != -1 and index != QP.ListWidgetGetSelection( self._list_box ) :
self._Select( index )
def SelectDown( self ):
current_selection = QP.ListWidgetGetSelection( self._list_box )
if current_selection != -1:
num_entries = self._list_box.count()
if current_selection == num_entries - 1: selection = 0
else: selection = current_selection + 1
if selection != current_selection:
self._Select( selection )
def SelectPage( self, page_to_select ):
for ( key, page ) in list(self._keys_to_active_pages.items()):
if page == page_to_select:
self._Select( self._GetIndex( key ) )
return
def SelectUp( self ):
current_selection = QP.ListWidgetGetSelection( self._list_box )
if current_selection != -1:
num_entries = self._list_box.count()
if current_selection == 0: selection = num_entries - 1
else: selection = current_selection - 1
if selection != current_selection:
self._Select( selection )

View File

@ -176,6 +176,54 @@ class BetterQListWidget( QW.QListWidget ):
return len( indices )
def keyPressEvent( self, event: QG.QKeyEvent ):
if event.modifiers() & QC.Qt.ControlModifier and event.key() in ( QC.Qt.Key_C, QC.Qt.Key_Insert ):
event.accept()
try:
texts_to_copy = []
for list_widget_item in self.selectedItems():
user_role_data = list_widget_item.data( QC.Qt.UserRole )
if isinstance( user_role_data, str ):
text = user_role_data
else:
text = list_widget_item.text()
texts_to_copy.append( text )
if len( texts_to_copy ) == 0:
return
copyable_text = '\n'.join( texts_to_copy )
CG.client_controller.pub( 'clipboard', 'text', copyable_text )
except Exception as e:
HydrusData.ShowText( 'Could not copy some text from a list!' )
HydrusData.ShowException( e )
else:
QW.QListWidget.keyPressEvent( self, event )
def MoveSelected( self, distance: int ):
if distance == 0:

View File

@ -19,6 +19,7 @@ from hydrus.client.gui import ClientGUIStringControls
from hydrus.client.gui import ClientGUIStringPanels
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.gui.parsing import ClientGUIParsingTest
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.gui.widgets import ClientGUIMenuButton
@ -68,7 +69,7 @@ class EditCompoundFormulaPanel( EditSpecificFormulaPanel ):
edit_panel = ClientGUICommon.StaticBox( self, 'edit' )
self._formulae = QW.QListWidget( edit_panel )
self._formulae = ClientGUIListBoxes.BetterQListWidget( edit_panel )
self._formulae.setSelectionMode( QW.QAbstractItemView.SingleSelection )
self._formulae.itemDoubleClicked.connect( self.Edit )
@ -787,7 +788,7 @@ class EditHTMLFormulaPanel( EditSpecificFormulaPanel ):
edit_panel = ClientGUICommon.StaticBox( self, 'edit' )
self._tag_rules = QW.QListWidget( edit_panel )
self._tag_rules = ClientGUIListBoxes.BetterQListWidget( edit_panel )
self._tag_rules.setSelectionMode( QW.QAbstractItemView.SingleSelection )
self._tag_rules.itemDoubleClicked.connect( self.Edit )
@ -1156,7 +1157,7 @@ class EditJSONFormulaPanel( EditSpecificFormulaPanel ):
edit_panel = ClientGUICommon.StaticBox( self, 'edit' )
self._parse_rules = QW.QListWidget( edit_panel )
self._parse_rules = ClientGUIListBoxes.BetterQListWidget( edit_panel )
self._parse_rules.setSelectionMode( QW.QAbstractItemView.SingleSelection )
self._parse_rules.itemDoubleClicked.connect( self.Edit )

View File

@ -9,7 +9,6 @@ from qtpy import QtGui as QG
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
@ -426,6 +425,7 @@ class BetterCheckBoxList( QW.QListWidget ):
class BetterChoice( QW.QComboBox ):
def __init__( self, *args, **kwargs ):
@ -1074,359 +1074,7 @@ class Gauge( QW.QProgressBar ):
self._is_pulsing = True
class ListBook( QW.QWidget ):
def __init__( self, *args, **kwargs ):
QW.QWidget.__init__( self, *args, **kwargs )
self._keys_to_active_pages = {}
self._keys_to_proto_pages = {}
self._list_box = QW.QListWidget( self )
self._list_box.setSelectionMode( QW.QListWidget.SingleSelection )
self._empty_panel = QW.QWidget( self )
self._current_key = None
self._current_panel = self._empty_panel
self._panel_sizer = QP.VBoxLayout()
QP.AddToLayout( self._panel_sizer, self._empty_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
hbox = QP.HBoxLayout( margin = 0 )
QP.AddToLayout( hbox, self._list_box, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( hbox, self._panel_sizer, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._list_box.itemSelectionChanged.connect( self.EventSelection )
self.setLayout( hbox )
def _ActivatePage( self, key ):
( classname, args, kwargs ) = self._keys_to_proto_pages[ key ]
page = classname( *args, **kwargs )
page.setVisible( False )
QP.AddToLayout( self._panel_sizer, page, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._keys_to_active_pages[ key ] = page
del self._keys_to_proto_pages[ key ]
def _GetIndex( self, key ):
for i in range( self._list_box.count() ):
i_key = self._list_box.item( i ).data( QC.Qt.UserRole )
if i_key == key:
return i
return -1
def _Select( self, selection ):
if selection == -1:
self._current_key = None
else:
self._current_key = self._list_box.item( selection ).data( QC.Qt.UserRole )
self._current_panel.setVisible( False )
self._list_box.blockSignals( True )
QP.ListWidgetSetSelection( self._list_box, selection )
self._list_box.blockSignals( False )
if selection == -1:
self._current_panel = self._empty_panel
else:
if self._current_key in self._keys_to_proto_pages:
self._ActivatePage( self._current_key )
self._current_panel = self._keys_to_active_pages[ self._current_key ]
self._current_panel.show()
self.update()
def AddPage( self, display_name, key, page, select = False ):
if self._GetIndex( key ) != -1:
raise HydrusExceptions.NameException( 'That entry already exists!' )
if not isinstance( page, tuple ):
page.setVisible( False )
QP.AddToLayout( self._panel_sizer, page, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
# Could call QListWidget.sortItems() here instead of doing it manually
current_display_names = QP.ListWidgetGetStrings( self._list_box )
insertion_index = len( current_display_names )
for ( i, current_display_name ) in enumerate( current_display_names ):
if current_display_name > display_name:
insertion_index = i
break
item = QW.QListWidgetItem()
item.setText( display_name )
item.setData( QC.Qt.UserRole, key )
self._list_box.insertItem( insertion_index, item )
self._keys_to_active_pages[ key ] = page
if self._list_box.count() == 1:
self._Select( 0 )
elif select:
index = self._GetIndex( key )
self._Select( index )
def AddPageArgs( self, display_name, key, classname, args, kwargs ):
if self._GetIndex( key ) != -1:
raise HydrusExceptions.NameException( 'That entry already exists!' )
# Could call QListWidget.sortItems() here instead of doing it manually
current_display_names = QP.ListWidgetGetStrings( self._list_box )
insertion_index = len( current_display_names )
for ( i, current_display_name ) in enumerate( current_display_names ):
if current_display_name > display_name:
insertion_index = i
break
item = QW.QListWidgetItem()
item.setText( display_name )
item.setData( QC.Qt.UserRole, key )
self._list_box.insertItem( insertion_index, item )
self._keys_to_proto_pages[ key ] = ( classname, args, kwargs )
if self._list_box.count() == 1:
self._Select( 0 )
def DeleteAllPages( self ):
self._panel_sizer.removeWidget( self._empty_panel )
QP.ClearLayout( self._panel_sizer, delete_widgets=True )
QP.AddToLayout( self._panel_sizer, self._empty_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._current_key = None
self._current_panel = self._empty_panel
self._keys_to_active_pages = {}
self._keys_to_proto_pages = {}
self._list_box.clear()
def DeleteCurrentPage( self ):
selection = QP.ListWidgetGetSelection( self._list_box )
if selection != -1:
key_to_delete = self._current_key
page_to_delete = self._current_panel
next_selection = selection + 1
previous_selection = selection - 1
if next_selection < self._list_box.count():
self._Select( next_selection )
elif previous_selection >= 0:
self._Select( previous_selection )
else:
self._Select( -1 )
self._panel_sizer.removeWidget( page_to_delete )
page_to_delete.deleteLater()
del self._keys_to_active_pages[ key_to_delete ]
QP.ListWidgetDelete( self._list_box, selection )
def EventSelection( self ):
selection = QP.ListWidgetGetSelection( self._list_box )
if selection != self._GetIndex( self._current_key ):
self._Select( selection )
def GetCurrentKey( self ):
return self._current_key
def GetCurrentPage( self ):
if self._current_panel == self._empty_panel:
return None
else:
return self._current_panel
def GetActivePages( self ):
return list(self._keys_to_active_pages.values())
def GetPage( self, key ):
if key in self._keys_to_proto_pages:
self._ActivatePage( key )
if key in self._keys_to_active_pages:
return self._keys_to_active_pages[ key ]
raise Exception( 'That page not found!' )
def GetPageCount( self ):
return len( self._keys_to_active_pages ) + len( self._keys_to_proto_pages )
def KeyExists( self, key ):
return key in self._keys_to_active_pages or key in self._keys_to_proto_pages
def Select( self, key ):
index = self._GetIndex( key )
if index != -1 and index != QP.ListWidgetGetSelection( self._list_box ) :
self._Select( index )
def SelectDown( self ):
current_selection = QP.ListWidgetGetSelection( self._list_box )
if current_selection != -1:
num_entries = self._list_box.count()
if current_selection == num_entries - 1: selection = 0
else: selection = current_selection + 1
if selection != current_selection:
self._Select( selection )
def SelectPage( self, page_to_select ):
for ( key, page ) in list(self._keys_to_active_pages.items()):
if page == page_to_select:
self._Select( self._GetIndex( key ) )
return
def SelectUp( self ):
current_selection = QP.ListWidgetGetSelection( self._list_box )
if current_selection != -1:
num_entries = self._list_box.count()
if current_selection == 0: selection = num_entries - 1
else: selection = current_selection - 1
if selection != current_selection:
self._Select( selection )
class NoneableSpinCtrl( QW.QWidget ):
valueChanged = QC.Signal()
@ -1741,6 +1389,7 @@ class OnOffButton( QW.QPushButton ):
self._SetValue( value )
class RegexButton( BetterButton ):
def __init__( self, parent ):

View File

@ -2443,6 +2443,8 @@ class HydrusResourceClientAPIRestrictedAddURLsAssociateURL( HydrusResourceClient
def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ):
normalise_urls = request.parsed_request_args.GetValue( 'normalise_urls', bool, default_value = True )
urls_to_add = []
if 'url_to_add' in request.parsed_request_args:
@ -2480,13 +2482,16 @@ class HydrusResourceClientAPIRestrictedAddURLsAssociateURL( HydrusResourceClient
domain_manager = CG.client_controller.network_engine.domain_manager
try:
if normalise_urls:
urls_to_add = [ domain_manager.NormaliseURL( url ) for url in urls_to_add ]
except HydrusExceptions.URLClassException as e:
raise HydrusExceptions.BadRequestException( e )
try:
urls_to_add = [ domain_manager.NormaliseURL( url ) for url in urls_to_add ]
except HydrusExceptions.URLClassException as e:
raise HydrusExceptions.BadRequestException( e )
if len( urls_to_add ) == 0 and len( urls_to_delete ) == 0:

View File

@ -44,7 +44,14 @@ def ConvertURLClassesIntoAPIPairs( url_classes ):
example_url = url_class.GetExampleURL()
api_url = url_class.GetAPIURL( example_url )
try:
api_url = url_class.GetAPIURL( example_url )
except:
continue
for other_url_class in url_classes:

View File

@ -105,8 +105,8 @@ options = {}
# Misc
NETWORK_VERSION = 20
SOFTWARE_VERSION = 570
CLIENT_API_VERSION = 63
SOFTWARE_VERSION = 571
CLIENT_API_VERSION = 64
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -37,18 +37,29 @@ from hydrus.core.files.images import HydrusImageMetadata
from hydrus.core.files.images import HydrusImageNormalisation
from hydrus.core.files.images import HydrusImageOpening
def EnableLoadTruncatedImages():
def SetEnableLoadTruncatedImages( value: bool ):
if hasattr( PILImageFile, 'LOAD_TRUNCATED_IMAGES' ):
# this can now cause load hangs due to the trunc load code adding infinite fake EOFs to the file stream, wew lad
# hence debug only
PILImageFile.LOAD_TRUNCATED_IMAGES = True
PILImageFile.LOAD_TRUNCATED_IMAGES = value
return True
else:
try:
import PIL
HydrusData.Print( f'Could not set the PIL image trunctation value to {value}--perhaps this version of PIL ({PIL.__version__})does not support it?' )
except:
HydrusData.Print( f'Could not set the PIL image trunctation value to {value}, and could not determine the PIL version! Something is busted!' )
return False

View File

@ -301,7 +301,7 @@ class Controller( HydrusController.HydrusController ):
else:
self.SetRunningTwistedServices( self._services )
self.RestartServices()
#
@ -358,6 +358,11 @@ class Controller( HydrusController.HydrusController ):
self._admin_service.ServerReportRequestUsed()
def RestartServices( self ):
self.SetRunningTwistedServices( self._services )
def Run( self ):
self.RecordRunningStart()
@ -570,7 +575,7 @@ class Controller( HydrusController.HydrusController ):
[ self._admin_service ] = [ service for service in self._services if service.GetServiceType() == HC.SERVER_ADMIN ]
self.SetRunningTwistedServices( self._services )
self.RestartServices()
def ShutdownView( self ):

View File

@ -55,6 +55,7 @@ class HydrusServiceAdmin( HydrusServiceRestricted ):
root.putChild( b'backup', ServerServerResources.HydrusResourceRestrictedBackup( self._service, HydrusServer.REMOTE_DOMAIN ) )
root.putChild( b'lock_on', ServerServerResources.HydrusResourceRestrictedLockOn( self._service, HydrusServer.REMOTE_DOMAIN ) )
root.putChild( b'lock_off', ServerServerResources.HydrusResourceRestrictedLockOff( self._service, HydrusServer.REMOTE_DOMAIN ) )
root.putChild( b'restart_services', ServerServerResources.HydrusResourceRestrictedRestartServices( self._service, HydrusServer.REMOTE_DOMAIN ) )
root.putChild( b'services', ServerServerResources.HydrusResourceRestrictedServices( self._service, HydrusServer.REMOTE_DOMAIN ) )
root.putChild( b'shutdown', ServerServerResources.HydrusResourceShutdown( self._service, HydrusServer.LOCAL_DOMAIN ) )
root.putChild( b'vacuum', ServerServerResources.HydrusResourceRestrictedVacuum( self._service, HydrusServer.REMOTE_DOMAIN ) )
@ -62,6 +63,7 @@ class HydrusServiceAdmin( HydrusServiceRestricted ):
return root
class HydrusServiceRepository( HydrusServiceRestricted ):
def _InitRoot( self ):

View File

@ -1312,6 +1312,7 @@ class HydrusResourceRestrictedUpdate( HydrusResourceRestricted ):
return response_context
class HydrusResourceRestrictedImmediateUpdate( HydrusResourceRestricted ):
def _checkAccountPermissions( self, request: HydrusServerRequest.HydrusRequest ):
@ -1332,6 +1333,7 @@ class HydrusResourceRestrictedImmediateUpdate( HydrusResourceRestricted ):
return response_context
class HydrusResourceRestrictedMetadataUpdate( HydrusResourceRestricted ):
def _checkAccountPermissions( self, request: HydrusServerRequest.HydrusRequest ):
@ -1352,6 +1354,24 @@ class HydrusResourceRestrictedMetadataUpdate( HydrusResourceRestricted ):
return response_context
class HydrusResourceRestrictedRestartServices( HydrusResourceRestricted ):
def _checkAccountPermissions( self, request: HydrusServerRequest.HydrusRequest ):
request.hydrus_account.CheckPermission( HC.CONTENT_TYPE_SERVICES, HC.PERMISSION_ACTION_MODERATE )
def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ):
HG.server_controller.CallLater( 1.0, HG.server_controller.RestartServices )
response_context = HydrusServerResources.ResponseContext( 200 )
return response_context
class HydrusResourceRestrictedVacuum( HydrusResourceRestricted ):
def _checkAccountPermissions( self, request: HydrusServerRequest.HydrusRequest ):

View File

@ -3266,6 +3266,15 @@ class TestClientAPI( unittest.TestCase ):
self.assertEqual( HG.test_controller.GetWrite( 'import_url_test' ), [ ( ( url, set( filterable_tags ), additional_service_keys_to_tags, 'muh /tv/', None, True ), {} ) ] )
def _test_associate_urls( self, connection, set_up_permissions ):
api_permissions = set_up_permissions[ 'everything' ]
access_key_hex = api_permissions.GetAccessKey().hex()
headers = { 'Hydrus-Client-API-Access-Key' : access_key_hex, 'Content-Type' : HC.mime_mimetype_string_lookup[ HC.APPLICATION_JSON ] }
# associate url
HG.test_controller.ClearWrites( 'content_updates' )
@ -3368,6 +3377,101 @@ class TestClientAPI( unittest.TestCase ):
HF.compare_content_update_packages( self, content_update_package, expected_content_update_package )
# normalisation - True
HG.test_controller.ClearWrites( 'content_updates' )
hash = bytes.fromhex( '3b820114f658d768550e4e3d4f1dced3ff8db77443472b5ad93700647ad2d3ba' )
unnormalised_url = 'https://rule34.xxx/index.php?page=post&id=2588418&s=view'
normalised_url = 'https://rule34.xxx/index.php?id=2588418&page=post&s=view'
request_dict = { 'urls_to_add' : [ unnormalised_url ], 'hashes' : [ hash.hex() ] }
request_body = json.dumps( request_dict )
connection.request( 'POST', '/add_urls/associate_url', body = request_body, headers = headers )
response = connection.getresponse()
data = response.read()
self.assertEqual( response.status, 200 )
expected_content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdates( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, [ ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_URLS, HC.CONTENT_UPDATE_ADD, ( [ normalised_url ], { hash } ) ) ] )
[ ( ( content_update_package, ), kwargs ) ] = HG.test_controller.GetWrite( 'content_updates' )
HF.compare_content_update_packages( self, content_update_package, expected_content_update_package )
# normalisation - False
HG.test_controller.ClearWrites( 'content_updates' )
hash = bytes.fromhex( '3b820114f658d768550e4e3d4f1dced3ff8db77443472b5ad93700647ad2d3ba' )
unnormalised_url = 'https://rule34.xxx/index.php?page=post&id=2588418&s=view'
request_dict = { 'urls_to_add' : [ unnormalised_url ], 'hashes' : [ hash.hex() ], 'normalise_urls' : False }
request_body = json.dumps( request_dict )
connection.request( 'POST', '/add_urls/associate_url', body = request_body, headers = headers )
response = connection.getresponse()
data = response.read()
self.assertEqual( response.status, 200 )
expected_content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdates( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, [ ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_URLS, HC.CONTENT_UPDATE_ADD, ( [ unnormalised_url ], { hash } ) ) ] )
[ ( ( content_update_package, ), kwargs ) ] = HG.test_controller.GetWrite( 'content_updates' )
HF.compare_content_update_packages( self, content_update_package, expected_content_update_package )
# normalisation - crazy url error
HG.test_controller.ClearWrites( 'content_updates' )
hash = bytes.fromhex( '3b820114f658d768550e4e3d4f1dced3ff8db77443472b5ad93700647ad2d3ba' )
crazy_nonsense = 'hello'
request_dict = { 'urls_to_add' : [ crazy_nonsense ], 'hashes' : [ hash.hex() ] }
request_body = json.dumps( request_dict )
connection.request( 'POST', '/add_urls/associate_url', body = request_body, headers = headers )
response = connection.getresponse()
data = response.read()
self.assertEqual( response.status, 400 )
# normalisation - crazy url ok
HG.test_controller.ClearWrites( 'content_updates' )
hash = bytes.fromhex( '3b820114f658d768550e4e3d4f1dced3ff8db77443472b5ad93700647ad2d3ba' )
crazy_nonsense = 'hello'
request_dict = { 'urls_to_add' : [ crazy_nonsense ], 'hashes' : [ hash.hex() ], 'normalise_urls' : False }
request_body = json.dumps( request_dict )
connection.request( 'POST', '/add_urls/associate_url', body = request_body, headers = headers )
response = connection.getresponse()
data = response.read()
self.assertEqual( response.status, 200 )
expected_content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdates( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, [ ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_URLS, HC.CONTENT_UPDATE_ADD, ( [ crazy_nonsense ], { hash } ) ) ] )
[ ( ( content_update_package, ), kwargs ) ] = HG.test_controller.GetWrite( 'content_updates' )
HF.compare_content_update_packages( self, content_update_package, expected_content_update_package )
def _test_manage_cookies( self, connection, set_up_permissions ):
@ -6390,6 +6494,7 @@ class TestClientAPI( unittest.TestCase ):
self._test_add_tags_search_tags( connection, set_up_permissions )
self._test_add_tags_get_tag_siblings_and_parents( connection, set_up_permissions )
self._test_add_urls( connection, set_up_permissions )
self._test_associate_urls( connection, set_up_permissions )
self._test_manage_duplicates( connection, set_up_permissions )
self._test_manage_cookies( connection, set_up_permissions )
self._test_manage_headers( connection, set_up_permissions )

View File

@ -22,11 +22,11 @@ Send2Trash>=1.5.0
service-identity>=18.1.0
Twisted>=20.3.0
opencv-python-headless==4.7.0.72
python-mpv==1.0.3
opencv-python-headless==4.8.1.78
python-mpv==1.0.5
requests==2.31.0
QtPy==2.3.1
PySide6==6.5.2
QtPy==2.4.1
PySide6==6.6.0
setuptools==65.5.1
setuptools==69.1.1

View File

@ -36,6 +36,7 @@ exe = EXE(pyz,
[],
exclude_binaries=True,
name='hydrus_client',
contents_directory='.',
debug=False,
bootloader_ignore_signals=False,
strip=False,

View File

@ -22,6 +22,7 @@ exe = EXE(pyz,
[],
exclude_binaries=True,
name='hydrus_server',
contents_directory='.',
debug=False,
bootloader_ignore_signals=False,
strip=False,

View File

@ -22,14 +22,14 @@ Send2Trash>=1.5.0
service-identity>=18.1.0
Twisted>=20.3.0
opencv-python-headless==4.7.0.72
python-mpv==0.5.2
opencv-python-headless==4.8.1.78
python-mpv==1.0.5
requests==2.31.0
QtPy==2.3.1
PySide6==6.5.2
QtPy==2.4.1
PySide6==6.6.0
setuptools==65.5.1
setuptools==69.1.1
pyinstaller==5.5
pyinstaller==6.2
mkdocs-material

View File

@ -22,15 +22,15 @@ Send2Trash>=1.5.0
service-identity>=18.1.0
Twisted>=20.3.0
opencv-python-headless==4.7.0.72
python-mpv==1.0.3
opencv-python-headless==4.8.1.78
python-mpv==1.0.5
requests==2.31.0
QtPy==2.3.1
PyQt6==6.5.2
PyQt6-Qt6==6.5.2
QtPy==2.4.1
PyQt6==6.6.0
PyQt6-Qt6==6.6.0
setuptools==65.5.1
setuptools==69.1.1
pyobjc-core>=10.1
pyobjc-framework-Cocoa>=10.1

View File

@ -45,9 +45,12 @@ Filename: {app}\hydrus_client.exe; Description: Open the client; Flags: postinst
[Files]
Source: dist\Hydrus Network\*; DestDir: {app}; Flags: ignoreversion recursesubdirs createallsubdirs
[InstallDelete]
;v571: I made this basically do a clean install every time. There is no nice way to say "delete all folders except db", so might need to add specific versioned foldernames in future!
Name: {app}\Crypto; Type: filesandordirs; Components: install
Name: {app}\cv2; Type: filesandordirs; Components: install
Name: {app}\PySide6; Type: filesandordirs; Components: install
Name: {app}\tcl; Type: filesandordirs; Components: install
Name: {app}\tcl8; Type: filesandordirs; Components: install
Name: {app}\tk; Type: filesandordirs; Components: install
Name: {app}\wx; Type: filesandordirs; Components: install
Name: {app}\lz4-3.0.2-py3.7.egg-info; Type: filesandordirs; Components: install
@ -58,23 +61,6 @@ Name: {app}\lib2to3; Type: filesandordirs; Components: install
Name: {app}\mpl-data; Type: filesandordirs; Components: install
Name: {app}\matplotlib; Type: filesandordirs; Components: install
Name: {app}\cryptography; Type: filesandordirs; Components: install
Name: {app}\client.exe; Type: files; Components: install
Name: {app}\server.exe; Type: files; Components: install
Name: {app}\opencv_ffmpeg344_64.dll; Type: files; Components: install
Name: {app}\opencv_ffmpeg400_64.dll; Type: files; Components: install
Name: {app}\opencv_ffmpeg410_64.dll; Type: files; Components: install
Name: {app}\opencv_videoio_ffmpeg411_64.dll; Type: files; Components: install
Name: {app}\opencv_videoio_ffmpeg412_64.dll; Type: files; Components: install
Name: {app}\opencv_videoio_ffmpeg420_64.dll; Type: files; Components: install
Name: {app}\opencv_videoio_ffmpeg440_64.dll; Type: files; Components: install
Name: {app}\wxmsw30u_core_vc140_x64.dll; Type: files; Components: install
Name: {app}\wxmsw30u_adv_vc140_x64.dll; Type: files; Components: install
Name: {app}\wxbase30u_vc140_x64.dll; Type: files; Components: install
Name: {app}\wxbase30u_net_vc140_x64.dll; Type: files; Components: install
Name: {app}\tk86t.dll; Type: files; Components: install
Name: {app}\tcl86t.dll; Type: files; Components: install
Name: {app}\_tkinter.pyd; Type: files; Components: install
Name: {app}\_yaml.cp36-win_amd64.pyd; Type: files; Components: install
Name: {app}\_yaml.cp37-win_amd64.pyd; Type: files; Components: install
Name: {app}\_cffi_backend.cp36-win_amd64.pyd; Type: files; Components: install
Name: {app}\_distutils_findvs.pyd; Type: files; Components: install
Name: {app}\*.exe; Type: files; Components: install
Name: {app}\*.pyd; Type: files; Components: install
Name: {app}\*.dll; Type: files; Components: install

View File

@ -41,6 +41,7 @@ exe = EXE(pyz,
[],
exclude_binaries=True,
name='hydrus_client',
contents_directory='.',
debug=False,
bootloader_ignore_signals=False,
strip=False,

View File

@ -22,6 +22,7 @@ exe = EXE(pyz,
[],
exclude_binaries=True,
name='hydrus_server',
contents_directory='.',
debug=False,
bootloader_ignore_signals=False,
strip=False,

View File

@ -22,16 +22,16 @@ Send2Trash>=1.5.0
service-identity>=18.1.0
Twisted>=20.3.0
opencv-python-headless==4.7.0.72
python-mpv==1.0.3
opencv-python-headless==4.8.1.78
python-mpv==1.0.5
requests==2.31.0
QtPy==2.3.1
PySide6==6.5.2
QtPy==2.4.1
PySide6==6.6.0
setuptools==65.5.1
setuptools==69.1.1
pyinstaller==5.5
pyinstaller==6.2
mkdocs-material
PyWin32

View File

@ -21,4 +21,4 @@ service-identity>=18.1.0
Twisted>=20.3.0
requests==2.31.0
setuptools==65.5.1
setuptools==69.1.1

View File

@ -1 +1 @@
python-mpv==1.0.4
python-mpv==1.0.5

View File

@ -1 +1 @@
opencv-python-headless==4.7.0.72
opencv-python-headless==4.8.1.78

View File

@ -1 +1 @@
opencv-python-headless==4.8.0.76
opencv-python-headless==4.9.0.80

View File

@ -1,2 +1,2 @@
QtPy==2.3.1
PySide6==6.5.2
QtPy==2.4.1
PySide6==6.6.0

View File

@ -1,2 +1,2 @@
QtPy==2.4.1
PySide6==6.6.0
PySide6==6.6.3

View File

@ -12,7 +12,7 @@ Send2Trash>=1.5.0
service-identity>=18.1.0
Twisted>=20.3.0
opencv-python-headless==4.7.0.72
opencv-python-headless==4.8.1.78
requests==2.31.0
setuptools==65.5.1
setuptools==69.1.1

View File

@ -1,4 +1,4 @@
PyInstaller==5.5
PyInstaller==6.2
mock>=4.0.0
httmock>=1.4.0

View File

@ -1,4 +1,4 @@
PyInstaller==5.5
PyInstaller==6.2
mock>=4.0.0
httmock>=1.4.0