diff --git a/db/help my db is broke.txt b/db/help my db is broke.txt
index 4a7686cc..46fec7da 100644
--- a/db/help my db is broke.txt
+++ b/db/help my db is broke.txt
@@ -1,3 +1,12 @@
+*** The purpose of this document ***
+
+This is mostly an explainer for a specific error some databases encounter, 'database image is malformed'. The cloning and repair steps only relate to this specific problem, but as this problem is caused by hard drive damage, users who have other hard drive problems are pointed here as background reading.
+
+Please take a breath and read carefully. Search online if there is anything you don't understand, and plan out what you are going to do. Whatever the problem, do not try to rush it while you are stressed out, since that often causes mistakes.
+
+
+*** Malformed Database ***
+
If you are getting weird but serious-sounding errors like 'database image is malformed', your database may be corrupt!
If your first language is not english, 'db' means 'database'.
@@ -91,7 +100,7 @@ The four files are separate, so if one is broken but the other three are fine, y
*** fix the problem ***
-There are three known repair operations. Try one and see if you can pass an integrity_check on the broken file. Clone works well for most simple problems, but the others have better success in different situations.
+There are three known repair operations. Try one and see if you can pass an integrity_check on the broken file. Clone works well for almost all simple problems, but the others have better success in different situations.
** clone **
@@ -127,7 +136,7 @@ Do not delete your original files for now. Just rename them to 'old' and move th
** repair **
-This command tries to fix a database file in place. It works very very slowly. While it may be able to recover some rows a clones would lose, it cannot fix everything and may leave you with a database that is still malformed, so you'll want to run integrity_check again and do a clone if needed.
+This command tries to fix a database file in place. I do not recommend it as it works very very slowly. While it may be able to recover some rows a clones would lose, it cannot fix everything and may leave you with a database that is still malformed, so you'll want to run integrity_check again and do a clone if needed.
It is very important you have a backup copy of the broken file(s) here, as this edits the file in place.
@@ -216,15 +225,15 @@ If you are absolutely certain you restored your database files to the point wher
If you are in this situation, I am happy to help you figure things out, but you know your system's hardware better than I do. As far as I know, I cannot create a 'malformed' error with my database access routines even if wanted to--it can only come from an external fault.
-*** a note on Write Caching and 'Delayed Write Failed' ***
+*** a note on Write Caching and 'Delayed Write Failed' on NVMe drives ***
-Some users encountered 'malformed' problems alongside Windows OS errors about 'Delayed Write Failed' on the hydrus directory, usually after sleeping and then waking the computer.
+Some users encountered 'malformed' or 'disk I/O error' problems alongside Windows OS errors about 'Delayed Write Failed' on the hydrus directory, usually after sleeping and then waking the computer, but also after the database has finished a lot of work.
-This seems to be something to do with Write Caching failing for certain very large WAL journal files. I believe I have the underlying cause of large WAL files fixed, but if you nonetheless get this specific error, the two things to try seem to be:
+This seems to be something to do with Write Caching failing for certain very large WAL journal files, and particularly affects some NVMe drive(r)s. I have attempted to relieve the large WAL files, but 'disk I/O error' is still happening for some users. The two things to try seem to be:
-1) Disable Write Caching on the drive. This sucks, since write caching is normally really useful, but it seems to help/fix it completely, so it may be ok for a hydrus-exclusive drive.
+1) Launch the program with "--db_journal_mode TRUNCATE" argument, turning off WAL journaling. Check the 'launch arguments' article in the help for more info. This generally fixes the problem completely.
-2) Launch the program with "--db_journal_mode TRUNCATE" argument, turning off WAL journaling. Check the 'launch arguments' article in the help for more info.
+2) Disable Write Caching on the drive. This sucks, since write caching is normally really useful, but it seems to help/fix it completely, so it may be ok for a hydrus-exclusive drive.
*** lastly, if you did not have any backup before this ***
diff --git a/db/help my db is not booting.txt b/db/help my db is not booting.txt
index 908e3198..67b9220d 100644
--- a/db/help my db is not booting.txt
+++ b/db/help my db is not booting.txt
@@ -4,6 +4,6 @@ So, if your client seems suddenly to take a very long time to start up, just sit
You can double-check this by looking at the client executable in your OS's Task Manager (Ctrl+Shift+Esc on Windows). If it is doing some CPU/HDD, there is no need to kill the process.
-Please contact hydrus_dev if it really does seem stuck (say, no progress after an hour, or if CPU/HDD activity completely drops to nothing), or if every startup is delayed like this.
+Please contact hydrus_dev if it really does seem stuck (say, no progress after an hour, or if CPU/HDD activity completely drops to nothing for several minutes), or if every startup is delayed like this.
-One possible cause of delayed startup every time (usually a delay _before_ the splash screen appears) is overly paranoid virus scanners rechecking all of hydrus every time it starts. To relieve this, please check your anti-virus software's options and make sure your hard drive is defragged.
\ No newline at end of file
+One possible cause of delayed startup every time (usually a delay _before_ the splash screen appears) is overly paranoid virus scanners rechecking all of hydrus every time it starts. To relieve this, please check your anti-virus software's options and make sure, if you have an HDD, that your disk is defragged.
\ No newline at end of file
diff --git a/db/help my media files are broke.txt b/db/help my media files are broke.txt
index 507c20dc..cc8039fa 100644
--- a/db/help my media files are broke.txt
+++ b/db/help my media files are broke.txt
@@ -1,3 +1,10 @@
+*** The purpose of this document ***
+
+If you have missing files or missing file records, this document will walk you through the specific steps on how to fix it. If you have encountered a related problem, such as losing one of your 'fxx' sub-folders, it may also help as background reading.
+
+
+*** Missing Files ***
+
If you have lost lots of media files due to a mistake or a drive failing, or if you have rolled back to an older backup and now your file structure is out of sync with your database record, there are several things to do.
First off, make sure you are running on good hard drives now. Check the 'help my db is broke' help document for info here. If your .db files were also on a failing disk, you'll want to run through that whole document to make sure your database itself is ok--there's no point trying to fix the file record on a malformed database.
diff --git a/docs/changelog.md b/docs/changelog.md
index 4bb28267..a172fb1b 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -3,6 +3,53 @@
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
+## [Version 491](https://github.com/hydrusnetwork/hydrus/releases/tag/v491)
+
+### system predicates
+* the advanced OR input, where you can type tags in complicated logical expressions, now supports system predicates! most system predicates are supported using their typical display strings. it uses the same engine as the client api, so check the examples here https://hydrusnetwork.github.io/hydrus/developer_api.html#get_files_search_files sorry for the delay here
+* the advanced input also runs tags better through the hydrus tag 'cleaning' process, so things like whitespace between the namespace colon and the subtag are cleaned up correctly, and invalid tags should be excluded
+* it also starts with the keyboard focus in the text input
+* and I think I fixed an issue with '!'', 'not', or '-' negation prefixes not parsing
+* highlighted the example parseable system predicate texts in the Client API help, and added 'last viewed' to it
+
+### misc
+* altering your services in _manage services_ no longer causes a full page refresh for all currently open search pages
+* in a related thing, if you click the file or tag domain of a file search page to be the same as it just was, you no longer get a page refresh
+* the rating widgets now show their current rating value on their tooltips
+* when setting a numerical rating by a drag, it no longer matters if your mouse strays above or below the widget--it will still set
+* the String Processing system has a new 'String Tag Filter' processing step. this applies the normal tag filtering object to your list of strings and also performs the hydrus 'tag cleaning' process on them, making them all lowercase and trimming whitespace and so on
+* the sibling/parent sync is now even more polite when told to do work in 'normal' time. this has been hitting a lot of new users really hard, so it should now really trickle work during normal time, throttling down when it hits a bump to avoid stunlocking you but also responding quickly to recent changes if you are fully synced
+* the database repair code is now better at healing damaged fast-text-search (FTS) tables. previously, in cases of partial damage to the virtual table, the repair code would error out
+* fixed a bug where certain search predicate calendar dates that are acceptable in Linux but not in Windows caused Windows to fail to load the session. if you put in 1965 as a search date, it should now revert to the current time one next load etc...
+* the test to see if a directory is writeable-to is improved and now handles Windows's Program Files directory correctly
+* improved how the boot scripts handle incorrect/bad database directory paths. the error handling works better, and it figures out a fallback location for crash.log better
+* a new button on 'review services' now lets advanced users copy the service key to the clipboard
+* the migrate tags dialog now lists file repositories, ipfs services, and 'all my files' as potential file filter domains
+* when checking it has space for a large transaction like a vacuum, hydrus now tries to check if you are running on a ramdisk or other severely space-limited temp dir and offers more text if this is true
+* updated the '4chan style thread api parser' to handle posts with multiple files, which fixes tvchan.moe and probably anything else running NPFchan
+* some logic testing around showing 'return to inbox' and the actual operation is fixed so it only applies to local files. in some weird advanced situations, you could previously send deleted files to inbox
+
+### new import/export framework
+* started a new modular metadata import/export pipeline. this thing starts out today by doing the work of newline-separated tags in a .txt sidecar file and will expand to do all sorts of metadata in other formats like JSON and XML. it will also, eventually, support arbitrary cross-type conversions like tags to urls or ratings to tags
+* export folders now support '.txt' sidecar tag exporting!
+* the '.txt' sidecar tag importing in import folders or manual imports is now handled by the new pipeline
+* the '.txt' sidecar exporting in the manual export dialog is now handled by the new pipeline
+* please expect the UI around '.txt' sidecar importing and exporting to change significantly in future. you'll be selecting different metadata types to import or export, make string processing steps to alter or filter what you get, and of course be able to compile it all into more complicated filetypes
+
+### cleanup and refactoring
+* mr bones gets two new columns to line up the numbers better
+* a bunch of export code got moved around. created a new module 'exporting', and moved ClientExporting.py to it, renaming to ClientExportingFiles.py
+* removed an old prototype for sidecar exporting and related plans for UI
+* the 'missing file folders on boot' dialog now points users to 'help my media files are broke.txt'
+* brushed up the 'help my x is broke.txt' documents in the database directory a little
+* fixed some surplus double backslashes in the help
+* a secret tiny label change/fix, let's see if anyone notices
+* cleaned up how the rating widgets manage and update rating state. it was ancient bad code
+* updated how different rating values are converted to UI text
+* misc cleanup of some free space checking code
+* fixed some bad quote characters in client api help JSON examples
+* improved some error handling for uploading pending content and sped up file uploads a little
+
## [Version 490](https://github.com/hydrusnetwork/hydrus/releases/tag/v490)
### misc
@@ -293,32 +340,3 @@
* added a unit test to test this
* added archive timestamp and hash hex sort enum definitions to the 'search_files' client api help
* client api version is now 31
-
-## [Version 480](https://github.com/hydrusnetwork/hydrus/releases/tag/v480)
-
-### file notes and media viewer hover windows
-* file notes are now shown on the media viewer! this is a first version, pretty ugly, and may have font layout bugs for some systems, but it works. they hang just below the top-right hover, both in the canvas background and with their own hover if you mouseover. clicking on any note will open 'edit notes' on that note
-* the duplicate filter's always-on hover _should_ slide out of the way when there are many notes
-* furthermore, I rewrote the backend of hover windows. they are now embedded into the media viewer rather than being separate frameless toolbar windows. this should relieve several problems different users had--for instance, if you click a hover, you now no longer lose focus on the main media viewer window. I hacked some of this to get it to work, but along the way I undid three other hacks, so overall it should be better. please let me know how this works for you!
-* fixed a long time hover window positioning bug where the top-right window would sometimes pop in for a frame the first time you moved the mouse to the top middle before repositioning and hiding itself again
-* removed the 'notes' icon from the top right hover window
-* refactored a bunch of canvas background code
-
-### client api
-* search_files/get_thumbnail now returns image/jpeg or image/png Content-Type. it _should_ be super fast, but let me know if it lags after 3k thumbs or something
-* you can now ask for CBOR or JSON specifically by using the 'Accept' request header, regardless of your own request Content-Type (issue #1110)
-* if you send or ask for CBOR but it is not available for that client, you now get a new 'Not Acceptable' 406 response (previously it would 500 or 200 but in JSON)
-* updated the help regarding the above and wrote some unit tests to check CBOR/JSON requests and responses
-* client api version is now 30
-
-### misc
-* added a link to 'Hyshare', at https://github.com/floogulinc/hyshare, to the Client API help. it is a neat way to share galleries with friends, just like the the old 'local booru'
-* building on last week's shift-select improvement, I tweaked it and shift-select and ctrl-select are back to not setting the preview focus. you can ctrl-click a bunch of vids in quick silence again
-* the menu on the 'file log' button is now attached to the downloader page lists and the menu when you right-click on the file log panel. you can now access these actions without having to highlight a big query
-* the same is also true of the search/check log!
-* when you select a new downloader in the gallery download page, the keyboard focus now moves immediately to the query text input box
-* tweaked the zoom locking code in the duplicate filter again. the 'don't lock that way if there is spillover' test, which is meant to stop garbage site banners from being hidden just offscreen, is much more strict. it now only cares about 10% or so spillover, assuming that with a large 'B' the spillover will be obvious. this should improve some odd zoom locking situations where the first pair change was ok and the rest were weird
-* if you exit the client before the first session loads (either it is really huge or a problem breaks/delays your boot) the client will not save any 'last/exit session' (previously, it was saving empty here, requiring inconvenient load from a backup)
-* if you have a really really huge session, the client is now more careful about not booting delayed background tasks like subscriptions until the session is in place
-* on 'migrate database', the thumbnail size estimate now has a min-max range and a tooltip to clarify that it is an estimate
-* fixed a bug in the new 'sort by file hash' pre-sort when applying system:limit
diff --git a/docs/developer_api.md b/docs/developer_api.md
index 668fb594..b68ab78b 100644
--- a/docs/developer_api.md
+++ b/docs/developer_api.md
@@ -256,10 +256,10 @@ Response:
"service_key": "616c6c206c6f63616c2066696c6573"
}
],
- 'all_local_media': [
+ "all_local_media": [
{
- 'name': 'all my files',
- 'service_key': '616c6c206c6f63616c206d65646961'
+ "name": "all my files",
+ "service_key": "616c6c206c6f63616c206d65646961"
}
],
"all_known_files": [
@@ -301,7 +301,7 @@ Arguments (in JSON):
: - `path`: (the path you want to import)
```json title="Example request body"
-{"path": "E:\\to_import\\ayanami.jpg"}
+{"path": "E:\to_import\ayanami.jpg"}
```
Arguments (as bytes):
@@ -1286,7 +1286,7 @@ If the access key's permissions only permit search for certain tags, at least on
Wildcards and namespace searches are supported, so if you search for 'character:sam*' or 'series:*', this will be handled correctly clientside.
-Many system predicates are also supported using a text parser! The parser was designed by a clever user for human input and allows for a certain amount of error (e.g. ~= instead of ≈, or "isn't" instead of "is not") or requires more information (e.g. the specific hashes for a hash lookup). Here's a big list of current formats supported:
+**Many system predicates are also supported using a text parser!** The parser was designed by a clever user for human input and allows for a certain amount of error (e.g. ~= instead of ≈, or "isn't" instead of "is not") or requires more information (e.g. the specific hashes for a hash lookup). **Here's a big list of examples that are supported:**
??? example "System Predicates"
* system:everything
@@ -1323,8 +1323,11 @@ Many system predicates are also supported using a text parser! The parser was de
* system:hash = abcdef01 abcdef02 md5
* system:modified date < 7 years 45 days 7h
* system:modified date > 2011-06-04
+ * system:last viewed time < 7 years 45 days 7h
+ * system:last view time < 7 years 45 days 7h
* system:date modified > 7 years 2 months
* system:date modified < 0 years 1 month 1 day 1 hour
+ * system:import time < 7 years 45 days 7h
* system:time imported < 7 years 45 days 7h
* system:time imported > 2011-06-04
* system:time imported > 7 years 2 months
@@ -1370,7 +1373,7 @@ Many system predicates are also supported using a text parser! The parser was de
* system:no note with name note name
* system:does not have note with name note name
-More system predicate types and input formats will be available in future. Please test out the system predicates you want to send. Reverse engineering system predicate data from text is obviously tricky. If a system predicate does not parse, you'll get 400.
+Please test out the system predicates you want to send. If you are in _help->advanced mode_, you can test this parser in the advanced text input dialog when you click the OR\* button on a tag autocomplete dropdown. More system predicate types and input formats will be available in future. Reverse engineering system predicate data from text is obviously tricky. If a system predicate does not parse, you'll get 400.
Also, OR predicates are now supported! Just nest within the tag list, and it'll be treated like an OR. For instance:
diff --git a/docs/launch_arguments.md b/docs/launch_arguments.md
index 5cb2a91f..1878f0d6 100644
--- a/docs/launch_arguments.md
+++ b/docs/launch_arguments.md
@@ -23,7 +23,7 @@ Which gives you a full listing of all below arguments, however this will not wor
Lets you customise where hydrus should use for its base database directory. This is install_dir/db by default, but many advanced deployments will move this around, as described [here](database_migration.md). When an argument takes a complicated value like a path that could itself include whitespace, you should wrap it in quote marks, like this:
```
--d="E:\\my hydrus\\hydrus db"
+-d="E:\my hydrus\hydrus db"
```
##**`--temp_dir TEMP_DIR`**
@@ -31,7 +31,7 @@ Lets you customise where hydrus should use for its base database directory. This
This tells all aspects of the client, including the SQLite database, to use a different path for temp operations. This would be by default your system temp path, such as:
```
-C:\\Users\\You\\AppData\\Local\\Temp
+C:\Users\You\AppData\Local\Temp
```
But you can also check it in _help->about_. A handful of database operations (PTR tag processing, vacuums) require a lot of free space, so if your system drive is very full, or you have unusual ramdisk-based temp storage limits, you may want to relocate to another location or drive.
diff --git a/docs/old_changelog.html b/docs/old_changelog.html
index f5b1b503..b6b5a77e 100644
--- a/docs/old_changelog.html
+++ b/docs/old_changelog.html
@@ -33,6 +33,53 @@
+
+
+ - system predicates:
+ - the advanced OR input, where you can type tags in complicated logical expressions, now supports system predicates! most system predicates are supported using their typical display strings. it uses the same engine as the client api, so check the examples here https://hydrusnetwork.github.io/hydrus/developer_api.html#get_files_search_files sorry for the delay here
+ - the advanced input also runs tags better through the hydrus tag 'cleaning' process, so things like whitespace between the namespace colon and the subtag are cleaned up correctly, and invalid tags should be excluded
+ - it also starts with the keyboard focus in the text input
+ - and I think I fixed an issue with '!'', 'not', or '-' negation prefixes not parsing
+ - highlighted the example parseable system predicate texts in the Client API help, and added 'last viewed' to it
+ - .
+ - misc:
+ - altering your services in _manage services_ no longer causes a full page refresh for all currently open search pages
+ - in a related thing, if you click the file or tag domain of a file search page to be the same as it just was, you no longer get a page refresh
+ - the rating widgets now show their current rating value on their tooltips
+ - when setting a numerical rating by a drag, it no longer matters if your mouse strays above or below the widget--it will still set
+ - the String Processing system has a new 'String Tag Filter' processing step. this applies the normal tag filtering object to your list of strings and also performs the hydrus 'tag cleaning' process on them, making them all lowercase and trimming whitespace and so on
+ - the sibling/parent sync is now even more polite when told to do work in 'normal' time. this has been hitting a lot of new users really hard, so it should now really trickle work during normal time, throttling down when it hits a bump to avoid stunlocking you but also responding quickly to recent changes if you are fully synced
+ - the database repair code is now better at healing damaged fast-text-search (FTS) tables. previously, in cases of partial damage to the virtual table, the repair code would error out
+ - fixed a bug where certain search predicate calendar dates that are acceptable in Linux but not in Windows caused Windows to fail to load the session. if you put in 1965 as a search date, it should now revert to the current time one next load etc...
+ - the test to see if a directory is writeable-to is improved and now handles Windows's Program Files directory correctly
+ - improved how the boot scripts handle incorrect/bad database directory paths. the error handling works better, and it figures out a fallback location for crash.log better
+ - a new button on 'review services' now lets advanced users copy the service key to the clipboard
+ - the migrate tags dialog now lists file repositories, ipfs services, and 'all my files' as potential file filter domains
+ - when checking it has space for a large transaction like a vacuum, hydrus now tries to check if you are running on a ramdisk or other severely space-limited temp dir and offers more text if this is true
+ - updated the '4chan style thread api parser' to handle posts with multiple files, which fixes tvchan.moe and probably anything else running NPFchan
+ - some logic testing around showing 'return to inbox' and the actual operation is fixed so it only applies to local files. in some weird advanced situations, you could previously send deleted files to inbox
+ - .
+ - new import/export framework:
+ - started a new modular metadata import/export pipeline. this thing starts out today by doing the work of newline-separated tags in a .txt sidecar file and will expand to do all sorts of metadata in other formats like JSON and XML. it will also, eventually, support arbitrary cross-type conversions like tags to urls or ratings to tags
+ - export folders now support '.txt' sidecar tag exporting!
+ - the '.txt' sidecar tag importing in import folders or manual imports is now handled by the new pipeline
+ - the '.txt' sidecar exporting in the manual export dialog is now handled by the new pipeline
+ - please expect the UI around '.txt' sidecar importing and exporting to change significantly in future. you'll be selecting different metadata types to import or export, make string processing steps to alter or filter what you get, and of course be able to compile it all into more complicated filetypes
+ - .
+ - cleanup and refactoring:
+ - mr bones gets two new columns to line up the numbers better
+ - a bunch of export code got moved around. created a new module 'exporting', and moved ClientExporting.py to it, renaming to ClientExportingFiles.py
+ - removed an old prototype for sidecar exporting and related plans for UI
+ - the 'missing file folders on boot' dialog now points users to 'help my media files are broke.txt'
+ - brushed up the 'help my x is broke.txt' documents in the database directory a little
+ - fixed some surplus double backslashes in the help
+ - a secret tiny label change/fix, let's see if anyone notices
+ - cleaned up how the rating widgets manage and update rating state. it was ancient bad code
+ - updated how different rating values are converted to UI text
+ - misc cleanup of some free space checking code
+ - fixed some bad quote characters in client api help JSON examples
+ - improved some error handling for uploading pending content and sped up file uploads a little
+
- misc:
diff --git a/hydrus/client/ClientApplicationCommand.py b/hydrus/client/ClientApplicationCommand.py
index 92fd82b9..695a7eea 100644
--- a/hydrus/client/ClientApplicationCommand.py
+++ b/hydrus/client/ClientApplicationCommand.py
@@ -675,7 +675,7 @@ class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
service = HG.client_controller.services_manager.GetService( service_key )
- value_string = service.ConvertRatingToString( value )
+ value_string = service.ConvertNoneableRatingToString( value )
except HydrusExceptions.DataMissing:
diff --git a/hydrus/client/ClientSearch.py b/hydrus/client/ClientSearch.py
index 16cce625..40140829 100644
--- a/hydrus/client/ClientSearch.py
+++ b/hydrus/client/ClientSearch.py
@@ -409,8 +409,15 @@ class FileSystemPredicates( object ):
# convert this dt, which is in local time, to a gmt timestamp
- day_dt = datetime.datetime( year, month, day )
- timestamp = int( time.mktime( day_dt.timetuple() ) )
+ try:
+
+ day_dt = datetime.datetime( year, month, day )
+ timestamp = int( time.mktime( day_dt.timetuple() ) )
+
+ except:
+
+ timestamp = HydrusData.GetNow()
+
if operator == '<':
@@ -2263,8 +2270,15 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
dt = datetime.datetime( year, month, day )
- # make a timestamp (IN GMT SECS SINCE 1970) from the local meaning of 2018/02/01
- timestamp = int( time.mktime( dt.timetuple() ) )
+ try:
+
+ # make a timestamp (IN GMT SECS SINCE 1970) from the local meaning of 2018/02/01
+ timestamp = int( time.mktime( dt.timetuple() ) )
+
+ except:
+
+ timestamp = HydrusData.GetNow()
+
if operator == '<':
@@ -2429,7 +2443,7 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
else:
- pretty_value = service.ConvertRatingToString( value )
+ pretty_value = service.ConvertNoneableRatingToString( value )
base += ' for {} {} {}'.format( service.GetName(), operator, pretty_value )
diff --git a/hydrus/client/ClientServices.py b/hydrus/client/ClientServices.py
index 09f050c4..25a9e186 100644
--- a/hydrus/client/ClientServices.py
+++ b/hydrus/client/ClientServices.py
@@ -576,7 +576,7 @@ class ServiceLocalRating( Service ):
self._colours = dict( dictionary[ 'colours' ] )
- def ConvertRatingToString( self, rating: typing.Optional[ float ] ):
+ def ConvertNoneableRatingToString( self, rating: typing.Optional[ float ] ):
raise NotImplementedError()
@@ -599,7 +599,7 @@ class ServiceLocalRating( Service ):
class ServiceLocalRatingLike( ServiceLocalRating ):
- def ConvertRatingToString( self, rating: typing.Optional[ float ] ):
+ def ConvertNoneableRatingToString( self, rating: typing.Optional[ float ] ):
if rating is None:
@@ -620,6 +620,30 @@ class ServiceLocalRatingLike( ServiceLocalRating ):
return 'unknown'
+ def ConvertRatingStateToString( self, rating_state: int ):
+
+ if rating_state == ClientRatings.LIKE:
+
+ return 'like'
+
+ elif rating_state == ClientRatings.DISLIKE:
+
+ return 'dislike'
+
+ elif rating_state == ClientRatings.MIXED:
+
+ return 'mixed'
+
+ elif rating_state == ClientRatings.NULL:
+
+ return 'not set'
+
+ else:
+
+ return 'unknown'
+
+
+
class ServiceLocalRatingNumerical( ServiceLocalRating ):
def _GetSerialisableDictionary( self ):
@@ -648,21 +672,7 @@ class ServiceLocalRatingNumerical( ServiceLocalRating ):
- def ConvertRatingToStars( self, rating: float ) -> int:
-
- if self._allow_zero:
-
- stars = int( round( rating * self._num_stars ) )
-
- else:
-
- stars = int( round( rating * ( self._num_stars - 1 ) ) ) + 1
-
-
- return stars
-
-
- def ConvertRatingToString( self, rating: typing.Optional[ float ] ):
+ def ConvertNoneableRatingToString( self, rating: typing.Optional[ float ] ):
if rating is None:
@@ -679,6 +689,40 @@ class ServiceLocalRatingNumerical( ServiceLocalRating ):
return 'unknown'
+ def ConvertRatingStateAndRatingToString( self, rating_state: int, rating: float ):
+
+ if rating_state == ClientRatings.SET:
+
+ return self.ConvertNoneableRatingToString( rating )
+
+ elif rating_state == ClientRatings.MIXED:
+
+ return 'mixed'
+
+ elif rating_state == ClientRatings.NULL:
+
+ return 'not set'
+
+ else:
+
+ return 'unknown'
+
+
+
+ def ConvertRatingToStars( self, rating: float ) -> int:
+
+ if self._allow_zero:
+
+ stars = int( round( rating * self._num_stars ) )
+
+ else:
+
+ stars = int( round( rating * ( self._num_stars - 1 ) ) ) + 1
+
+
+ return stars
+
+
def ConvertStarsToRating( self, stars: int ) -> float:
if self._allow_zero:
@@ -3189,6 +3233,7 @@ class ServiceIPFS( ServiceRemote ):
network_job.WaitUntilDone()
+
parsing_text = network_job.GetContentText()
diff --git a/hydrus/client/ClientStrings.py b/hydrus/client/ClientStrings.py
index cfab0812..895c473c 100644
--- a/hydrus/client/ClientStrings.py
+++ b/hydrus/client/ClientStrings.py
@@ -11,6 +11,7 @@ from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusSerialisable
+from hydrus.core import HydrusTags
STRING_CONVERSION_REMOVE_TEXT_FROM_BEGINNING = 0
STRING_CONVERSION_REMOVE_TEXT_FROM_END = 1
@@ -244,7 +245,14 @@ class StringConverter( StringProcessingStep ):
# the given struct is in local time, so time.mktime is correct
- timestamp = int( time.mktime( struct_time ) )
+ try:
+
+ timestamp = int( time.mktime( struct_time ) )
+
+ except:
+
+ timestamp = HydrusData.GetNow()
+
elif timezone == HC.TIMEZONE_OFFSET:
@@ -1057,6 +1065,134 @@ class StringSplitter( StringProcessingStep ):
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_STRING_SPLITTER ] = StringSplitter
+class StringTagFilter( StringProcessingStep ):
+
+ SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_STRING_TAG_FILTER
+ SERIALISABLE_NAME = 'String Tag Filter'
+ SERIALISABLE_VERSION = 1
+
+ def __init__( self, tag_filter = None, example_string = 'blue eyes' ):
+
+ StringProcessingStep.__init__( self )
+
+ if tag_filter is None:
+
+ tag_filter = HydrusTags.TagFilter()
+
+
+ self._tag_filter = tag_filter
+
+ self._example_string = example_string
+
+
+ def _GetSerialisableInfo( self ):
+
+ serialisable_tag_filter = self._tag_filter.GetSerialisableTuple()
+
+ return ( serialisable_tag_filter, self._example_string )
+
+
+ def _InitialiseFromSerialisableInfo( self, serialisable_info ):
+
+ ( serialisable_tag_filter, self._example_string ) = serialisable_info
+
+ self._tag_filter = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_tag_filter )
+
+
+ def ConvertAndFilter( self, tag_texts ):
+
+ tags = HydrusTags.CleanTags( tag_texts )
+
+ tags = self._tag_filter.Filter( tags, apply_unnamespaced_rules_to_namespaced_tags = True )
+
+ tags = sorted( tags, key = HydrusTags.ConvertTagToSortable )
+
+ return tags
+
+
+ def GetExampleString( self ) -> str:
+
+ return self._example_string
+
+
+ def GetTagFilter( self ) -> HydrusTags.TagFilter:
+
+ return self._tag_filter
+
+
+ def MakesChanges( self ) -> bool:
+
+ # it always scans for valid tags
+
+ return True
+
+
+ def Matches( self, text ):
+
+ try:
+
+ self.Test( text )
+
+ return True
+
+ except HydrusExceptions.StringMatchException:
+
+ return False
+
+
+
+ def Test( self, text ):
+
+ if isinstance( text, bytes ):
+
+ raise HydrusExceptions.StringMatchException( 'Got a bytes value in a string match!' )
+
+
+ presentation_text = '"{}"'.format( text )
+
+ try:
+
+ tags = HydrusTags.CleanTags( [ text ] )
+
+ if len( tags ) == 0:
+
+ raise Exception()
+
+ else:
+
+ tag = list( tags )[0]
+
+
+ except:
+
+ raise HydrusExceptions.StringMatchException( '{} was not a valid tag!'.format( presentation_text ) )
+
+
+ if not self._tag_filter.TagOK( tag, apply_unnamespaced_rules_to_namespaced_tags = True ):
+
+ raise HydrusExceptions.StringMatchException( '{} did not pass the tag filter!'.format( presentation_text ) )
+
+
+
+ def ToString( self, simple = False, with_type = False ) -> str:
+
+ if simple:
+
+ return 'tag filter'
+
+
+ result = '{}, such as {}'.format( self._tag_filter.ToPermittedString(), self._example_string )
+
+ if with_type:
+
+ result = 'TAG FILTER: {}'.format( result )
+
+
+ return result
+
+
+HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_STRING_TAG_FILTER ] = StringTagFilter
+
class StringProcessor( StringProcessingStep ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_STRING_PROCESSOR
@@ -1146,6 +1282,17 @@ class StringProcessor( StringProcessingStep ):
+ elif isinstance( processing_step, StringTagFilter ):
+
+ try:
+
+ next_strings = processing_step.ConvertAndFilter( current_strings )
+
+ except:
+
+ next_strings = current_strings
+
+
else:
next_strings = []
diff --git a/hydrus/client/db/ClientDB.py b/hydrus/client/db/ClientDB.py
index 9e86dd39..79aa0d7f 100644
--- a/hydrus/client/db/ClientDB.py
+++ b/hydrus/client/db/ClientDB.py
@@ -6377,6 +6377,10 @@ class DB( HydrusDB.HydrusDB ):
def _InboxFiles( self, hash_ids ):
+ location_context = ClientLocation.LocationContext( current_service_keys = ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, ) )
+
+ hash_ids = self.modules_files_storage.FilterHashIds( location_context, hash_ids )
+
inboxed_hash_ids = self.modules_files_metadata_basic.InboxFiles( hash_ids )
if len( inboxed_hash_ids ) > 0:
@@ -11687,6 +11691,36 @@ class DB( HydrusDB.HydrusDB ):
self._Execute( 'DELETE FROM service_info WHERE service_id = ?;', ( self.modules_services.local_update_service_id, ) )
+ if version == 490:
+
+ try:
+
+ domain_manager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
+
+ domain_manager.Initialise()
+
+ #
+
+ domain_manager.OverwriteDefaultParsers( ( '4chan-style thread api parser', ) )
+
+ #
+
+ domain_manager.TryToLinkURLClassesAndParsers()
+
+ #
+
+ self.modules_serialisable.SetJSONDump( domain_manager )
+
+ except Exception as e:
+
+ HydrusData.PrintException( e )
+
+ message = 'Trying to update some downloader objects failed! Please let hydrus dev know!'
+
+ self.pub_initial_message( message )
+
+
+
self._controller.frame_splash_status.SetTitleText( 'updated db to v{}'.format( HydrusData.ToHumanInt( version + 1 ) ) )
self._Execute( 'UPDATE version SET version = ?;', ( version + 1, ) )
diff --git a/hydrus/client/db/ClientDBFilesStorage.py b/hydrus/client/db/ClientDBFilesStorage.py
index 304a8c68..5ba234e8 100644
--- a/hydrus/client/db/ClientDBFilesStorage.py
+++ b/hydrus/client/db/ClientDBFilesStorage.py
@@ -691,7 +691,7 @@ class ClientDBFilesStorage( ClientDBModule.ClientDBModule ):
for ( table_name, ( create_query_without_name, version_added ) ) in table_generation_dict.items():
- self._Execute( create_query_without_name.format( table_name ) )
+ self._CreateTable( create_query_without_name, table_name )
index_generation_dict = self._GetServiceIndexGenerationDict( service_id )
diff --git a/hydrus/client/db/ClientDBMappingsCacheSpecificDisplay.py b/hydrus/client/db/ClientDBMappingsCacheSpecificDisplay.py
index c4b58c9a..539e33f2 100644
--- a/hydrus/client/db/ClientDBMappingsCacheSpecificDisplay.py
+++ b/hydrus/client/db/ClientDBMappingsCacheSpecificDisplay.py
@@ -532,7 +532,7 @@ class ClientDBMappingsCacheSpecificDisplay( ClientDBModule.ClientDBModule ):
for ( table_name, ( create_query_without_name, version_added ) ) in table_generation_dict.items():
- self._Execute( create_query_without_name.format( table_name ) )
+ self._CreateTable( create_query_without_name, table_name )
if populate_from_storage:
diff --git a/hydrus/client/db/ClientDBMappingsCacheSpecificStorage.py b/hydrus/client/db/ClientDBMappingsCacheSpecificStorage.py
index 3ff2636b..da734da4 100644
--- a/hydrus/client/db/ClientDBMappingsCacheSpecificStorage.py
+++ b/hydrus/client/db/ClientDBMappingsCacheSpecificStorage.py
@@ -323,7 +323,7 @@ class ClientDBMappingsCacheSpecificStorage( ClientDBModule.ClientDBModule ):
for ( table_name, ( create_query_without_name, version_added ) ) in table_generation_dict.items():
- self._Execute( create_query_without_name.format( table_name ) )
+ self._CreateTable( create_query_without_name, table_name )
self.modules_mappings_counts.CreateTables( ClientTags.TAG_DISPLAY_STORAGE, file_service_id, tag_service_id )
diff --git a/hydrus/client/db/ClientDBMappingsCounts.py b/hydrus/client/db/ClientDBMappingsCounts.py
index f8e9e0a7..ae85b594 100644
--- a/hydrus/client/db/ClientDBMappingsCounts.py
+++ b/hydrus/client/db/ClientDBMappingsCounts.py
@@ -189,7 +189,7 @@ class ClientDBMappingsCounts( ClientDBModule.ClientDBModule ):
for ( table_name, ( create_query_without_name, version_added ) ) in table_generation_dict.items():
- self._Execute( create_query_without_name.format( table_name ) )
+ self._CreateTable( create_query_without_name, table_name )
#
diff --git a/hydrus/client/db/ClientDBMappingsStorage.py b/hydrus/client/db/ClientDBMappingsStorage.py
index ffb5c7d0..2879c915 100644
--- a/hydrus/client/db/ClientDBMappingsStorage.py
+++ b/hydrus/client/db/ClientDBMappingsStorage.py
@@ -135,7 +135,7 @@ class ClientDBMappingsStorage( ClientDBModule.ClientDBModule ):
for ( table_name, ( create_query_without_name, version_added ) ) in table_generation_dict.items():
- self._Execute( create_query_without_name.format( table_name ) )
+ self._CreateTable( create_query_without_name, table_name )
index_generation_dict = self._GetServiceIndexGenerationDict( service_id )
diff --git a/hydrus/client/db/ClientDBRepositories.py b/hydrus/client/db/ClientDBRepositories.py
index 27c7a8a0..ca5b6abf 100644
--- a/hydrus/client/db/ClientDBRepositories.py
+++ b/hydrus/client/db/ClientDBRepositories.py
@@ -322,7 +322,7 @@ class ClientDBRepositories( ClientDBModule.ClientDBModule ):
for ( table_name, ( create_query_without_name, version_added ) ) in table_generation_dict.items():
- self._Execute( create_query_without_name.format( table_name ) )
+ self._CreateTable( create_query_without_name, table_name )
index_generation_dict = self._GetServiceIndexGenerationDict( service_id )
diff --git a/hydrus/client/db/ClientDBTagParents.py b/hydrus/client/db/ClientDBTagParents.py
index adc10e1d..37cfae0c 100644
--- a/hydrus/client/db/ClientDBTagParents.py
+++ b/hydrus/client/db/ClientDBTagParents.py
@@ -222,7 +222,7 @@ class ClientDBTagParents( ClientDBModule.ClientDBModule ):
for ( table_name, ( create_query_without_name, version_added ) ) in table_generation_dict.items():
- self._Execute( create_query_without_name.format( table_name ) )
+ self._CreateTable( create_query_without_name, table_name )
index_generation_dict = self._GetServiceIndexGenerationDict( tag_service_id )
diff --git a/hydrus/client/db/ClientDBTagSearch.py b/hydrus/client/db/ClientDBTagSearch.py
index 43b5293a..720a415f 100644
--- a/hydrus/client/db/ClientDBTagSearch.py
+++ b/hydrus/client/db/ClientDBTagSearch.py
@@ -415,7 +415,7 @@ class ClientDBTagSearch( ClientDBModule.ClientDBModule ):
for ( table_name, ( create_query_without_name, version_added ) ) in table_generation_dict.items():
- self._Execute( create_query_without_name.format( table_name ) )
+ self._CreateTable( create_query_without_name, table_name )
index_generation_dict = self._GetServiceIndexGenerationDictSingle( file_service_id, tag_service_id )
diff --git a/hydrus/client/db/ClientDBTagSiblings.py b/hydrus/client/db/ClientDBTagSiblings.py
index e5ea65c3..4c286f30 100644
--- a/hydrus/client/db/ClientDBTagSiblings.py
+++ b/hydrus/client/db/ClientDBTagSiblings.py
@@ -262,7 +262,7 @@ class ClientDBTagSiblings( ClientDBModule.ClientDBModule ):
for ( table_name, ( create_query_without_name, version_added ) ) in table_generation_dict.items():
- self._Execute( create_query_without_name.format( table_name ) )
+ self._CreateTable( create_query_without_name, table_name )
index_generation_dict = self._GetServiceIndexGenerationDict( tag_service_id )
diff --git a/hydrus/client/ClientExporting.py b/hydrus/client/exporting/ClientExportingFiles.py
similarity index 85%
rename from hydrus/client/ClientExporting.py
rename to hydrus/client/exporting/ClientExportingFiles.py
index 9642032e..16698d0b 100644
--- a/hydrus/client/ClientExporting.py
+++ b/hydrus/client/exporting/ClientExportingFiles.py
@@ -1,6 +1,7 @@
import collections
import os
import re
+import typing
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
@@ -12,10 +13,9 @@ from hydrus.core import HydrusTags
from hydrus.core import HydrusThreading
from hydrus.client import ClientConstants as CC
-from hydrus.client import ClientFiles
-from hydrus.client import ClientLocation
from hydrus.client import ClientPaths
from hydrus.client import ClientSearch
+from hydrus.client.exporting import ClientExportingMetadata
from hydrus.client.media import ClientMediaManagers
from hydrus.client.metadata import ClientTags
from hydrus.client.metadata import ClientTagSorting
@@ -277,10 +277,24 @@ class ExportFolder( HydrusSerialisable.SerialisableBaseNamed ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_EXPORT_FOLDER
SERIALISABLE_NAME = 'Export Folder'
- SERIALISABLE_VERSION = 4
- SERIALISABLE_VERSION = 5
+ SERIALISABLE_VERSION = 6
- def __init__( self, name, path = '', export_type = HC.EXPORT_FOLDER_TYPE_REGULAR, delete_from_client_after_export = False, file_search_context = None, run_regularly = True, period = 3600, phrase = None, last_checked = 0, paused = False, run_now = False, last_error = '' ):
+ def __init__(
+ self,
+ name,
+ path = '',
+ export_type = HC.EXPORT_FOLDER_TYPE_REGULAR,
+ delete_from_client_after_export = False,
+ file_search_context = None,
+ metadata_routers = None,
+ run_regularly = True,
+ period = 3600,
+ phrase = None,
+ last_checked = 0,
+ paused = False,
+ run_now = False,
+ last_error = ''
+ ):
HydrusSerialisable.SerialisableBaseNamed.__init__( self, name )
@@ -296,6 +310,11 @@ class ExportFolder( HydrusSerialisable.SerialisableBaseNamed ):
file_search_context = ClientSearch.FileSearchContext( location_context = default_location_context )
+ if metadata_routers is None:
+
+ metadata_routers = []
+
+
if phrase is None:
phrase = HG.client_controller.new_options.GetString( 'export_phrase' )
@@ -305,6 +324,7 @@ class ExportFolder( HydrusSerialisable.SerialisableBaseNamed ):
self._export_type = export_type
self._delete_from_client_after_export = delete_from_client_after_export
self._file_search_context = file_search_context
+ self._metadata_routers = HydrusSerialisable.SerialisableList( metadata_routers )
self._run_regularly = run_regularly
self._period = period
self._phrase = phrase
@@ -317,13 +337,14 @@ class ExportFolder( HydrusSerialisable.SerialisableBaseNamed ):
def _GetSerialisableInfo( self ):
serialisable_file_search_context = self._file_search_context.GetSerialisableTuple()
+ serialisable_metadata_routers = self._metadata_routers.GetSerialisableTuple()
- return ( self._path, self._export_type, self._delete_from_client_after_export, serialisable_file_search_context, self._run_regularly, self._period, self._phrase, self._last_checked, self._paused, self._run_now, self._last_error )
+ return ( self._path, self._export_type, self._delete_from_client_after_export, serialisable_file_search_context, serialisable_metadata_routers, self._run_regularly, self._period, self._phrase, self._last_checked, self._paused, self._run_now, self._last_error )
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
- ( self._path, self._export_type, self._delete_from_client_after_export, serialisable_file_search_context, self._run_regularly, self._period, self._phrase, self._last_checked, self._paused, self._run_now, self._last_error ) = serialisable_info
+ ( self._path, self._export_type, self._delete_from_client_after_export, serialisable_file_search_context, serialisable_metadata_routers, self._run_regularly, self._period, self._phrase, self._last_checked, self._paused, self._run_now, self._last_error ) = serialisable_info
if self._export_type == HC.EXPORT_FOLDER_TYPE_SYNCHRONISE:
@@ -331,6 +352,7 @@ class ExportFolder( HydrusSerialisable.SerialisableBaseNamed ):
self._file_search_context = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_file_search_context )
+ self._metadata_routers = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_metadata_routers )
def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
@@ -381,6 +403,19 @@ class ExportFolder( HydrusSerialisable.SerialisableBaseNamed ):
return ( 5, new_serialisable_info )
+ if version == 5:
+
+ ( path, export_type, delete_from_client_after_export, serialisable_file_search_context, run_regularly, period, phrase, last_checked, paused, run_now, last_error ) = old_serialisable_info
+
+ metadata_routers = HydrusSerialisable.SerialisableList()
+
+ serialisable_metadata_routers = metadata_routers.GetSerialisableTuple()
+
+ new_serialisable_info = ( path, export_type, delete_from_client_after_export, serialisable_file_search_context, serialisable_metadata_routers, run_regularly, period, phrase, last_checked, paused, run_now, last_error )
+
+ return ( 6, new_serialisable_info )
+
+
def _DoExport( self ):
@@ -473,6 +508,11 @@ class ExportFolder( HydrusSerialisable.SerialisableBaseNamed ):
+ for metadata_router in self._metadata_routers:
+
+ metadata_router.Work( media_result, dest_path )
+
+
sync_paths.add( dest_path )
@@ -622,6 +662,11 @@ class ExportFolder( HydrusSerialisable.SerialisableBaseNamed ):
return self._last_error
+ def GetMetadataRouters( self ) -> typing.Collection[ ClientExportingMetadata.SingleFileMetadataRouter ]:
+
+ return self._metadata_routers
+
+
def RunNow( self ):
self._paused = False
@@ -634,82 +679,3 @@ class ExportFolder( HydrusSerialisable.SerialisableBaseNamed ):
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_EXPORT_FOLDER ] = ExportFolder
-
-class SidecarExporter( HydrusSerialisable.SerialisableBase ):
-
- SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_SIDECAR_EXPORTER
- SERIALISABLE_NAME = 'Sidecar Exporter'
- SERIALISABLE_VERSION = 1
-
- def __init__( self, service_keys_to_tag_data = None ):
-
- if service_keys_to_tag_data is None:
-
- service_keys_to_tag_data = {}
-
-
- HydrusSerialisable.SerialisableBase.__init__( self )
-
- self._service_keys_to_tag_data = service_keys_to_tag_data
-
-
- def _GetSerialisableInfo( self ):
-
- serialisable_service_keys_and_tag_data = [ ( service_key.hex(), tag_filter.GetSerialisableTuple(), tag_display_type ) for ( service_key, ( tag_filter, tag_display_type ) ) in self._service_keys_to_tag_data.items() ]
-
- return serialisable_service_keys_and_tag_data
-
-
- def _InitialiseFromSerialisableInfo( self, serialisable_info ):
-
- serialisable_service_keys_and_tag_data = serialisable_info
-
- self._service_keys_to_tag_data = { bytes.fromhex( service_key_hex ) : ( HydrusSerialisable.CreateFromSerialisableTuple( serialisable_tag_filter ), tag_display_type ) for ( service_key_hex, serialisable_tag_filter, tag_display_type ) in serialisable_service_keys_and_tag_data }
-
-
- def ExportSidecar( self, directory: str, filename: str, tags_manager: ClientMediaManagers.TagsManager ):
-
- my_service_keys = set( self._service_keys_to_tag_data.keys() )
-
- for service_key in my_service_keys:
-
- if not HG.client_controller.services_manager.ServiceExists( service_key ):
-
- del self._service_keys_to_tag_data[ service_key ]
-
-
-
- all_tags = set()
-
- for ( service_key, ( tag_filter, tag_display_type ) ) in self._service_keys_to_tag_data.items():
-
- tags = tags_manager.GetCurrent( service_key, tag_display_type )
-
- tags = tag_filter.Filter( tags )
-
- all_tags.update( tags )
-
-
- if len( all_tags ) > 0:
-
- all_tags = list( all_tags )
-
- tag_sort = ClientTagSorting.TagSort.STATICGetTextASCDefault()
-
- ClientTagSorting.SortTags( tag_sort, all_tags )
-
- txt_path = os.path.join( directory, filename + '.txt' )
-
- with open( txt_path, 'w', encoding = 'utf-8' ) as f:
-
- f.write( '\n'.join( tags ) )
-
-
-
-
- def GetTagData( self ):
-
- return dict( self._service_keys_to_tag_data )
-
-
-HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_SIDECAR_EXPORTER ] = SidecarExporter
diff --git a/hydrus/client/exporting/ClientExportingMetadata.py b/hydrus/client/exporting/ClientExportingMetadata.py
new file mode 100644
index 00000000..bd1ce37d
--- /dev/null
+++ b/hydrus/client/exporting/ClientExportingMetadata.py
@@ -0,0 +1,321 @@
+import os
+import typing
+
+from hydrus.core import HydrusConstants as HC
+from hydrus.core import HydrusData
+from hydrus.core import HydrusGlobals as HG
+from hydrus.core import HydrusSerialisable
+from hydrus.core import HydrusTags
+from hydrus.core import HydrusText
+
+from hydrus.client import ClientStrings
+from hydrus.client.media import ClientMediaResult
+from hydrus.client.metadata import ClientTags
+
+def GetSidecarPath( actual_file_path: str, suffix: str, file_extension: str ):
+
+ path_components = [ actual_file_path ]
+
+ if suffix != '':
+
+ path_components.append( suffix )
+
+
+ path_components.append( file_extension )
+
+ return '.'.join( path_components )
+
+
+class SingleFileMetadataExporterMedia( object ):
+
+ def Export( self, hash: bytes, rows: typing.Collection[ str ] ):
+
+ raise NotImplementedError()
+
+
+
+class SingleFileMetadataImporterMedia( object ):
+
+ def Import( self, media_result: ClientMediaResult.MediaResult ):
+
+ raise NotImplementedError()
+
+
+
+class SingleFileMetadataExporterSidecar( object ):
+
+ def Export( self, actual_file_path: str, rows: typing.Collection[ str ] ):
+
+ raise NotImplementedError()
+
+
+
+class SingleFileMetadataImporterSidecar( object ):
+
+ def Import( self, actual_file_path: str ):
+
+ raise NotImplementedError()
+
+
+
+# TODO: add ToString and any other stuff here so this can all show itself prettily in a listbox
+# 'I grab a .reversotags.txt sidecar and reverse the text and then send it as tags to my tags'
+
+class SingleFileMetadataRouter( HydrusSerialisable.SerialisableBase ):
+
+ SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_METADATA_SINGLE_FILE_ROUTER
+ SERIALISABLE_NAME = 'Metadata Single File Converter'
+ SERIALISABLE_VERSION = 1
+
+ def __init__( self, importers = None, string_processor = None, exporter = None ):
+
+ if importers is None:
+
+ importers = []
+
+
+ if string_processor is None:
+
+ string_processor = ClientStrings.StringProcessor()
+
+
+ if exporter is None:
+
+ exporter = SingleFileMetadataImporterExporterTXT()
+
+
+ HydrusSerialisable.SerialisableBase.__init__( self )
+
+ self._importers = HydrusSerialisable.SerialisableList( importers )
+ self._string_processor = string_processor
+ self._exporter = exporter
+
+
+ def _GetSerialisableInfo( self ):
+
+ serialisable_importers = self._importers.GetSerialisableTuple()
+ serialisable_string_processor = self._string_processor.GetSerialisableTuple()
+ serialisable_exporter = self._exporter.GetSerialisableTuple()
+
+ return ( serialisable_importers, serialisable_string_processor, serialisable_exporter )
+
+
+ def _InitialiseFromSerialisableInfo( self, serialisable_info ):
+
+ ( serialisable_importers, serialisable_string_processor, serialisable_exporter ) = serialisable_info
+
+ self._importers = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_importers )
+ self._string_processor = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_processor )
+ self._exporter = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_exporter )
+
+
+ def GetExporter( self ):
+
+ return self._exporter
+
+
+ def GetImportedSidecarTexts( self, file_path: str, and_process_them = True ):
+
+ rows = set()
+
+ for importer in self._importers:
+
+ if isinstance( importer, SingleFileMetadataImporterSidecar ):
+
+ rows.update( importer.Import( file_path ) )
+
+ else:
+
+ raise Exception( 'This convertor does not import from a sidecar!' )
+
+
+
+ rows = sorted( rows, key = HydrusTags.ConvertTagToSortable )
+
+ if and_process_them:
+
+ rows = self._string_processor.ProcessStrings( starting_strings = rows )
+
+
+ return rows
+
+
+ def GetImporters( self ):
+
+ return self._importers
+
+
+ def Work( self, media_result: ClientMediaResult.MediaResult, file_path: str ):
+
+ rows = set()
+
+ for importer in self._importers:
+
+ if isinstance( importer, SingleFileMetadataImporterSidecar ):
+
+ rows.update( importer.Import( file_path ) )
+
+ elif isinstance( importer, SingleFileMetadataImporterMedia ):
+
+ rows.update( importer.Import( media_result ) )
+
+ else:
+
+ raise Exception( 'Problem with importer object!' )
+
+
+
+ rows = sorted( rows, key = HydrusTags.ConvertTagToSortable )
+
+ rows = self._string_processor.ProcessStrings( starting_strings = rows )
+
+ if len( rows ) == 0:
+
+ return
+
+
+ if isinstance( self._exporter, SingleFileMetadataExporterSidecar ):
+
+ self._exporter.Export( file_path, rows )
+
+ elif isinstance( self._exporter, SingleFileMetadataExporterMedia ):
+
+ self._exporter.Export( media_result.GetHash(), rows )
+
+ else:
+
+ raise Exception( 'Problem with exporter object!' )
+
+
+
+
+HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_METADATA_SINGLE_FILE_ROUTER ] = SingleFileMetadataRouter
+
+class SingleFileMetadataImporterExporterMediaTags( HydrusSerialisable.SerialisableBase, SingleFileMetadataExporterMedia, SingleFileMetadataImporterMedia ):
+
+ SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_METADATA_SINGLE_FILE_IMPORTER_EXPORTER_MEDIA_TAGS
+ SERIALISABLE_NAME = 'Metadata Single File Importer Exporter Media Tags'
+ SERIALISABLE_VERSION = 1
+
+ def __init__( self, service_key = None ):
+
+ HydrusSerialisable.SerialisableBase.__init__( self )
+ SingleFileMetadataExporterMedia.__init__( self )
+ SingleFileMetadataImporterMedia.__init__( self )
+
+ self._service_key = service_key
+
+
+ def _GetSerialisableInfo( self ):
+
+ return self._service_key.hex()
+
+
+ def _InitialiseFromSerialisableInfo( self, serialisable_info ):
+
+ serialisable_service_key = serialisable_info
+
+ self._service_key = bytes.fromhex( serialisable_service_key )
+
+
+ def GetServiceKey( self ) -> bytes:
+
+ return self._service_key
+
+
+ def Import( self, media_result: ClientMediaResult.MediaResult ):
+
+ tags = media_result.GetTagsManager().GetCurrent( self._service_key, ClientTags.TAG_DISPLAY_STORAGE )
+
+ return tags
+
+
+ def Export( self, hash: bytes, rows: typing.Collection[ str ] ):
+
+ if HG.client_controller.services_manager.GetServiceType( self._service_key ) == HC.LOCAL_TAG:
+
+ add_content_action = HC.CONTENT_UPDATE_ADD
+
+ else:
+
+ add_content_action = HC.CONTENT_UPDATE_PEND
+
+
+ hashes = { hash }
+
+ content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, add_content_action, ( tag, hashes ) ) for tag in rows ]
+
+ HG.client_controller.WriteSynchronous( 'content_updates', { self._service_key : content_updates } )
+
+
+
+HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_METADATA_SINGLE_FILE_IMPORTER_EXPORTER_MEDIA_TAGS ] = SingleFileMetadataImporterExporterMediaTags
+
+class SingleFileMetadataImporterExporterTXT( HydrusSerialisable.SerialisableBase, SingleFileMetadataExporterSidecar, SingleFileMetadataImporterSidecar ):
+
+ SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_METADATA_SINGLE_FILE_IMPORTER_EXPORTER_TXT
+ SERIALISABLE_NAME = 'Metadata Single File Importer Exporter TXT'
+ SERIALISABLE_VERSION = 1
+
+ def __init__( self, suffix = None ):
+
+ HydrusSerialisable.SerialisableBase.__init__( self )
+ SingleFileMetadataExporterSidecar.__init__( self )
+ SingleFileMetadataImporterSidecar.__init__( self )
+
+ if suffix is None:
+
+ suffix = ''
+
+
+ self._suffix = suffix
+
+
+ def _GetSerialisableInfo( self ):
+
+ return self._suffix
+
+
+ def _InitialiseFromSerialisableInfo( self, serialisable_info ):
+
+ self._suffix = serialisable_info
+
+
+ def Export( self, actual_file_path: str, rows: typing.Collection[ str ] ):
+
+ path = GetSidecarPath( actual_file_path, self._suffix, 'txt' )
+
+ with open( path, 'w', encoding = 'utf-8' ) as f:
+
+ f.write( '\n'.join( rows ) )
+
+
+
+ def Import( self, actual_file_path: str ) -> typing.Collection[ str ]:
+
+ path = GetSidecarPath( actual_file_path, self._suffix, 'txt' )
+
+ if not os.path.exists( path ):
+
+ return []
+
+
+ try:
+
+ with open( path, 'r', encoding = 'utf-8' ) as f:
+
+ raw_text = f.read()
+
+
+ except Exception as e:
+
+ raise Exception( 'Could not import from {}: {}'.format( path, str( e ) ) )
+
+
+ rows = HydrusText.DeserialiseNewlinedTexts( raw_text )
+
+ return rows
+
+
+
+HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_METADATA_SINGLE_FILE_IMPORTER_EXPORTER_TXT ] = SingleFileMetadataImporterExporterTXT
diff --git a/hydrus/client/exporting/__init__.py b/hydrus/client/exporting/__init__.py
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/hydrus/client/exporting/__init__.py
@@ -0,0 +1 @@
+
diff --git a/hydrus/client/gui/ClientGUI.py b/hydrus/client/gui/ClientGUI.py
index 4cfa847f..b6840a70 100644
--- a/hydrus/client/gui/ClientGUI.py
+++ b/hydrus/client/gui/ClientGUI.py
@@ -37,12 +37,12 @@ from hydrus.core.networking import HydrusNetworking
from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
-from hydrus.client import ClientExporting
from hydrus.client import ClientLocation
from hydrus.client import ClientParsing
from hydrus.client import ClientPaths
from hydrus.client import ClientServices
from hydrus.client import ClientThreading
+from hydrus.client.exporting import ClientExportingFiles
from hydrus.client.gui import ClientGUIAsync
from hydrus.client.gui import ClientGUICharts
from hydrus.client.gui import ClientGUICore as CGC
@@ -108,6 +108,12 @@ def THREADUploadPending( service_key ):
finished_all_uploads = False
+ paused_content_types = set()
+ unauthorised_content_types = set()
+ content_types_to_request = set()
+
+ job_key = ClientThreading.JobKey( pausable = True, cancellable = True )
+
try:
service = HG.client_controller.services_manager.GetService( service_key )
@@ -127,8 +133,6 @@ def THREADUploadPending( service_key ):
- job_key = ClientThreading.JobKey( pausable = True, cancellable = True )
-
job_key.SetStatusTitle( 'uploading pending to ' + service_name )
nums_pending = HG.client_controller.Read( 'nums_pending' )
@@ -139,10 +143,6 @@ def THREADUploadPending( service_key ):
if service_type in HC.REPOSITORIES:
- paused_content_types = set()
- unauthorised_content_types = set()
- content_types_to_request = set()
-
content_types_to_count_types_and_permissions = {
HC.CONTENT_TYPE_FILES : ( ( HC.SERVICE_INFO_NUM_PENDING_FILES, HC.PERMISSION_ACTION_CREATE ), ( HC.SERVICE_INFO_NUM_PETITIONED_FILES, HC.PERMISSION_ACTION_PETITION ) ),
HC.CONTENT_TYPE_MAPPINGS : ( ( HC.SERVICE_INFO_NUM_PENDING_MAPPINGS, HC.PERMISSION_ACTION_CREATE ), ( HC.SERVICE_INFO_NUM_PETITIONED_MAPPINGS, HC.PERMISSION_ACTION_PETITION ) ),
@@ -341,6 +341,12 @@ def THREADUploadPending( service_key ):
continue
+ except Exception as e:
+
+ HydrusData.ShowText( 'File could not be pinned: {}'.format( e ) )
+
+ return
+
else:
@@ -361,8 +367,6 @@ def THREADUploadPending( service_key ):
HG.client_controller.pub( 'notify_new_pending' )
- time.sleep( 0.1 )
-
HG.client_controller.WaitUntilViewFree()
total_time_this_loop_took = HydrusData.GetNowPrecise() - time_started_this_loop
@@ -389,19 +393,6 @@ def THREADUploadPending( service_key ):
job_key.DeleteVariable( 'popup_gauge_1' )
job_key.SetVariable( 'popup_text_1', 'upload done!' )
- HydrusData.Print( job_key.ToString() )
-
- job_key.Finish()
-
- if len( content_types_to_request ) == 0:
-
- job_key.Delete()
-
- else:
-
- job_key.Delete( 5 )
-
-
except Exception as e:
r = re.search( '[a-fA-F0-9]{64}', str( e ) )
@@ -423,33 +414,47 @@ def THREADUploadPending( service_key ):
finally:
- if finished_all_uploads:
+ HydrusData.Print( job_key.ToString() )
+
+ job_key.Finish()
+
+ if len( content_types_to_request ) == 0:
- if service_type == HC.TAG_REPOSITORY:
-
- types_to_delete = (
- HC.SERVICE_INFO_NUM_PENDING_MAPPINGS,
- HC.SERVICE_INFO_NUM_PENDING_TAG_SIBLINGS,
- HC.SERVICE_INFO_NUM_PENDING_TAG_PARENTS,
- HC.SERVICE_INFO_NUM_PETITIONED_MAPPINGS,
- HC.SERVICE_INFO_NUM_PETITIONED_TAG_SIBLINGS,
- HC.SERVICE_INFO_NUM_PETITIONED_TAG_PARENTS
- )
-
- elif service_type in ( HC.FILE_REPOSITORY, HC.IPFS ):
-
- types_to_delete = (
- HC.SERVICE_INFO_NUM_PENDING_FILES,
- HC.SERVICE_INFO_NUM_PETITIONED_FILES
- )
-
+ job_key.Delete()
- HG.client_controller.Write( 'delete_service_info', service_key, types_to_delete )
+ else:
+ job_key.Delete( 5 )
+
+
+
+ if finished_all_uploads:
+
+ if service_type == HC.TAG_REPOSITORY:
+
+ types_to_delete = (
+ HC.SERVICE_INFO_NUM_PENDING_MAPPINGS,
+ HC.SERVICE_INFO_NUM_PENDING_TAG_SIBLINGS,
+ HC.SERVICE_INFO_NUM_PENDING_TAG_PARENTS,
+ HC.SERVICE_INFO_NUM_PETITIONED_MAPPINGS,
+ HC.SERVICE_INFO_NUM_PETITIONED_TAG_SIBLINGS,
+ HC.SERVICE_INFO_NUM_PETITIONED_TAG_PARENTS
+ )
+
+ elif service_type in ( HC.FILE_REPOSITORY, HC.IPFS ):
+
+ types_to_delete = (
+ HC.SERVICE_INFO_NUM_PENDING_FILES,
+ HC.SERVICE_INFO_NUM_PETITIONED_FILES
+ )
+
+
+ HG.client_controller.Write( 'delete_service_info', service_key, types_to_delete )
HG.client_controller.pub( 'notify_pending_upload_finished', service_key )
+
class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes, CAC.ApplicationCommandProcessorMixin ):
def __init__( self, controller ):
@@ -4826,7 +4831,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes, CAC.ApplicationCo
def _OpenExportFolder( self ):
- export_path = ClientExporting.GetExportPath()
+ export_path = ClientExportingFiles.GetExportPath()
if export_path is None:
diff --git a/hydrus/client/gui/ClientGUIDragDrop.py b/hydrus/client/gui/ClientGUIDragDrop.py
index 6ddc9a76..e945d44a 100644
--- a/hydrus/client/gui/ClientGUIDragDrop.py
+++ b/hydrus/client/gui/ClientGUIDragDrop.py
@@ -10,7 +10,7 @@ from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusPaths
from hydrus.core import HydrusText
-from hydrus.client import ClientExporting
+from hydrus.client.exporting import ClientExportingFiles
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import QtPorting as QP
@@ -83,12 +83,12 @@ def DoFileExportDragDrop( window, page_key, media, alt_down ):
elif discord_dnd_fix_possible and os.path.exists( temp_dir ):
- fallback_filename_terms = ClientExporting.ParseExportPhrase( '{hash}' )
+ fallback_filename_terms = ClientExportingFiles.ParseExportPhrase( '{hash}' )
try:
filename_pattern = new_options.GetString( 'discord_dnd_filename_pattern' )
- filename_terms = ClientExporting.ParseExportPhrase( filename_pattern )
+ filename_terms = ClientExportingFiles.ParseExportPhrase( filename_pattern )
if len( filename_terms ) == 0:
@@ -104,11 +104,11 @@ def DoFileExportDragDrop( window, page_key, media, alt_down ):
for ( m, original_path ) in media_and_original_paths:
- filename = ClientExporting.GenerateExportFilename( temp_dir, m, filename_terms )
+ filename = ClientExportingFiles.GenerateExportFilename( temp_dir, m, filename_terms )
if filename == HC.mime_ext_lookup[ m.GetMime() ]:
- filename = ClientExporting.GenerateExportFilename( temp_dir, m, fallback_filename_terms )
+ filename = ClientExportingFiles.GenerateExportFilename( temp_dir, m, fallback_filename_terms )
dnd_path = os.path.join( temp_dir, filename )
diff --git a/hydrus/client/gui/ClientGUIExport.py b/hydrus/client/gui/ClientGUIExport.py
index e859ba78..b89df9e5 100644
--- a/hydrus/client/gui/ClientGUIExport.py
+++ b/hydrus/client/gui/ClientGUIExport.py
@@ -11,13 +11,13 @@ from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusPaths
-from hydrus.core import HydrusTags
from hydrus.client import ClientConstants as CC
-from hydrus.client import ClientExporting
from hydrus.client import ClientLocation
from hydrus.client import ClientSearch
from hydrus.client import ClientThreading
+from hydrus.client.exporting import ClientExportingFiles
+from hydrus.client.exporting import ClientExportingMetadata
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIScrolledPanels
@@ -81,7 +81,7 @@ class EditExportFoldersPanel( ClientGUIScrolledPanels.EditPanel ):
period = 15 * 60
- export_folder = ClientExporting.ExportFolder( name, path, export_type = export_type, delete_from_client_after_export = delete_from_client_after_export, file_search_context = file_search_context, period = period, phrase = phrase )
+ export_folder = ClientExportingFiles.ExportFolder( name, path, export_type = export_type, delete_from_client_after_export = delete_from_client_after_export, file_search_context = file_search_context, period = period, phrase = phrase )
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit export folder' ) as dlg:
@@ -100,7 +100,7 @@ class EditExportFoldersPanel( ClientGUIScrolledPanels.EditPanel ):
- def _ConvertExportFolderToListCtrlTuples( self, export_folder: ClientExporting.ExportFolder ):
+ def _ConvertExportFolderToListCtrlTuples( self, export_folder: ClientExportingFiles.ExportFolder ):
( name, path, export_type, delete_from_client_after_export, file_search_context, run_regularly, period, phrase, last_checked, paused, run_now ) = export_folder.ToTuple()
@@ -206,7 +206,7 @@ class EditExportFoldersPanel( ClientGUIScrolledPanels.EditPanel ):
class EditExportFolderPanel( ClientGUIScrolledPanels.EditPanel ):
- def __init__( self, parent, export_folder: ClientExporting.ExportFolder ):
+ def __init__( self, parent, export_folder: ClientExportingFiles.ExportFolder ):
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
@@ -232,14 +232,6 @@ class EditExportFolderPanel( ClientGUIScrolledPanels.EditPanel ):
#
- self._query_box = ClientGUICommon.StaticBox( self, 'query to export' )
-
- self._page_key = b'export folders placeholder'
-
- self._tag_autocomplete = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self._query_box, self._page_key, file_search_context, allow_all_known_files = False, force_system_everything = True )
-
- #
-
self._period_box = ClientGUICommon.StaticBox( self, 'export period' )
self._period = ClientGUITime.TimeDeltaButton( self._period_box, min = 3 * 60, days = True, hours = True, minutes = True )
@@ -252,6 +244,14 @@ class EditExportFolderPanel( ClientGUIScrolledPanels.EditPanel ):
#
+ self._query_box = ClientGUICommon.StaticBox( self, 'query to export' )
+
+ self._page_key = b'export folders placeholder'
+
+ self._tag_autocomplete = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self._query_box, self._page_key, file_search_context, allow_all_known_files = False, force_system_everything = True )
+
+ #
+
self._phrase_box = ClientGUICommon.StaticBox( self, 'filenames' )
self._pattern = QW.QLineEdit( self._phrase_box )
@@ -260,6 +260,16 @@ class EditExportFolderPanel( ClientGUIScrolledPanels.EditPanel ):
#
+ self._metadata_routers_box = ClientGUICommon.StaticBox( self, 'metadata export' )
+
+ self._current_metadata_routers = list( export_folder.GetMetadataRouters() )
+
+ text = 'This will export all the files\' tags, newline separated, into .txts beside the files themselves.'
+
+ self._export_tag_txts_services_button = ClientGUICommon.BetterButton( self._metadata_routers_box, 'set tag .txt services', self._SetTxtServices )
+
+ #
+
self._name.setText( name )
self._path.SetPath( path )
@@ -332,22 +342,99 @@ If you select synchronise, be careful!'''
self._phrase_box.Add( phrase_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+ self._metadata_routers_box.Add( self._export_tag_txts_services_button, CC.FLAGS_ON_RIGHT )
+
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._path_box, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._type_box, CC.FLAGS_EXPAND_PERPENDICULAR )
- QP.AddToLayout( vbox, self._query_box, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( vbox, self._period_box, CC.FLAGS_EXPAND_PERPENDICULAR )
+ QP.AddToLayout( vbox, self._query_box, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( vbox, self._phrase_box, CC.FLAGS_EXPAND_PERPENDICULAR )
+ QP.AddToLayout( vbox, self._metadata_routers_box, CC.FLAGS_EXPAND_PERPENDICULAR )
self.widget().setLayout( vbox )
self._UpdateTypeDeleteUI()
+ self._UpdateTxtButton()
+
self._type.currentIndexChanged.connect( self._UpdateTypeDeleteUI )
self._delete_from_client_after_export.clicked.connect( self.EventDeleteFilesAfterExport )
+ def _GetCurrentTxtTagServiceKeys( self ):
+
+ current_txt_tag_service_keys = set()
+
+ if len( self._current_metadata_routers ) > 0:
+
+ metadata_router = self._current_metadata_routers[0]
+
+ for importer in metadata_router.GetImporters():
+
+ if isinstance( importer, ClientExportingMetadata.SingleFileMetadataImporterExporterMediaTags ):
+
+ service_key = importer.GetServiceKey()
+
+ current_txt_tag_service_keys.add( service_key )
+
+
+
+
+ return current_txt_tag_service_keys
+
+
+ def _SetTxtServices( self ):
+
+ # TODO: obviously replace all this, and elsewhere, with a unified metadata router edit UI panel/button
+
+ current_txt_tag_service_keys = self._GetCurrentTxtTagServiceKeys()
+
+ services_manager = HG.client_controller.services_manager
+
+ tag_services = services_manager.GetServices( HC.REAL_TAG_SERVICES )
+
+ choice_tuples = [ ( service.GetName(), service.GetServiceKey(), service.GetServiceKey() in current_txt_tag_service_keys ) for service in tag_services ]
+
+ try:
+
+ neighbouring_txt_tag_service_keys = ClientGUIDialogsQuick.SelectMultipleFromList( self, 'select tag services', choice_tuples )
+
+ except HydrusExceptions.CancelledException:
+
+ return
+
+
+ importers = [ ClientExportingMetadata.SingleFileMetadataImporterExporterMediaTags( service_key ) for service_key in neighbouring_txt_tag_service_keys ]
+
+ exporter = ClientExportingMetadata.SingleFileMetadataImporterExporterTXT()
+
+ metadata_router = ClientExportingMetadata.SingleFileMetadataRouter( importers = importers, exporter = exporter )
+
+ self._current_metadata_routers = [ metadata_router ]
+
+ self._UpdateTxtButton()
+
+
+ def _UpdateTxtButton( self ):
+
+ current_txt_tag_service_keys = self._GetCurrentTxtTagServiceKeys()
+
+ if len( current_txt_tag_service_keys ) == 0:
+
+ tt = 'No services set.'
+
+ else:
+
+ names = sorted( [ HG.client_controller.services_manager.GetName( service_key ) for service_key in current_txt_tag_service_keys ] )
+
+ tt = ', '.join( names )
+
+
+ self._export_tag_txts_services_button.setToolTip( tt )
+
+
def _UpdateTypeDeleteUI( self ):
if self._type.GetValue() == HC.EXPORT_FOLDER_TYPE_SYNCHRONISE:
@@ -415,7 +502,7 @@ If you select synchronise, be careful!'''
try:
- ClientExporting.ParseExportPhrase( phrase )
+ ClientExportingFiles.ParseExportPhrase( phrase )
except Exception as e:
@@ -428,12 +515,13 @@ If you select synchronise, be careful!'''
last_error = self._export_folder.GetLastError()
- export_folder = ClientExporting.ExportFolder(
+ export_folder = ClientExportingFiles.ExportFolder(
name,
path = path,
export_type = export_type,
delete_from_client_after_export = delete_from_client_after_export,
file_search_context = file_search_context,
+ metadata_routers = self._current_metadata_routers,
run_regularly = run_regularly,
period = period,
phrase = phrase,
@@ -446,86 +534,6 @@ If you select synchronise, be careful!'''
return export_folder
-class EditSidecarExporterPanel( ClientGUIScrolledPanels.EditPanel ):
-
- def __init__( self, parent, sidecar_exporter: ClientExporting.SidecarExporter ):
-
- ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
-
- self._service_keys_to_tag_data = dict( sidecar_exporter.GetTagData() )
-
- #
-
- # ok, I guess a multi-column list of services, then tag filter and display type options
- # open it, you make a new edit panel type
-
- # add (with test for remaining services), edit, delete
-
- #
-
- # populate that lad
-
- #
-
- vbox = QP.VBoxLayout()
-
- #QP.AddToLayout( vbox, self._tag_data_listctrl, CC.FLAGS_EXPAND_PERPENDICULAR )
-
- self.widget().setLayout( vbox )
-
-
- def GetValue( self ):
-
- sidecar_exporter = ClientExporting.SidecarExporter( service_keys_to_tag_data = self._service_keys_to_tag_data )
-
- return sidecar_exporter
-
-
-class EditSidecarExporterTagDataPanel( ClientGUIScrolledPanels.EditPanel ):
-
- def __init__( self, parent, tag_filter: HydrusTags.TagFilter, tag_display_type: int ):
-
- ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
-
- #
-
- message = 'Filter the tags you want to export here. Anything that passes this filter is exported.'
-
- self._tag_filter = ClientGUITags.TagFilterButton( self, message, tag_filter )
-
- self._tag_display_type = ClientGUICommon.BetterChoice( self )
-
- self._tag_display_type.addItem( 'with siblings and parents applied', ClientTags.TAG_DISPLAY_ACTUAL )
- self._tag_display_type.addItem( 'as the tags are actually stored', ClientTags.TAG_DISPLAY_STORAGE )
-
- #
-
- self._tag_display_type.SetValue( tag_display_type )
-
- #
-
- vbox = QP.VBoxLayout()
-
- rows = []
-
- rows.append( ( 'Tags to export: ', self._tag_filter ) )
- rows.append( ( 'Type to export: ', self._tag_display_type ) )
-
- gridbox = ClientGUICommon.WrapInGrid( self, rows )
-
- QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
-
- self.widget().setLayout( vbox )
-
-
- def GetValue( self ):
-
- tag_filter = self._tag_filter.GetValue()
- tag_display_type = self._tag_display_type.GetValue()
-
- return ( tag_filter, tag_display_type )
-
-
class ReviewExportFilesPanel( ClientGUIScrolledPanels.ReviewPanel ):
def __init__( self, parent, flat_media, do_export_and_then_quit = False ):
@@ -591,7 +599,7 @@ class ReviewExportFilesPanel( ClientGUIScrolledPanels.ReviewPanel ):
#
- export_path = ClientExporting.GetExportPath()
+ export_path = ClientExportingFiles.GetExportPath()
if export_path is not None:
@@ -763,7 +771,7 @@ class ReviewExportFilesPanel( ClientGUIScrolledPanels.ReviewPanel ):
try:
- terms = ClientExporting.ParseExportPhrase( pattern )
+ terms = ClientExportingFiles.ParseExportPhrase( pattern )
except Exception as e:
@@ -817,6 +825,11 @@ class ReviewExportFilesPanel( ClientGUIScrolledPanels.ReviewPanel ):
pauser = HydrusData.BigJobPauser()
+ importers = [ ClientExportingMetadata.SingleFileMetadataImporterExporterMediaTags( service_key ) for service_key in neighbouring_txt_tag_service_keys ]
+ exporter = ClientExportingMetadata.SingleFileMetadataImporterExporterTXT()
+
+ metadata_router = ClientExportingMetadata.SingleFileMetadataRouter( importers = importers, exporter = exporter )
+
for ( index, ( ordering_index, media, path ) ) in enumerate( to_do ):
if job_key.IsCancelled():
@@ -849,25 +862,7 @@ class ReviewExportFilesPanel( ClientGUIScrolledPanels.ReviewPanel ):
if export_tag_txts:
- tags_manager = media.GetTagsManager()
-
- tags = set()
-
- for service_key in neighbouring_txt_tag_service_keys:
-
- current_tags = tags_manager.GetCurrent( service_key, ClientTags.TAG_DISPLAY_ACTUAL )
-
- tags.update( current_tags )
-
-
- tags = sorted( tags )
-
- txt_path = path + '.txt'
-
- with open( txt_path, 'w', encoding = 'utf-8' ) as f:
-
- f.write( '\n'.join( tags ) )
-
+ metadata_router.Work( media.GetMediaResult(), path )
source_path = client_files_manager.GetFilePath( hash, mime, check_file_exists = False )
@@ -908,6 +903,8 @@ class ReviewExportFilesPanel( ClientGUIScrolledPanels.ReviewPanel ):
deletee_medias = { media for ( ordering_index, media, path ) in to_do }
+ local_file_service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )
+
chunks_of_deletee_medias = HydrusData.SplitListIntoChunks( list( deletee_medias ), 64 )
for chunk_of_deletee_medias in chunks_of_deletee_medias:
@@ -918,7 +915,7 @@ class ReviewExportFilesPanel( ClientGUIScrolledPanels.ReviewPanel ):
for media in chunk_of_deletee_medias:
- for service_key in media.GetLocationsManager().GetCurrent():
+ for service_key in media.GetLocationsManager().GetCurrent().intersection( local_file_service_keys ):
service_keys_to_hashes[ service_key ].add( media.GetHash() )
@@ -963,9 +960,9 @@ class ReviewExportFilesPanel( ClientGUIScrolledPanels.ReviewPanel ):
pattern = self._pattern.text()
- terms = ClientExporting.ParseExportPhrase( pattern )
+ terms = ClientExportingFiles.ParseExportPhrase( pattern )
- filename = ClientExporting.GenerateExportFilename( directory, media, terms, do_not_use_filenames = self._existing_filenames )
+ filename = ClientExportingFiles.GenerateExportFilename( directory, media, terms, do_not_use_filenames = self._existing_filenames )
path = os.path.join( directory, filename )
@@ -1058,7 +1055,7 @@ class ReviewExportFilesPanel( ClientGUIScrolledPanels.ReviewPanel ):
else:
- names = [ HG.client_controller.services_manager.GetName( service_key ) for service_key in self._neighbouring_txt_tag_service_keys ]
+ names = sorted( [ HG.client_controller.services_manager.GetName( service_key ) for service_key in self._neighbouring_txt_tag_service_keys ] )
tt = ', '.join( names )
diff --git a/hydrus/client/gui/ClientGUIFunctions.py b/hydrus/client/gui/ClientGUIFunctions.py
index e92dc9d6..0fc254c6 100644
--- a/hydrus/client/gui/ClientGUIFunctions.py
+++ b/hydrus/client/gui/ClientGUIFunctions.py
@@ -248,7 +248,7 @@ def GetTLWParents( widget ):
return parent_tlws
-def IsQtAncestor( child, ancestor, through_tlws = False ):
+def IsQtAncestor( child: QW.QWidget, ancestor: QW.QWidget, through_tlws = False ):
if child == ancestor:
diff --git a/hydrus/client/gui/ClientGUIRatings.py b/hydrus/client/gui/ClientGUIRatings.py
index ce29a500..8148663c 100644
--- a/hydrus/client/gui/ClientGUIRatings.py
+++ b/hydrus/client/gui/ClientGUIRatings.py
@@ -1,3 +1,5 @@
+import typing
+
from qtpy import QtCore as QC
from qtpy import QtGui as QG
from qtpy import QtWidgets as QW
@@ -184,6 +186,8 @@ class RatingLike( QW.QWidget ):
self._service_key = service_key
+ self._rating_state = ClientRatings.NULL
+
self._widget_event_filter = QP.WidgetEventFilter( self )
self._widget_event_filter.EVT_LEFT_DOWN( self.EventLeftDown )
@@ -195,12 +199,39 @@ class RatingLike( QW.QWidget ):
self._dirty = True
+ self._UpdateTooltip()
+
def _Draw( self, painter ):
raise NotImplementedError()
+ def _SetRating( self, rating: typing.Optional[ float ] ):
+
+ self._rating_state = rating
+
+ self._UpdateTooltip()
+
+
+ def _UpdateTooltip( self ):
+
+ text = HG.client_controller.services_manager.GetName( self._service_key )
+
+ try:
+
+ service = HG.client_controller.services_manager.GetService( self._service_key )
+
+ tt = '{} - {}'.format( service.GetName(), service.ConvertRatingStateToString( self._rating_state ) )
+
+ except HydrusExceptions.DataMissing:
+
+ tt = 'service missing'
+
+
+ self.setToolTip( tt )
+
+
def EventLeftDown( self, event ):
raise NotImplementedError()
@@ -218,6 +249,11 @@ class RatingLike( QW.QWidget ):
raise NotImplementedError()
+ def GetRatingState( self ):
+
+ return self._rating_state
+
+
def GetServiceKey( self ):
return self._service_key
@@ -225,13 +261,6 @@ class RatingLike( QW.QWidget ):
class RatingLikeDialog( RatingLike ):
- def __init__( self, parent, service_key ):
-
- RatingLike.__init__( self, parent, service_key )
-
- self._rating_state = ClientRatings.NULL
-
-
def _Draw( self, painter ):
painter.setBackground( QG.QBrush( QP.GetBackgroundColour( self.parentWidget() ) ) )
@@ -247,8 +276,8 @@ class RatingLikeDialog( RatingLike ):
def EventLeftDown( self, event ):
- if self._rating_state == ClientRatings.LIKE: self._rating_state = ClientRatings.NULL
- else: self._rating_state = ClientRatings.LIKE
+ if self._rating_state == ClientRatings.LIKE: self._SetRating( ClientRatings.NULL )
+ else: self._SetRating( ClientRatings.LIKE )
self._dirty = True
@@ -257,22 +286,17 @@ class RatingLikeDialog( RatingLike ):
def EventRightDown( self, event ):
- if self._rating_state == ClientRatings.DISLIKE: self._rating_state = ClientRatings.NULL
- else: self._rating_state = ClientRatings.DISLIKE
+ if self._rating_state == ClientRatings.DISLIKE: self._SetRating( ClientRatings.NULL )
+ else: self._SetRating( ClientRatings.DISLIKE )
self._dirty = True
self.update()
- def GetRatingState( self ):
-
- return self._rating_state
-
-
def SetRatingState( self, rating_state ):
- self._rating_state = rating_state
+ self._SetRating( rating_state )
self._dirty = True
@@ -303,14 +327,18 @@ class RatingNumerical( QW.QWidget ):
self.setMinimumSize( QC.QSize( my_width, 16 ) )
- self._last_rating_set = None
+ self._rating_state = ClientRatings.NULL
+ self._rating = 0.0
self._dirty = True
def _ClearRating( self ):
- self._last_rating_set = None
+ self._rating_state = ClientRatings.NULL
+ self._rating = 0.0
+
+ self._UpdateTooltip()
def _Draw( self, painter ):
@@ -318,12 +346,11 @@ class RatingNumerical( QW.QWidget ):
raise NotImplementedError()
- def _GetRatingFromClickEvent( self, event ):
+ def _GetRatingStateAndRatingFromClickEvent( self, event ):
click_pos = event.pos()
x = event.pos().x()
- y = event.pos().y()
BORDER = 1
@@ -331,6 +358,8 @@ class RatingNumerical( QW.QWidget ):
adjusted_click_pos = click_pos - QC.QPoint( BORDER, BORDER )
+ adjusted_click_pos.setY( BORDER + 1 )
+
my_active_rect = QC.QRect( QC.QPoint( 0, 0 ), my_active_size )
if my_active_rect.contains( adjusted_click_pos ):
@@ -355,22 +384,57 @@ class RatingNumerical( QW.QWidget ):
rating = self._service.ConvertStarsToRating( stars )
- return rating
+ return ( ClientRatings.SET, rating )
- return None
+ return ( ClientRatings.NULL, 0.0 )
def _SetRating( self, rating ):
- self._last_rating_set = rating
+ if rating is None:
+
+ self._ClearRating()
+
+ else:
+
+ self._rating_state = ClientRatings.SET
+ self._rating = rating
+
+ self._UpdateTooltip()
+
+
+
+ def _UpdateTooltip( self ):
+
+ text = HG.client_controller.services_manager.GetName( self._service_key )
+
+ try:
+
+ service = HG.client_controller.services_manager.GetService( self._service_key )
+
+ tt = '{} - {}'.format( service.GetName(), service.ConvertRatingStateAndRatingToString( self._rating_state, self._rating ) )
+
+ except HydrusExceptions.DataMissing:
+
+ tt = 'service missing'
+
+
+ self.setToolTip( tt )
def EventLeftDown( self, event ):
- rating = self._GetRatingFromClickEvent( event )
+ ( rating_state, rating ) = self._GetRatingStateAndRatingFromClickEvent( event )
- self._SetRating( rating )
+ if rating_state == ClientRatings.NULL:
+
+ self._ClearRating()
+
+ elif rating_state == ClientRatings.SET:
+
+ self._SetRating( rating )
+
def EventRightDown( self, event ):
@@ -387,11 +451,14 @@ class RatingNumerical( QW.QWidget ):
if event.buttons() & QC.Qt.LeftButton:
- rating = self._GetRatingFromClickEvent( event )
+ ( rating_state, rating ) = self._GetRatingStateAndRatingFromClickEvent( event )
- if rating != self._last_rating_set:
+ if rating_state != self._rating_state or rating != self._rating:
- self._SetRating( rating )
+ if rating_state == ClientRatings.SET:
+
+ self._SetRating( rating )
+
@@ -405,14 +472,6 @@ class RatingNumerical( QW.QWidget ):
class RatingNumericalDialog( RatingNumerical ):
- def __init__( self, parent, service_key ):
-
- RatingNumerical.__init__( self, parent, service_key )
-
- self._rating_state = ClientRatings.NULL
- self._rating = None
-
-
def _ClearRating( self ):
RatingNumerical._ClearRating( self )
@@ -478,4 +537,6 @@ class RatingNumericalDialog( RatingNumerical ):
self.update()
+ self._UpdateTooltip()
+
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
index 5dcdf656..d24e6b82 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
@@ -4345,7 +4345,7 @@ class RepairFileSystemPanel( ClientGUIScrolledPanels.ManagePanel ):
text += os.linesep * 2
text += 'Then hit \'apply\', and the client will launch. You should double-check all your locations under database->migrate database immediately.'
text += os.linesep * 2
- text += '2) If the locations are not available, or you do not know what they should be, or you wish to fix this outside of the program, hit \'cancel\' to gracefully cancel client boot. Feel free to contact hydrus dev for help.'
+ text += '2) If the locations are not available, or you do not know what they should be, or you wish to fix this outside of the program, hit \'cancel\' to gracefully cancel client boot. Feel free to contact hydrus dev for help. Regardless of the situation, the document at "install_dir/db/help my media files are broke.txt" may be useful background reading.'
if self._only_thumbs:
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsReview.py b/hydrus/client/gui/ClientGUIScrolledPanelsReview.py
index 316c747c..a4906ebe 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsReview.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsReview.py
@@ -1049,6 +1049,9 @@ class MigrateTagsPanel( ClientGUIScrolledPanels.ReviewPanel ):
self._migration_source_file_filter = ClientGUICommon.BetterChoice( self._migration_panel )
source_file_service_keys = list( HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) ) )
+ source_file_service_keys.extend( HG.client_controller.services_manager.GetServiceKeys( ( HC.FILE_REPOSITORY, ) ) )
+ source_file_service_keys.extend( HG.client_controller.services_manager.GetServiceKeys( ( HC.IPFS, ) ) )
+ source_file_service_keys.append( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY )
source_file_service_keys.append( CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
source_file_service_keys.append( CC.COMBINED_FILE_SERVICE_KEY )
@@ -2817,7 +2820,7 @@ class ReviewHowBonedAmI( ClientGUIScrolledPanels.ReviewPanel ):
panel_vbox = QP.VBoxLayout()
# spacing to make the weird unicode characters join up neater
- text_table_layout = QP.GridLayout( cols = 4, spacing = 0 )
+ text_table_layout = QP.GridLayout( cols = 6, spacing = 0 )
text_table_layout.setHorizontalSpacing( ClientGUIFunctions.ConvertTextToPixelWidth( self, 2 ) )
@@ -2827,42 +2830,54 @@ class ReviewHowBonedAmI( ClientGUIScrolledPanels.ReviewPanel ):
QP.AddToLayout( text_table_layout, QW.QWidget( panel ), CC.FLAGS_ON_LEFT )
QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = 'Files' ), CC.FLAGS_CENTER )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = '%' ), CC.FLAGS_CENTER )
QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = 'Size' ), CC.FLAGS_CENTER )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = '%' ), CC.FLAGS_CENTER )
QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = 'Average' ), CC.FLAGS_CENTER )
#
QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = 'Total Ever Imported:' ), CC.FLAGS_ON_LEFT )
QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = HydrusData.ToHumanInt( num_supertotal ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, QW.QWidget( panel ), CC.FLAGS_ON_LEFT )
QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = HydrusData.ToHumanBytes( size_supertotal ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, QW.QWidget( panel ), CC.FLAGS_ON_LEFT )
QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = HydrusData.ToHumanBytes( supertotal_average_filesize ) ), CC.FLAGS_ON_RIGHT )
#
QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = '\u251cAll My Files:' ), CC.FLAGS_ON_LEFT )
- QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = '{} ({})'.format( HydrusData.ToHumanInt( num_total ), ClientData.ConvertZoomToPercentage( current_num_percent ) ) ), CC.FLAGS_ON_RIGHT )
- QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = '{} ({})'.format( HydrusData.ToHumanBytes( size_total ), ClientData.ConvertZoomToPercentage( current_size_percent ) ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = HydrusData.ToHumanInt( num_total ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = ClientData.ConvertZoomToPercentage( current_num_percent ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = HydrusData.ToHumanBytes( size_total ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = ClientData.ConvertZoomToPercentage( current_size_percent ) ), CC.FLAGS_ON_RIGHT )
QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = HydrusData.ToHumanBytes( current_average_filesize ) ), CC.FLAGS_ON_RIGHT )
#
QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = '\u2502\u251cInbox:' ), CC.FLAGS_ON_LEFT )
- QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = '{} ({})'.format( HydrusData.ToHumanInt( num_inbox ), ClientData.ConvertZoomToPercentage( inbox_num_percent ) ) ), CC.FLAGS_ON_RIGHT )
- QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = '{} ({})'.format( HydrusData.ToHumanBytes( size_inbox ), ClientData.ConvertZoomToPercentage( inbox_size_percent ) ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = HydrusData.ToHumanInt( num_inbox ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = ClientData.ConvertZoomToPercentage( inbox_num_percent ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = HydrusData.ToHumanBytes( size_inbox ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = ClientData.ConvertZoomToPercentage( inbox_size_percent ) ), CC.FLAGS_ON_RIGHT )
QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = HydrusData.ToHumanBytes( inbox_average_filesize ) ), CC.FLAGS_ON_RIGHT )
#
QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = '\u2502\u2514Archive:' ), CC.FLAGS_ON_LEFT )
- QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = '{} ({})'.format( HydrusData.ToHumanInt( num_archive ), ClientData.ConvertZoomToPercentage( archive_num_percent ) ) ), CC.FLAGS_ON_RIGHT )
- QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = '{} ({})'.format( HydrusData.ToHumanBytes( size_archive ), ClientData.ConvertZoomToPercentage( archive_size_percent ) ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = HydrusData.ToHumanInt( num_archive ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = ClientData.ConvertZoomToPercentage( archive_num_percent ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = HydrusData.ToHumanBytes( size_archive ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = ClientData.ConvertZoomToPercentage( archive_size_percent ) ), CC.FLAGS_ON_RIGHT )
QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = HydrusData.ToHumanBytes( archive_average_filesize ) ), CC.FLAGS_ON_RIGHT )
#
QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = '\u2514Deleted:' ), CC.FLAGS_ON_LEFT )
- QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = '{} ({})'.format( HydrusData.ToHumanInt( num_deleted ), ClientData.ConvertZoomToPercentage( deleted_num_percent ) ) ), CC.FLAGS_ON_RIGHT )
- QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = '{} ({})'.format( HydrusData.ToHumanBytes( size_deleted ), ClientData.ConvertZoomToPercentage( deleted_size_percent ) ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = HydrusData.ToHumanInt( num_deleted ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = ClientData.ConvertZoomToPercentage( deleted_num_percent ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = HydrusData.ToHumanBytes( size_deleted ) ), CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = ClientData.ConvertZoomToPercentage( deleted_size_percent ) ), CC.FLAGS_ON_RIGHT )
QP.AddToLayout( text_table_layout, ClientGUICommon.BetterStaticText( panel, label = HydrusData.ToHumanBytes( deleted_average_filesize ) ), CC.FLAGS_ON_RIGHT )
#
@@ -3691,11 +3706,11 @@ class ReviewVacuumData( ClientGUIScrolledPanels.ReviewPanel ):
#
- info_message = '''Vacuuming is essentially an aggressive defrag of a database file. The content of the database is copied to a new file, which then has tightly packed pages and no empty 'free' pages.
+ info_message = '''Vacuuming is essentially an aggressive defrag of a database file. The entire database is copied contiguously to a new file, which then has tightly packed pages and no empty 'free' pages.
Because the new database is tightly packed, it will generally be smaller than the original file. This is currently the only way to truncate a hydrus database file.
-Vacuuming is an expensive operation. It requires lots of free space on your drive(s), hydrus cannot operate while it is going on, and it tends to run quite slow, about 1-40MB/s. The main benefit is in truncating the database files after you delete a lot of data, so I recommend you only do it on files with a lot of free space.'''
+Vacuuming is an expensive operation. It requires lots of free space on your drive(s) (including a full copy in your temp directory!), hydrus cannot operate while it is going on, and it tends to run quite slow, about 1-40MB/s. The main benefit is in truncating the database files after you delete a lot of data, so I recommend you only do it on files with a lot of free space.'''
st = ClientGUICommon.BetterStaticText( self, label = info_message )
diff --git a/hydrus/client/gui/ClientGUIStringPanels.py b/hydrus/client/gui/ClientGUIStringPanels.py
index 82e3295c..29acb868 100644
--- a/hydrus/client/gui/ClientGUIStringPanels.py
+++ b/hydrus/client/gui/ClientGUIStringPanels.py
@@ -1,3 +1,4 @@
+import os
import re
import typing
@@ -15,6 +16,7 @@ from hydrus.client import ClientStrings
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIScrolledPanels
+from hydrus.client.gui import ClientGUITags
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListBoxes
@@ -1792,6 +1794,134 @@ class EditStringSplitterPanel( ClientGUIScrolledPanels.EditPanel ):
self._UpdateControls()
+class EditStringTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
+
+ def __init__( self, parent: QW.QWidget, string_tag_filter: ClientStrings.StringTagFilter, test_data = typing.Optional[ ClientParsing.ParsingTestData ] ):
+
+ ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
+
+ message = 'This works the same as any tag filter elsewhere in the program. Note that it converts your texts to valid hydrus tags, so everything is coming out lowercase with trimmed whitespace, and invalid tags will never pass.'
+
+ tag_filter = string_tag_filter.GetTagFilter()
+
+ self._tag_filter_button = ClientGUITags.TagFilterButton( self, message, tag_filter )
+
+ self._example_string = QW.QLineEdit( self )
+
+ self._example_string_matches = ClientGUICommon.BetterStaticText( self )
+
+ #
+
+ if test_data is not None:
+
+ if len( test_data.texts ) > 0:
+
+ self._example_string.setText( list( test_data.texts )[0] )
+
+
+
+ #
+
+ rows = []
+
+ rows.append( ( 'tag filter: ', self._tag_filter_button ) )
+ rows.append( ( 'example string: ', self._example_string ) )
+
+ gridbox = ClientGUICommon.WrapInGrid( self, rows )
+
+ vbox = QP.VBoxLayout()
+
+ QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+ QP.AddToLayout( vbox, self._example_string_matches, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ self.widget().setLayout( vbox )
+
+ #
+
+ self._tag_filter_button.valueChanged.connect( self._UpdateTestResult )
+ self._example_string.textChanged.connect( self._UpdateTestResult )
+
+ self._UpdateTestResult()
+
+
+ def _GetValue( self ):
+
+ tag_filter = self._tag_filter_button.GetValue()
+
+ example_string = self._example_string.text()
+
+ string_tag_filter = ClientStrings.StringTagFilter( tag_filter = tag_filter, example_string = example_string )
+
+ return string_tag_filter
+
+
+ def _UpdateTestResult( self ):
+
+ string_tag_filter = self._GetValue()
+
+ try:
+
+ string_tag_filter.Test( self._example_string.text() )
+
+ self._example_string_matches.setText( 'Example matches ok!' )
+ self._example_string_matches.setObjectName( 'HydrusValid' )
+
+ except HydrusExceptions.StringMatchException as e:
+
+ reason = str( e )
+
+ self._example_string_matches.setText( 'Example does not match - '+reason )
+ self._example_string_matches.setObjectName( 'HydrusInvalid' )
+
+
+ self._example_string_matches.style().polish( self._example_string_matches )
+
+
+ def GetValue( self ):
+
+ string_match = self._GetValue()
+
+ try:
+
+ string_match.Test( string_match.GetExampleString() )
+
+ except HydrusExceptions.StringMatchException:
+
+ raise HydrusExceptions.VetoException( 'Please enter an example text that matches the given rules!' )
+
+
+ return string_match
+
+
+ def SetValue( self, string_match: ClientStrings.StringMatch ):
+
+ ( match_type, match_value, min_chars, max_chars, example_string ) = string_match.ToTuple()
+
+ self._match_type.SetValue( match_type )
+
+ self._match_value_flexible_input.SetValue( ClientStrings.ALPHA )
+
+ if match_type == ClientStrings.STRING_MATCH_FIXED:
+
+ self._match_value_fixed_input.setText( match_value )
+
+ elif match_type == ClientStrings.STRING_MATCH_FLEXIBLE:
+
+ self._match_value_flexible_input.SetValue( match_value )
+
+ elif match_type == ClientStrings.STRING_MATCH_REGEX:
+
+ self._match_value_regex_input.setText( match_value )
+
+
+ self._min_chars.SetValue( min_chars )
+ self._max_chars.SetValue( max_chars )
+
+ self._example_string.setText( example_string )
+
+ self._UpdateControlVisibility()
+
+
class EditStringProcessorPanel( ClientGUIScrolledPanels.EditPanel ):
def __init__( self, parent, string_processor: ClientStrings.StringProcessor, test_data: ClientParsing.ParsingTestData ):
@@ -1853,6 +1983,7 @@ class EditStringProcessorPanel( ClientGUIScrolledPanels.EditPanel ):
choice_tuples = [
( 'String Match', ClientStrings.StringMatch, 'An object that filters strings.' ),
+ ( 'String Tag Filter', ClientStrings.StringTagFilter, 'An object that filters strings using tag rules.' ),
( 'String Converter', ClientStrings.StringConverter, 'An object that converts strings from one thing to another.' ),
( 'String Splitter', ClientStrings.StringSplitter, 'An object that breaks strings into smaller strings.' ),
( 'String Sorter', ClientStrings.StringSorter, 'An object that sorts strings.' ),
@@ -1868,15 +1999,15 @@ class EditStringProcessorPanel( ClientGUIScrolledPanels.EditPanel ):
raise HydrusExceptions.VetoException()
- if string_processing_step_type == ClientStrings.StringMatch:
+ if string_processing_step_type in ( ClientStrings.StringMatch, ClientStrings.StringTagFilter ):
example_text = self._single_test_panel.GetStartingText()
- string_processing_step = ClientStrings.StringMatch( example_string = example_text )
+ string_processing_step = string_processing_step_type( example_string = example_text )
example_text = self._GetExampleTextForStringProcessingStep( string_processing_step )
- string_processing_step = ClientStrings.StringMatch( example_string = example_text )
+ string_processing_step = string_processing_step_type( example_string = example_text )
else:
@@ -1918,6 +2049,12 @@ class EditStringProcessorPanel( ClientGUIScrolledPanels.EditPanel ):
panel = EditStringSlicerPanel( dlg, string_processing_step, test_data = test_data )
+ elif isinstance( string_processing_step, ClientStrings.StringTagFilter ):
+
+ test_data = ClientParsing.ParsingTestData( {}, ( example_text, ) )
+
+ panel = EditStringTagFilterPanel( dlg, string_processing_step, test_data = test_data )
+
dlg.SetPanel( panel )
diff --git a/hydrus/client/gui/ClientGUITags.py b/hydrus/client/gui/ClientGUITags.py
index 624f1da6..c4b394d2 100644
--- a/hydrus/client/gui/ClientGUITags.py
+++ b/hydrus/client/gui/ClientGUITags.py
@@ -5194,6 +5194,8 @@ class ReviewTagDisplayMaintenancePanel( ClientGUIScrolledPanels.ReviewPanel ):
class TagFilterButton( ClientGUICommon.BetterButton ):
+ valueChanged = QC.Signal()
+
def __init__( self, parent, message, tag_filter, only_show_blacklist = False, label_prefix = None ):
ClientGUICommon.BetterButton.__init__( self, parent, 'tag filter', self._EditTagFilter )
@@ -5231,6 +5233,8 @@ class TagFilterButton( ClientGUICommon.BetterButton ):
self._UpdateLabel()
+ self.valueChanged.emit()
+
diff --git a/hydrus/client/gui/canvas/ClientGUICanvas.py b/hydrus/client/gui/canvas/ClientGUICanvas.py
index da5e8993..b7b61fb1 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvas.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvas.py
@@ -4867,7 +4867,7 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
ClientGUIMenus.AppendMenuItem( menu, 'archive', 'Archive this file, taking it out of the inbox.', self._Archive )
- elif self._current_media.HasArchive():
+ elif self._current_media.HasArchive() and self._current_media.GetLocationsManager().IsLocal():
ClientGUIMenus.AppendMenuItem( menu, 'return to inbox', 'Put this file back in the inbox.', self._Inbox )
diff --git a/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py b/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py
index 67c37ab8..f62f375b 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py
@@ -39,14 +39,9 @@ class RatingLikeCanvas( ClientGUIRatings.RatingLike ):
self._canvas_key = canvas_key
self._current_media = None
- self._rating_state = None
service = HG.client_controller.services_manager.GetService( service_key )
- name = service.GetName()
-
- self.setToolTip( name )
-
HG.client_controller.sub( self, 'ProcessContentUpdates', 'content_updates_gui' )
HG.client_controller.sub( self, 'SetDisplayMedia', 'canvas_new_display_media' )
@@ -59,8 +54,6 @@ class RatingLikeCanvas( ClientGUIRatings.RatingLike ):
if self._current_media is not None:
- self._rating_state = ClientRatings.GetLikeStateFromMedia( ( self._current_media, ), self._service_key )
-
ClientGUIRatings.DrawLike( painter, 0, 0, self._service_key, self._rating_state )
@@ -109,6 +102,8 @@ class RatingLikeCanvas( ClientGUIRatings.RatingLike ):
if HydrusData.SetsIntersect( self._hashes, hashes ):
+ self._SetRatingFromCurrentMedia()
+
self._dirty = True
self.update()
@@ -121,6 +116,20 @@ class RatingLikeCanvas( ClientGUIRatings.RatingLike ):
+ def _SetRatingFromCurrentMedia( self ):
+
+ if self._current_media is None:
+
+ rating_state = ClientRatings.NULL
+
+ else:
+
+ rating_state = ClientRatings.GetLikeStateFromMedia( ( self._current_media, ), self._service_key )
+
+
+ self._SetRating( rating_state )
+
+
def SetDisplayMedia( self, canvas_key, media ):
if canvas_key == self._canvas_key:
@@ -136,6 +145,8 @@ class RatingLikeCanvas( ClientGUIRatings.RatingLike ):
self._hashes = self._current_media.GetHashes()
+ self._SetRatingFromCurrentMedia()
+
self._dirty = True
self.update()
@@ -155,10 +166,6 @@ class RatingNumericalCanvas( ClientGUIRatings.RatingNumerical ):
self._hashes = set()
- name = self._service.GetName()
-
- self.setToolTip( name )
-
HG.client_controller.sub( self, 'ProcessContentUpdates', 'content_updates_gui' )
HG.client_controller.sub( self, 'SetDisplayMedia', 'canvas_new_display_media' )
@@ -225,6 +232,8 @@ class RatingNumericalCanvas( ClientGUIRatings.RatingNumerical ):
self.update()
+ self._UpdateTooltip()
+
return
@@ -252,6 +261,8 @@ class RatingNumericalCanvas( ClientGUIRatings.RatingNumerical ):
self.update()
+ self._UpdateTooltip()
+
diff --git a/hydrus/client/gui/lists/ClientGUIListConstants.py b/hydrus/client/gui/lists/ClientGUIListConstants.py
index 1edeadb1..63257f78 100644
--- a/hydrus/client/gui/lists/ClientGUIListConstants.py
+++ b/hydrus/client/gui/lists/ClientGUIListConstants.py
@@ -1459,7 +1459,7 @@ column_list_type_name_lookup[ COLUMN_LIST_VACUUM_DATA.ID ] = 'vacuum data'
register_column_type( COLUMN_LIST_VACUUM_DATA.ID, COLUMN_LIST_VACUUM_DATA.NAME, 'name', False, 32, True )
register_column_type( COLUMN_LIST_VACUUM_DATA.ID, COLUMN_LIST_VACUUM_DATA.SIZE, 'size', False, 8, True )
-register_column_type( COLUMN_LIST_VACUUM_DATA.ID, COLUMN_LIST_VACUUM_DATA.FREE_SPACE, 'free space', False, 14, True )
+register_column_type( COLUMN_LIST_VACUUM_DATA.ID, COLUMN_LIST_VACUUM_DATA.FREE_SPACE, 'internal free space', False, 14, True )
register_column_type( COLUMN_LIST_VACUUM_DATA.ID, COLUMN_LIST_VACUUM_DATA.LAST_VACUUM, 'last vacuum', False, 32, True )
register_column_type( COLUMN_LIST_VACUUM_DATA.ID, COLUMN_LIST_VACUUM_DATA.CAN_VACUUM, 'can vacuum?', False, 64, True )
register_column_type( COLUMN_LIST_VACUUM_DATA.ID, COLUMN_LIST_VACUUM_DATA.VACUUM_TIME_ESTIMATE, 'vacuum time estimate', False, 48, True )
diff --git a/hydrus/client/gui/pages/ClientGUIResults.py b/hydrus/client/gui/pages/ClientGUIResults.py
index eb7ed649..3d132fcd 100644
--- a/hydrus/client/gui/pages/ClientGUIResults.py
+++ b/hydrus/client/gui/pages/ClientGUIResults.py
@@ -1183,7 +1183,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea, CAC.Applicatio
def _Inbox( self ):
- hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_ARCHIVE )
+ hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_ARCHIVE, is_in_file_service_key = CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
if len( hashes ) > 0:
@@ -3749,7 +3749,7 @@ class MediaPanelThumbnails( MediaPanel ):
selection_has_local_file_domain = True in ( locations_manager.IsLocal() and not locations_manager.IsTrashed() for locations_manager in selected_locations_managers )
selection_has_trash = True in ( locations_manager.IsTrashed() for locations_manager in selected_locations_managers )
selection_has_inbox = True in ( media.HasInbox() for media in self._selected_media )
- selection_has_archive = True in ( media.HasArchive() for media in self._selected_media )
+ selection_has_archive = True in ( media.HasArchive() and media.GetLocationsManager().IsLocal() for media in self._selected_media )
all_file_domains = HydrusData.MassUnion( locations_manager.GetCurrent() for locations_manager in all_locations_managers )
all_specific_file_domains = all_file_domains.difference( { CC.COMBINED_FILE_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY } )
diff --git a/hydrus/client/gui/search/ClientGUIACDropdown.py b/hydrus/client/gui/search/ClientGUIACDropdown.py
index beb88173..e1dfbba9 100644
--- a/hydrus/client/gui/search/ClientGUIACDropdown.py
+++ b/hydrus/client/gui/search/ClientGUIACDropdown.py
@@ -17,6 +17,7 @@ from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientLocation
from hydrus.client import ClientSearch
+from hydrus.client import ClientSearchParseSystemPredicates
from hydrus.client import ClientThreading
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIFunctions
@@ -1408,32 +1409,6 @@ class AutoCompleteDropdownTags( AutoCompleteDropdown ):
HG.client_controller.sub( self, 'NotifyNewServices', 'notify_new_services' )
- def _SetTagService( self, tag_service_key ):
-
- if not HG.client_controller.services_manager.ServiceExists( tag_service_key ):
-
- tag_service_key = CC.COMBINED_TAG_SERVICE_KEY
-
-
- if tag_service_key == CC.COMBINED_TAG_SERVICE_KEY and self._location_context_button.GetValue().IsAllKnownFiles():
-
- default_location_context = HG.client_controller.new_options.GetDefaultLocalLocationContext()
-
- self._SetLocationContext( default_location_context )
-
-
- self._tag_service_key = tag_service_key
-
- self._search_results_list.SetTagServiceKey( self._tag_service_key )
- self._favourites_list.SetTagServiceKey( self._tag_service_key )
-
- self._UpdateTagServiceLabel()
-
- self.tagServiceChanged.emit( self._tag_service_key )
-
- self._SetListDirty()
-
-
def _GetCurrentBroadcastTextPredicate( self ) -> typing.Optional[ ClientSearch.Predicate ]:
raise NotImplementedError()
@@ -1475,6 +1450,11 @@ class AutoCompleteDropdownTags( AutoCompleteDropdown ):
location_context.FixMissingServices( HG.client_controller.services_manager.FilterValidServiceKeys )
+ if location_context == self._location_context_button.GetValue():
+
+ return
+
+
if location_context.IsAllKnownFiles() and self._tag_service_key == CC.COMBINED_TAG_SERVICE_KEY:
local_tag_services = HG.client_controller.services_manager.GetServices( ( HC.LOCAL_TAG, ) )
@@ -1494,6 +1474,39 @@ class AutoCompleteDropdownTags( AutoCompleteDropdown ):
self._current_list_parsed_autocomplete_text = parsed_autocomplete_text
+ def _SetTagService( self, tag_service_key ):
+
+ if not HG.client_controller.services_manager.ServiceExists( tag_service_key ):
+
+ tag_service_key = CC.COMBINED_TAG_SERVICE_KEY
+
+
+ if tag_service_key == CC.COMBINED_TAG_SERVICE_KEY and self._location_context_button.GetValue().IsAllKnownFiles():
+
+ default_location_context = HG.client_controller.new_options.GetDefaultLocalLocationContext()
+
+ self._SetLocationContext( default_location_context )
+
+
+ if tag_service_key == self._tag_service_key:
+
+ return False
+
+
+ self._tag_service_key = tag_service_key
+
+ self._search_results_list.SetTagServiceKey( self._tag_service_key )
+ self._favourites_list.SetTagServiceKey( self._tag_service_key )
+
+ self._UpdateTagServiceLabel()
+
+ self.tagServiceChanged.emit( self._tag_service_key )
+
+ self._SetListDirty()
+
+ return True
+
+
def _UpdateTagServiceLabel( self ):
tag_service = HG.client_controller.services_manager.GetService( self._tag_service_key )
@@ -2015,11 +2028,14 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
def _SetTagService( self, tag_service_key ):
- AutoCompleteDropdownTags._SetTagService( self, tag_service_key )
+ it_changed = AutoCompleteDropdownTags._SetTagService( self, tag_service_key )
- self._file_search_context.SetTagServiceKey( tag_service_key )
-
- self._SignalNewSearchState()
+ if it_changed:
+
+ self._file_search_context.SetTagServiceKey( tag_service_key )
+
+ self._SignalNewSearchState()
+
def _SetupTopListBox( self ):
@@ -2758,7 +2774,7 @@ class EditAdvancedORPredicates( ClientGUIScrolledPanels.EditPanel ):
summary = 'Enter a complicated tag search here as text, such as \'( blue eyes and blonde hair ) or ( green eyes and red hair )\', and this should turn it into hydrus-compatible search predicates.'
summary += os.linesep * 2
- summary += 'Accepted operators: not (!, -), and (&&), or (||), implies (=>), xor, xnor (iff, <=>), nand, nor.'
+ summary += 'Accepted operators: not (!, -), and (&&), or (||), implies (=>), xor, xnor (iff, <=>), nand, nor. Many system predicates are also supported.'
summary += os.linesep * 2
summary += 'Parentheses work the usual way. \\ can be used to escape characters (e.g. to search for tags including parentheses)'
@@ -2774,6 +2790,8 @@ class EditAdvancedORPredicates( ClientGUIScrolledPanels.EditPanel ):
self._input_text.textChanged.connect( self.EventUpdateText )
+ ClientGUIFunctions.SetFocusLater( self._input_text )
+
def _UpdateText( self ):
@@ -2794,10 +2812,20 @@ class EditAdvancedORPredicates( ClientGUIScrolledPanels.EditPanel ):
for s in result:
- row_preds = []
+ tag_preds = []
+
+ system_preds = []
+ system_pred_strings = []
for tag_string in s:
+ if tag_string.startswith( 'system:' ):
+
+ system_pred_strings.append( tag_string )
+
+ continue
+
+
if tag_string.startswith( '-' ):
inclusive = False
@@ -2809,6 +2837,17 @@ class EditAdvancedORPredicates( ClientGUIScrolledPanels.EditPanel ):
inclusive = True
+ try:
+
+ tag_string = HydrusTags.CleanTag( tag_string )
+
+ HydrusTags.CheckTagNotEmpty( tag_string )
+
+ except Exception as e:
+
+ raise ValueError( str( e ) )
+
+
if '*' in tag_string:
( namespace, subtag ) = HydrusTags.SplitTag( tag_string )
@@ -2827,9 +2866,23 @@ class EditAdvancedORPredicates( ClientGUIScrolledPanels.EditPanel ):
row_pred = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, value = tag_string, inclusive = inclusive )
- row_preds.append( row_pred )
+ tag_preds.append( row_pred )
+ if len( system_pred_strings ) > 0:
+
+ try:
+
+ system_preds = ClientSearchParseSystemPredicates.ParseSystemPredicateStringsToPredicates( system_pred_strings )
+
+ except Exception as e:
+
+ raise ValueError( str( e ) )
+
+
+
+ row_preds = tag_preds + system_preds
+
if len( row_preds ) == 1:
self._current_predicates.append( row_preds[0] )
diff --git a/hydrus/client/gui/search/ClientGUILocation.py b/hydrus/client/gui/search/ClientGUILocation.py
index 3c1918a9..b0b59d58 100644
--- a/hydrus/client/gui/search/ClientGUILocation.py
+++ b/hydrus/client/gui/search/ClientGUILocation.py
@@ -127,7 +127,7 @@ class LocationSearchContextButton( ClientGUICommon.BetterButton ):
def __init__( self, parent: QW.QWidget, location_context: ClientLocation.LocationContext ):
- self._location_context = location_context
+ self._location_context = ClientLocation.LocationContext()
ClientGUICommon.BetterButton.__init__( self, parent, 'initialising', self._EditLocation )
@@ -216,6 +216,11 @@ class LocationSearchContextButton( ClientGUICommon.BetterButton ):
location_context.FixMissingServices( HG.client_controller.services_manager.FilterValidServiceKeys )
+ if location_context == self._location_context:
+
+ return
+
+
self._location_context = location_context
self.setText( self._location_context.ToString( HG.client_controller.services_manager.GetName ) )
diff --git a/hydrus/client/gui/services/ClientGUIClientsideServices.py b/hydrus/client/gui/services/ClientGUIClientsideServices.py
index d6cc9fdd..8cf5b000 100644
--- a/hydrus/client/gui/services/ClientGUIClientsideServices.py
+++ b/hydrus/client/gui/services/ClientGUIClientsideServices.py
@@ -1517,6 +1517,8 @@ class ReviewServicePanel( QW.QWidget ):
self._id_button.setFixedWidth( width )
+ self._service_key_button = ClientGUICommon.BetterButton( self, 'copy service key', HG.client_controller.pub, 'clipboard', 'text', service.GetServiceKey().hex() )
+
self._refresh_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().refresh, self._RefreshButton )
service_type = self._service.GetServiceType()
@@ -1585,6 +1587,7 @@ class ReviewServicePanel( QW.QWidget ):
if not HG.client_controller.new_options.GetBoolean( 'advanced_mode' ):
self._id_button.hide()
+ self._service_key_button.hide()
vbox = QP.VBoxLayout()
@@ -1592,6 +1595,7 @@ class ReviewServicePanel( QW.QWidget ):
hbox = QP.HBoxLayout( margin = 0 )
QP.AddToLayout( hbox, self._id_button, CC.FLAGS_CENTER )
+ QP.AddToLayout( hbox, self._service_key_button, CC.FLAGS_CENTER )
QP.AddToLayout( hbox, self._refresh_button, CC.FLAGS_CENTER )
QP.AddToLayout( vbox, hbox, CC.FLAGS_ON_RIGHT )
diff --git a/hydrus/client/importing/options/TagImportOptions.py b/hydrus/client/importing/options/TagImportOptions.py
index b6da73f4..7312374b 100644
--- a/hydrus/client/importing/options/TagImportOptions.py
+++ b/hydrus/client/importing/options/TagImportOptions.py
@@ -12,6 +12,7 @@ from hydrus.core import HydrusText
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
+from hydrus.client.exporting import ClientExportingMetadata
from hydrus.client.importing.options import ClientImportOptions
from hydrus.client.media import ClientMediaResult
from hydrus.client.metadata import ClientTags
@@ -105,43 +106,27 @@ class FilenameTaggingOptions( HydrusSerialisable.SerialisableBase ):
if self._load_from_neighbouring_txt_files:
- txt_path = path + '.txt'
+ # TODO: this needs more work, making an actual Router object with an Exporter, grinding things towards flexible conversion with different types and actually firing off content updates vs 'get example tags for UI'
+ # I'm pretty sure we could also make a 'FilenameImporter' and pipe all the following gubbins into the same system lad
- if os.path.exists( txt_path ):
+ importer = ClientExportingMetadata.SingleFileMetadataImporterExporterTXT()
+
+ try:
- try:
+ txt_tags = importer.Import( path )
+
+ if True in ( len( txt_tag ) > 1024 for txt_tag in txt_tags ):
- with open( txt_path, 'r', encoding = 'utf-8' ) as f:
-
- txt_tags_string = f.read()
-
-
- except:
-
- HydrusData.ShowText( 'Could not parse the tags from ' + txt_path + '!' )
-
- tags.add( '___had problem reading .txt file--is it not in utf-8?' )
+ raise Exception( 'Tags were too long--I think this was not a regular text file!' )
- try:
-
- txt_tags = [ tag for tag in HydrusText.DeserialiseNewlinedTexts( txt_tags_string ) ]
-
- if True in ( len( txt_tag ) > 1024 for txt_tag in txt_tags ):
-
- HydrusData.ShowText( 'Tags were too long--I think this was not a regular text file!' )
-
- raise Exception()
-
-
- tags.update( txt_tags )
-
- except:
-
- HydrusData.ShowText( 'Could not parse the tags from ' + txt_path + '!' )
-
- tags.add( '___had problem parsing .txt file' )
-
+ tags.update( txt_tags )
+
+ except Exception as e:
+
+ HydrusData.ShowText( 'Problem getting tags from a txt sidecar! {}'.format( e ) )
+
+ tags.add( '___had problem parsing .txt file' )
diff --git a/hydrus/client/metadata/ClientTagsHandling.py b/hydrus/client/metadata/ClientTagsHandling.py
index d7800da3..c580ba8c 100644
--- a/hydrus/client/metadata/ClientTagsHandling.py
+++ b/hydrus/client/metadata/ClientTagsHandling.py
@@ -346,17 +346,18 @@ class TagDisplayMaintenanceManager( object ):
else:
- if actual_work_time > expected_work_time * 5:
+ if actual_work_time > expected_work_time * 10:
# if suddenly a job blats the user for ten seconds or _ten minutes_ during normal time, we are going to take a big break
-
- return 1800
+ work_rest_ratio = 30
else:
- return 30
+ work_rest_ratio = 9
+ return max( actual_work_time, expected_work_time ) * work_rest_ratio
+
def _GetServiceKeyToWorkOn( self ):
@@ -402,7 +403,7 @@ class TagDisplayMaintenanceManager( object ):
else:
- return 0.5
+ return 0.1
diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py
index 0677e94c..e4b4cd62 100644
--- a/hydrus/core/HydrusConstants.py
+++ b/hydrus/core/HydrusConstants.py
@@ -80,7 +80,7 @@ options = {}
# Misc
NETWORK_VERSION = 20
-SOFTWARE_VERSION = 490
+SOFTWARE_VERSION = 491
CLIENT_API_VERSION = 31
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
diff --git a/hydrus/core/HydrusDBBase.py b/hydrus/core/HydrusDBBase.py
index 62f7b67d..9b74a0f7 100644
--- a/hydrus/core/HydrusDBBase.py
+++ b/hydrus/core/HydrusDBBase.py
@@ -9,22 +9,22 @@ from hydrus.core import HydrusTemp
def CheckHasSpaceForDBTransaction( db_dir, num_bytes ):
+ space_needed = int( num_bytes * 1.1 )
+
if HG.no_db_temp_files:
- space_needed = int( num_bytes * 1.1 )
-
approx_available_memory = psutil.virtual_memory().available * 4 / 5
if approx_available_memory < num_bytes:
- raise Exception( 'I believe you need about ' + HydrusData.ToHumanBytes( space_needed ) + ' available memory, since you are running in no_db_temp_files mode, but you only seem to have ' + HydrusData.ToHumanBytes( approx_available_memory ) + '.' )
+ raise Exception( 'I believe you need about {} available memory, since you are running in no_db_temp_files mode, but you only seem to have {}.'.format( HydrusData.ToHumanBytes( space_needed ), HydrusData.ToHumanBytes( approx_available_memory ) ) )
db_disk_free_space = HydrusPaths.GetFreeSpace( db_dir )
if db_disk_free_space < space_needed:
- raise Exception( 'I believe you need about ' + HydrusData.ToHumanBytes( space_needed ) + ' on your db\'s disk partition, but you only seem to have ' + HydrusData.ToHumanBytes( db_disk_free_space ) + '.' )
+ raise Exception( 'I believe you need about {} on your db\'s disk partition, but you only seem to have {}.'.format( HydrusData.ToHumanBytes( space_needed ), HydrusData.ToHumanBytes( db_disk_free_space ) ) )
else:
@@ -37,27 +37,36 @@ def CheckHasSpaceForDBTransaction( db_dir, num_bytes ):
if temp_and_db_on_same_device:
- space_needed = int( num_bytes * 2.2 )
+ space_needed *= 2
if temp_disk_free_space < space_needed:
- raise Exception( 'I believe you need about ' + HydrusData.ToHumanBytes( space_needed ) + ' on your db\'s disk partition, which I think also holds your temporary path, but you only seem to have ' + HydrusData.ToHumanBytes( temp_disk_free_space ) + '.' )
+ raise Exception( 'I believe you need about {} on your db\'s disk partition, which I think also holds your temporary path, but you only seem to have {}.'.format( HydrusData.ToHumanBytes( space_needed ), HydrusData.ToHumanBytes( temp_disk_free_space ) ) )
else:
- space_needed = int( num_bytes * 1.1 )
-
if temp_disk_free_space < space_needed:
- raise Exception( 'I believe you need about ' + HydrusData.ToHumanBytes( space_needed ) + ' on your temporary path\'s disk partition, which I think is ' + temp_dir + ', but you only seem to have ' + HydrusData.ToHumanBytes( temp_disk_free_space ) + '.' )
+ message = 'I believe you need about {} on your temporary path\'s disk partition, which I think is {}, but you only seem to have {}.'.format( HydrusData.ToHumanBytes( space_needed ), temp_dir, HydrusData.ToHumanBytes( temp_disk_free_space ) )
+
+ if HydrusPaths.GetTotalSpace( temp_dir ) <= 4 * 1024 * 1024 * 1024:
+
+ message += ' I think you might be using a ramdisk! You may want to instead launch hydrus with a different temp dir. Please check the "launch arguments" section of the help.'
+
+ else:
+
+ message += ' Please note that temporary paths can be complicated, and if you have a ramdisk or OS settings limiting how large it can get, or you simply cannot free space on your system drive, you may want to instead launch hydrus with a different temp directory. Please check the "launch arguments" section of the help.'
+
+
+ raise Exception( message )
db_disk_free_space = HydrusPaths.GetFreeSpace( db_dir )
if db_disk_free_space < space_needed:
- raise Exception( 'I believe you need about ' + HydrusData.ToHumanBytes( space_needed ) + ' on your db\'s disk partition, but you only seem to have ' + HydrusData.ToHumanBytes( db_disk_free_space ) + '.' )
+ raise Exception( 'I believe you need about {} on your db\'s disk partition, but you only seem to have {}.'.format( HydrusData.ToHumanBytes( space_needed ), HydrusData.ToHumanBytes( db_disk_free_space ) ) )
diff --git a/hydrus/core/HydrusDBModule.py b/hydrus/core/HydrusDBModule.py
index 034d2780..3ebbf42e 100644
--- a/hydrus/core/HydrusDBModule.py
+++ b/hydrus/core/HydrusDBModule.py
@@ -29,6 +29,45 @@ class HydrusDBModule( HydrusDBBase.DBBase ):
return tuples
+ def _CreateTable( self, create_query_without_name: str, table_name: str ):
+
+ if 'fts4(' in create_query_without_name.lower():
+
+ # when we want to repair a missing fts4 table, the damaged old virtual table sometimes still has some sub-tables hanging around, which breaks the new create
+ # so, let's route all table creation through here and check for and clear any subtables beforehand!
+
+ if '.' in table_name:
+
+ ( schema, raw_table_name ) = table_name.split( '.', 1 )
+
+ sqlite_master_table = '{}.sqlite_master'.format( schema )
+
+ else:
+
+ raw_table_name = table_name
+ sqlite_master_table = 'sqlite_master'
+
+
+ # little test here to make sure we stay idempotent if the primary table actually already exists--don't want to delete things that are actually good!
+ if self._Execute( 'SELECT 1 FROM {} WHERE name = ?;'.format( sqlite_master_table ), ( raw_table_name, ) ).fetchone() is None:
+
+ possible_suffixes = [ '_content', '_docsize', '_segdir', '_segments', '_stat' ]
+
+ possible_subtable_names = [ '{}{}'.format( raw_table_name, suffix ) for suffix in possible_suffixes ]
+
+ for possible_subtable_name in possible_subtable_names:
+
+ if self._Execute( 'SELECT 1 FROM {} WHERE name = ?;'.format( sqlite_master_table ), ( possible_subtable_name, ) ).fetchone() is not None:
+
+ self._Execute( 'DROP TABLE {};'.format( possible_subtable_name ) )
+
+
+
+
+
+ self._Execute( create_query_without_name.format( table_name ) )
+
+
def _GetCriticalTableNames( self ) -> typing.Collection[ str ]:
return set()
@@ -119,7 +158,7 @@ class HydrusDBModule( HydrusDBBase.DBBase ):
for ( table_name, ( create_query_without_name, version_added ) ) in table_generation_dict.items():
- self._Execute( create_query_without_name.format( table_name ) )
+ self._CreateTable( create_query_without_name, table_name )
@@ -217,7 +256,7 @@ class HydrusDBModule( HydrusDBBase.DBBase ):
for ( table_name, create_query_without_name ) in missing_table_rows:
- self._Execute( create_query_without_name.format( table_name ) )
+ self._CreateTable( create_query_without_name, table_name )
cursor_transaction_wrapper.CommitAndBegin()
@@ -259,7 +298,7 @@ class HydrusDBModule( HydrusDBBase.DBBase ):
for ( table_name, create_query_without_name ) in missing_table_rows:
- self._Execute( create_query_without_name.format( table_name ) )
+ self._CreateTable( create_query_without_name, table_name )
cursor_transaction_wrapper.CommitAndBegin()
diff --git a/hydrus/core/HydrusData.py b/hydrus/core/HydrusData.py
index b360d3af..479dfbb0 100644
--- a/hydrus/core/HydrusData.py
+++ b/hydrus/core/HydrusData.py
@@ -1710,7 +1710,7 @@ def BaseToHumanBytes( size, sig_figs = 3 ):
pass
- return '{}{}B'.format( d, suffix )
+ return '{} {}B'.format( d, suffix )
ToHumanBytes = BaseToHumanBytes
diff --git a/hydrus/core/HydrusPaths.py b/hydrus/core/HydrusPaths.py
index 4e0ba94f..0614ee1b 100644
--- a/hydrus/core/HydrusPaths.py
+++ b/hydrus/core/HydrusPaths.py
@@ -223,45 +223,34 @@ def DirectoryIsWriteable( path ):
- return os.access( path, os.W_OK | os.X_OK )
-
- # old crazy method:
- '''
- # testing access bits on directories to see if we can make new files is multiplatform hellmode
- # so, just try it and see what happens
-
- temp_path = os.path.join( path, 'hydrus_temp_test_top_jej' )
-
- if os.path.exists( temp_path ):
+ if not os.access( path, os.W_OK | os.X_OK ):
- try:
-
- os.unlink( temp_path )
-
- except:
-
- return False
-
+ return False
+ # we'll actually do a file, since Program Files passes the above test lmaoooo
+
try:
- # using tempfile.TemporaryFile actually loops on PermissionError from Windows lmaaaooooo, thinking this is an already existing file
+ # also, using tempfile.TemporaryFile actually loops on PermissionError from Windows lmaaaooooo, thinking this is an already existing file
# so, just do it manually!
- f = open( temp_path, 'wb' )
+ test_path = os.path.join( path, 'hydrus_permission_test' )
- f.close()
+ with open( test_path, 'wb' ) as f:
+
+ f.write( b'If this file still exists, this directory can be written to but not deleted from.' )
+
- os.unlink( temp_path )
-
- return True
+ os.unlink( test_path )
except:
return False
- '''
+
+ return True
+
def FileisWriteable( path: str ):
return os.access( path, os.W_OK )
@@ -340,6 +329,12 @@ def GetFreeSpace( path ):
return disk_usage.free
+def GetTotalSpace( path ):
+
+ disk_usage = psutil.disk_usage( path )
+
+ return disk_usage.total
+
def LaunchDirectory( path ):
def do_it():
diff --git a/hydrus/core/HydrusSerialisable.py b/hydrus/core/HydrusSerialisable.py
index 5a3a847e..c3986579 100644
--- a/hydrus/core/HydrusSerialisable.py
+++ b/hydrus/core/HydrusSerialisable.py
@@ -115,6 +115,10 @@ SERIALISABLE_TYPE_GUI_SESSION_PAGE_DATA = 105
SERIALISABLE_TYPE_GUI_SESSION_CONTAINER_PAGE_NOTEBOOK = 106
SERIALISABLE_TYPE_GUI_SESSION_CONTAINER_PAGE_SINGLE = 107
SERIALISABLE_TYPE_PRESENTATION_IMPORT_OPTIONS = 108
+SERIALISABLE_TYPE_METADATA_SINGLE_FILE_ROUTER = 109
+SERIALISABLE_TYPE_METADATA_SINGLE_FILE_IMPORTER_EXPORTER_TXT = 110
+SERIALISABLE_TYPE_METADATA_SINGLE_FILE_IMPORTER_EXPORTER_MEDIA_TAGS = 111
+SERIALISABLE_TYPE_STRING_TAG_FILTER = 112
SERIALISABLE_TYPES_TO_OBJECT_TYPES = {}
diff --git a/hydrus/hydrus_client.py b/hydrus/hydrus_client.py
index 1a307b5b..d7419feb 100644
--- a/hydrus/hydrus_client.py
+++ b/hydrus/hydrus_client.py
@@ -67,7 +67,11 @@ try:
if not HydrusPaths.DirectoryIsWriteable( db_dir ):
- raise Exception( 'The given db path "{}" is not a writeable-to!'.format( db_dir ) )
+ message = 'The given db path "{}" is not a writeable-to!'.format( db_dir )
+
+ db_dir = HC.USERPATH_DB_DIR
+
+ raise Exception( message )
try:
@@ -76,12 +80,20 @@ try:
except:
- raise Exception( 'Could not ensure db path "{}" exists! Check the location is correct and that you have permission to write to it!'.format( db_dir ) )
+ message = 'Could not ensure db path "{}" exists! Check the location is correct and that you have permission to write to it!'.format( db_dir )
+
+ db_dir = HC.USERPATH_DB_DIR
+
+ raise Exception( message )
if not os.path.isdir( db_dir ):
- raise Exception( 'The given db path "{}" is not a directory!'.format( db_dir ) )
+ message = 'The given db path "{}" is not a directory!'.format( db_dir )
+
+ db_dir = HC.USERPATH_DB_DIR
+
+ raise Exception( message )
HG.db_journal_mode = result.db_journal_mode
diff --git a/hydrus/hydrus_server.py b/hydrus/hydrus_server.py
index a9baa716..06c80054 100644
--- a/hydrus/hydrus_server.py
+++ b/hydrus/hydrus_server.py
@@ -78,7 +78,11 @@ try:
if not HydrusPaths.DirectoryIsWriteable( db_dir ):
- raise Exception( 'The given db path "{}" is not a writeable-to!'.format( db_dir ) )
+ message = 'The given db path "{}" is not a writeable-to!'.format( db_dir )
+
+ db_dir = HC.USERPATH_DB_DIR
+
+ raise Exception( message )
try:
@@ -87,12 +91,20 @@ try:
except:
- raise Exception( 'Could not ensure db path "{}" exists! Check the location is correct and that you have permission to write to it!'.format( db_dir ) )
+ message = 'Could not ensure db path "{}" exists! Check the location is correct and that you have permission to write to it!'.format( db_dir )
+
+ db_dir = HC.USERPATH_DB_DIR
+
+ raise Exception( message )
if not os.path.isdir( db_dir ):
- raise Exception( 'The given db path "{}" is not a directory!'.format( db_dir ) )
+ message = 'The given db path "{}" is not a directory!'.format( db_dir )
+
+ db_dir = HC.USERPATH_DB_DIR
+
+ raise Exception( message )
HG.db_journal_mode = result.db_journal_mode
diff --git a/hydrus/test/TestClientDB.py b/hydrus/test/TestClientDB.py
index 20e8096d..f3ea3a84 100644
--- a/hydrus/test/TestClientDB.py
+++ b/hydrus/test/TestClientDB.py
@@ -12,11 +12,11 @@ from hydrus.core.networking import HydrusNetwork
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientDefaults
-from hydrus.client import ClientExporting
from hydrus.client import ClientLocation
from hydrus.client import ClientSearch
from hydrus.client import ClientServices
from hydrus.client.db import ClientDB
+from hydrus.client.exporting import ClientExportingFiles
from hydrus.client.gui.pages import ClientGUIManagement
from hydrus.client.gui.pages import ClientGUIPages
from hydrus.client.gui.pages import ClientGUISession
@@ -251,7 +251,7 @@ class TestClientDB( unittest.TestCase ):
file_search_context = ClientSearch.FileSearchContext( location_context = location_context, tag_search_context = tag_search_context, predicates = [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, 'test' ) ] )
- export_folder = ClientExporting.ExportFolder( 'test path', export_type = HC.EXPORT_FOLDER_TYPE_REGULAR, delete_from_client_after_export = False, file_search_context = file_search_context, period = 3600, phrase = '{hash}' )
+ export_folder = ClientExportingFiles.ExportFolder( 'test path', export_type = HC.EXPORT_FOLDER_TYPE_REGULAR, delete_from_client_after_export = False, file_search_context = file_search_context, period = 3600, phrase = '{hash}' )
self._write( 'serialisable', export_folder )
diff --git a/hydrus/test/TestHydrusNetworking.py b/hydrus/test/TestHydrusNetworking.py
index fe82f4ca..9bb1d635 100644
--- a/hydrus/test/TestHydrusNetworking.py
+++ b/hydrus/test/TestHydrusNetworking.py
@@ -444,7 +444,7 @@ class TestBandwidthTracker( unittest.TestCase ):
bandwidth_tracker.ReportDataUsed( 1024 )
bandwidth_tracker.ReportRequestUsed()
- self.assertEqual( bandwidth_tracker.GetCurrentMonthSummary(), 'used 1KB in 1 requests this month' )
+ self.assertEqual( bandwidth_tracker.GetCurrentMonthSummary(), 'used 1 KB in 1 requests this month' )
self.assertEqual( bandwidth_tracker.GetUsage( HC.BANDWIDTH_TYPE_DATA, 0 ), 0 )
self.assertEqual( bandwidth_tracker.GetUsage( HC.BANDWIDTH_TYPE_REQUESTS, 0 ), 0 )
@@ -497,7 +497,7 @@ class TestBandwidthTracker( unittest.TestCase ):
bandwidth_tracker.ReportDataUsed( 32 )
bandwidth_tracker.ReportRequestUsed()
- self.assertEqual( bandwidth_tracker.GetCurrentMonthSummary(), 'used 1.06KB in 3 requests this month' )
+ self.assertEqual( bandwidth_tracker.GetCurrentMonthSummary(), 'used 1.06 KB in 3 requests this month' )
self.assertEqual( bandwidth_tracker.GetUsage( HC.BANDWIDTH_TYPE_DATA, 0 ), 0 )
self.assertEqual( bandwidth_tracker.GetUsage( HC.BANDWIDTH_TYPE_REQUESTS, 0 ), 0 )
diff --git a/static/default/parsers/4chan-style thread api parser.png b/static/default/parsers/4chan-style thread api parser.png
index 649146b2..ba116cce 100644
Binary files a/static/default/parsers/4chan-style thread api parser.png and b/static/default/parsers/4chan-style thread api parser.png differ