Version 491

This commit is contained in:
Hydrus Network Developer 2022-07-13 16:35:17 -05:00
parent 3a143af7a5
commit 07333d60c5
53 changed files with 1484 additions and 523 deletions

View File

@ -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 ***

View File

@ -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.
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.

View File

@ -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.

View File

@ -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

View File

@ -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-&gt;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:

View File

@ -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.

View File

@ -33,6 +33,53 @@
<div class="content">
<h3 id="changelog"><a href="#changelog">changelog</a></h3>
<ul>
<li><h3 id="version_491"><a href="#version_491">version 491</a></h3></li>
<ul>
<li>system predicates:</li>
<li>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</li>
<li>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</li>
<li>it also starts with the keyboard focus in the text input</li>
<li>and I think I fixed an issue with '!'', 'not', or '-' negation prefixes not parsing</li>
<li>highlighted the example parseable system predicate texts in the Client API help, and added 'last viewed' to it</li>
<li>.</li>
<li>misc:</li>
<li>altering your services in _manage services_ no longer causes a full page refresh for all currently open search pages</li>
<li>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</li>
<li>the rating widgets now show their current rating value on their tooltips</li>
<li>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</li>
<li>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</li>
<li>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</li>
<li>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</li>
<li>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...</li>
<li>the test to see if a directory is writeable-to is improved and now handles Windows's Program Files directory correctly</li>
<li>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</li>
<li>a new button on 'review services' now lets advanced users copy the service key to the clipboard</li>
<li>the migrate tags dialog now lists file repositories, ipfs services, and 'all my files' as potential file filter domains</li>
<li>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</li>
<li>updated the '4chan style thread api parser' to handle posts with multiple files, which fixes tvchan.moe and probably anything else running NPFchan</li>
<li>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</li>
<li>.</li>
<li>new import/export framework:</li>
<li>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</li>
<li>export folders now support '.txt' sidecar tag exporting!</li>
<li>the '.txt' sidecar tag importing in import folders or manual imports is now handled by the new pipeline</li>
<li>the '.txt' sidecar exporting in the manual export dialog is now handled by the new pipeline</li>
<li>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</li>
<li>.</li>
<li>cleanup and refactoring:</li>
<li>mr bones gets two new columns to line up the numbers better</li>
<li>a bunch of export code got moved around. created a new module 'exporting', and moved ClientExporting.py to it, renaming to ClientExportingFiles.py</li>
<li>removed an old prototype for sidecar exporting and related plans for UI</li>
<li>the 'missing file folders on boot' dialog now points users to 'help my media files are broke.txt'</li>
<li>brushed up the 'help my x is broke.txt' documents in the database directory a little</li>
<li>fixed some surplus double backslashes in the help</li>
<li>a secret tiny label change/fix, let's see if anyone notices</li>
<li>cleaned up how the rating widgets manage and update rating state. it was ancient bad code</li>
<li>updated how different rating values are converted to UI text</li>
<li>misc cleanup of some free space checking code</li>
<li>fixed some bad quote characters in client api help JSON examples</li>
<li>improved some error handling for uploading pending content and sped up file uploads a little</li>
</ul>
<li><h3 id="version_490"><a href="#version_490">version 490</a></h3></li>
<ul>
<li>misc:</li>

View File

@ -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:

View File

@ -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 )

View File

@ -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()

View File

@ -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 = []

View File

@ -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, ) )

View File

@ -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 )

View File

@ -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:

View File

@ -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 )

View File

@ -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 )
#

View File

@ -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 )

View File

@ -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 )

View File

@ -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 )

View File

@ -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 )

View File

@ -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 )

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@

View File

@ -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:

View File

@ -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 )

View File

@ -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 )

View File

@ -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:

View File

@ -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()

View File

@ -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:

View File

@ -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 )

View File

@ -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 )

View File

@ -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()

View File

@ -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 )

View File

@ -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()

View File

@ -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 )

View File

@ -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 } )

View File

@ -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] )

View File

@ -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 ) )

View File

@ -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 )

View File

@ -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' )

View File

@ -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

View File

@ -80,7 +80,7 @@ options = {}
# Misc
NETWORK_VERSION = 20
SOFTWARE_VERSION = 490
SOFTWARE_VERSION = 491
CLIENT_API_VERSION = 31
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -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 ) ) )

View File

@ -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()

View File

@ -1710,7 +1710,7 @@ def BaseToHumanBytes( size, sig_figs = 3 ):
pass
return '{}{}B'.format( d, suffix )
return '{} {}B'.format( d, suffix )
ToHumanBytes = BaseToHumanBytes

View File

@ -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():

View File

@ -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 = {}

View File

@ -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

View File

@ -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

View File

@ -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 )

View File

@ -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 )

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB