Version 239

This commit is contained in:
Hydrus Network Developer 2017-01-04 16:48:23 -06:00
parent 86d1489786
commit 0d36699fb7
31 changed files with 1667 additions and 1202 deletions

View File

@ -8,6 +8,57 @@
<div class="content">
<h3>changelog</h3>
<ul>
<li><h3>version 239</h3></li>
<ul>
<li>finished up similar files search data maintenance code</li>
<li>similar files search data maintenance will now run during idle time</li>
<li>similar files search data maintenance can be called from database->maintain menu</li>
<li>the crowded database->maintenance menu is now split into maintain, regenerate, check</li>
<li>improved the similar files tree generation code, speeding searches significantly</li>
<li>wrote a new listctrl class to handle more complicated objects and also sort by underlying data</li>
<li>the new listctrl now handles object name non-duplication</li>
<li>cleaned a bunch of crap old listctrl code</li>
<li>manage export folders now uses the new listctrl</li>
<li>manage import folders now uses the new listctrl</li>
<li>manage subs now uses the new listctrl</li>
<li>manage scripts now uses the new listctrl</li>
<li>options media viewer options now uses the new listctrl</li>
<li>file import status panel now uses the new listctrl</li>
<li>the new listctrl can now quickly fetch item index from the underlying object</li>
<li>the file import status panel should now cope with extremely huge lists a bit better now!</li>
<li>multiple parsing child nodes can now import from/export to clipboard as lists</li>
<li>export folders now have names, so you can have multiple export folders pointing to the same path! existing export folders will get their path as their name, but this can be changed no prob</li>
<li>cleaned import/export folder dialogs</li>
<li>cleaned import/export folder dialog workflow</li>
<li>several misc import/export folder improvements</li>
<li>hydrus servers are now exclusively https with self-signed certificates</li>
<li>hydrus servers now create server.crt and server.key in their db folders for SSL--these files will be backed up along with everything else on an admin backup command</li>
<li>system:hash now ignores the file domain and any other predicate. it now returns very quickly, no matter the context</li>
<li>improved system:hash search logic</li>
<li>all the awkward choice dropdowns in system predicate panels are replaced with radioboxes</li>
<li>improved system:rating panel grid layout</li>
<li>wrote a better subclass of radiobox to handle more data</li>
<li>moved first half of thumbnail menu to new menu system</li>
<li>cleaned up a little thumbnail menu logic</li>
<li>improved the different ways services are added to thumbnail menu</li>
<li>thumbnail 'select' menu is logically cleaned up and allows for better file domain selection</li>
<li>the thumbnail menu's copy files and copy hashes to clipboard will now send them ordered (they were previously pseudo-random)</li>
<li>added 'paths' to the share->copy thumbnail menu for copying multiple files' paths. these are also ordered.</li>
<li>if the popup message manager does not have any errors, it will no longer unhide (which can annoyingly raise the main gui window) when the gui window does not have focus</li>
<li>removed some old redundant error reporting stuff in popups</li>
<li>improved and quietened some some mime detection failure code, sped up mime failure loop in all cases</li>
<li>massively simplified and atomised how new serialisable-object management panels can save their data</li>
<li>the manage subscription dialog now saves in a single, faster transaction</li>
<li>the manage script dialog now saves in a single, faster transaction</li>
<li>reduced redundant index work from analyze jobs</li>
<li>improved tag parse error handling</li>
<li>fixed media removal rules when deleting from the 'all local files' domain</li>
<li>polished and clarified some of the help's tag schema</li>
<li>misc cleanup</li>
<li>more misc cleanup</li>
<li>misc refactoring</li>
<li>more misc refactoring</li>
</ul>
<li><h3>version 238</h3></li>
<ul>
<li>added left/right equal ratio preference to the similar files search tree generation</li>

View File

@ -6,9 +6,8 @@
</head>
<body>
<div class="content">
<p class="warning">At the moment, the server is hacked-together and quite technical. It requires a fair amount of experience with the client and its concepts, and it does not operate on a timescale that works well on a LAN. I suggest you only try running your own server once you have a bit of experience synchronising with mine or someone else's and you think, 'Hey, I know exactly what that does, and I would like one!'</p>
<p class="warning">The server is hacked-together and quite technical. It requires a fair amount of experience with the client and its concepts, and it does not operate on a timescale that works well on a LAN. I suggest you only try running your own server once you have a bit of experience synchronising with mine or someone else's and you think, 'Hey, I know exactly what that does, and I would like one!'</p>
<h3>setting up a server</h3>
<p><b><i>If this stuff be-fuzzles you, and you just want a simple server set up on the same computer you run the client, you can go </i>help->i don't know what I am doing->just set up the server on this computer, please<i> and you _should_ be all set up with an admin service and tag/file repos automatically.</i></b></p>
<p>I will use two terms, <i>server</i> and <i>service</i>, to mean two distinct things:</p>
<ul>
<li>A <b>server</b> is an instantiation of the hydrus server executable (e.g. server.exe in Windows). It has a complicated and flexible database that can run many different services in parallel.</li>
@ -37,7 +36,7 @@
<p>On the page for your new server, hit the initialise button. If you have everything set right, the server should generate its first, administrator account and return the access key, which the client will automatically set to the account for you.</p>
<p class="warning">YOU'LL WANT TO SAVE THAT KEY IN A SAFE PLACE</p>
<p>If you lose your admin access key, there is no way to get it back, and if you are not sqlite-proficient, you'll have to restart from the beginning by deleting your server's database files.</p>
<p>If the client can't connect to the server, it is either not running or you have a firewall/port-mapping problem. If you want a quick way to test the server's visibility, just put its host:port into your browser; if working, it should return some simple html identifying itself.</p>
<p>If the client can't connect to the server, it is either not running or you have a firewall/port-mapping problem. If you want a quick way to test the server's visibility, just put https://host:port into your browser--if it is working, your browser will probably complain about its self-signed https certificate. Once you add a certificate exception, the server should return some simple html identifying itself.</p>
<h3>set up the server</h3>
<p>You should have a new submenu, 'administrate services', under 'services', in the client gui. This is where you control most server and service-wide stuff.</p>
<p><i>admin->your server->manage services</i> lets you add, edit, and delete the services your server runs. Every time you add one, you will also be added as that service's first administrator, and the admin menu will gain a new entry for it.</i>

View File

@ -50,26 +50,37 @@
<p>I prefer the full 'series:the lord of the rings' rather than 'lotr'. If you are an advanced user, please help out with tag siblings to help induce this.</p>
<h3>character:anna (frozen)</h3>
<p>I am not fond of putting a series name after a character because it looks unusual and is applied unreliably. It is done to separate same-named characters from each other (particularly when they have no canon surname), which is useful in places that search slowly or usually only deal in single-tag searches. I would prefer that namespaces say their namespace and nothing else. Some sites even say things like 'anna (disney)'. I don't really mind this stuff, but if you are adding a sibling to collapse these divergent tags into the 'proper' one, I'd prefer it all went to the simple and reliable 'character:anna'. Even better would be migrating towards a canon-ok unique name, like 'character:princess anna of arendelle'.</p>
<p>Including nicknames, like 'character:angela "mercy" ziegler' can be useful to establish uniqueness, but are not mandatory. 'character:harleen "harley quinn" frances quinzel' is probably overboard.</p>
<h3>protip: reign in your spergitude</h3>
<p>Importing all the many tags from the boorus is totally fine, but if you are typing tags yourself, I suggest you do not try to tag <a href="http://safebooru.org/index.php?page=post&s=list&tags=card_on_necklace">everything</a> <a href="http://gelbooru.com/index.php?page=post&s=list&tags=collarbone">in</a> <a href="https://e621.net/post/index?tags=cum_on_neck">the</a> <a href="http://danbooru.donmai.us/posts?tags=brown_vest">image</a>--tag everything that you would search for. You will save a lot of time. Anyone can see what is in an image just by looking at it--tags are primarily for finding things. Character, series and creator namespaces are a great place to start. After that, add what you are interested in.</p>
<p>In developing hydrus, I have discovered two rules to happy tagging:</p>
<ol>
<li>Don't try to be perfect.</li>
<li>Only add those tags you actually use in searches.</li>
</ol>
<p>Tagging can be fun, but it can also be complicated, and the problem space is gigantic. There is always works to do, and it is easy to exhaust onesself or get lost in the bushes agonising over whether to use 'smile' or 'smiling' or 'smirk' or one of a million other split hairs. Problems are easy to fix, and this marathon will never finish, so do not try to sprint.</p>
<p>The sheer number of tags can also be overwhelming. Importing all the many tags from the boorus is totally fine, but if you are typing tags yourself, I suggest you try not to exhaustively tag <a href="http://safebooru.org/index.php?page=post&s=list&tags=card_on_necklace">everything</a> <a href="http://gelbooru.com/index.php?page=post&s=list&tags=collarbone">in</a> <a href="https://e621.net/post/index?tags=cum_on_neck">the</a> <a href="http://danbooru.donmai.us/posts?tags=brown_vest">image</a>. You will save a lot of time and ultimately be much happier with your work. Anyone can see what is in an image just by looking at it--tags are primarily for finding things. Character, series and creator namespaces are a great place to start. After that, add what you are interested in, be that 'blue sky' or 'midriff'.</p>
<h3>siblings and parents</h3>
<p>Please do add <a href="advanced_siblings.html">siblings</a> and <a href="advanced_parents.html">parents</a>! If it is something not obvious, please explain the relationship in your submitted reason. If it <i>is</i> something obvious (e.g. 'wings' is a parent of 'angel wings'), don't bother to put a reason in; I'll just approve it.</p>
<p>My general thoughts:</p>
<ul>
<li>
<h3>siblings</h3>
<p>The correctness of a thing is in how it would describe itself, or how its creator would describe it. For instance, I usually prefer japanese names be written surname first and western names forename first. As I show in my siblings page, please go 'character:rei ayanami'->'character:ayanami rei'. Leave 'person:emma watson' and other western names as they are, obviously.</p>
<p>In general, if there is a more proper foreign name for something, correct any entirely anglicised version back to that, but stick to english letters. 'series:the melancholy of haruhi suzumiya'->'series:haruhi suzumiya no yuuutsu'.</p>
<p>I don't care about popularity as much as accuracy. If there are two things that mean the same 'series:pretty cure' and 'series:precure', I would prefer 'series:pretty cure' because it is the 'full and proper' rendering, even though there are more instances of 'precure' on the boorus.</p>
<p>In general, the correctness of a thing is in how it would describe itself, or how its creator would describe it.</p>
<p>For shorthand, I will say 'a'->'b' to mean 'a' is replaced by 'b'.</p>
<p>For instance, japanese names are usually written surname first and western forename first, so let's go 'character:rei ayanami'->'character:ayanami rei' but leave 'person:emma watson' and other western names as they are.</p>
<p>Unless it is too obscure, replace the english version of a word with any more proper or original foreign name. But stick to something a westerner can read. Do things like 'series:the melancholy of haruhi suzumiya'->'series:haruhi suzumiya no yuuutsu' or 'series:princess mononoke'->'series:mononoke hime'. There's even an argument for things like 'series:harry potter and the sorcerer's stone'->'series:harry potter and the philosopher's stone'.</p>
<p>Accents and other unusual/unicode characters are great in tags if they reflect the official marketed name, and should be preferred, but make sure there's an ascii->unicode sibling to make it easy for most users to type. 'series:pokemon'->'series:pokémon' is excellent, as it both reflects official branding and also helps anyone who can't easily produce 'é' on their keyboard find it.</p>
<p>I don't care about popularity as much as accuracy. Given 'series:pretty cure' and 'series:precure', I would prefer 'series:pretty cure' because it is the 'full and proper' rendering, even though there are more instances of 'precure' on the boorus.</p>
<p>Do correct for common plural mistakes. ear->ears, women->female, and so on.</p>
<p>Please <b>do not</b> go 'blah'->'character:blah' unless the name is popular and unique. No one is going to create a new character named 'ayanami rei', so that's fine, but going 'archer'->'<a href="http://typemoon.wikia.com/wiki/Archer_%28Fate/stay_night%29">character:archer</a>' is going to create a lot of false positives.</p>
<p>And feel free to replace any 'character (series)' booru artifacts as with the 'anna (frozen)' example above. 'character:anna (frozen)'->'character:princess anna of arendelle' is great wherever it makes sense.</p>
<p>But please <b>do not</b> go 'blah'->'character:blah' unless the name is popular and unique. No one is going to be confused by 'ayanami rei'->'character:ayanami rei', but going 'archer'->'<a href="http://typemoon.wikia.com/wiki/Archer_%28Fate/stay_night%29">character:archer</a>' is going to create a lot of false positives. There's a similar problem with something like 'character:mercy'->'character:angela "mercy" ziegler'--although the left hand side is namespaced, there are still plenty of <i>characters</i> named 'mercy', so a sibling that converts all Mercys to Overwatch's Mercy is not appropriate.</p>
<p>In general, swap out slang for proper terms. 'lube'->'lubricant', 'series:zelda'->'series:the legend of zelda'.</p>
</li>
<li>
<h3>parents</h3>
<p>Be shy about adding character:blah->series:whatever, but feel free to do it for main characters. 'character:harry potter'->'series:harry potter' seems fairly uncontroversial, for instance.</p>
<p>Please remember that parents are definitional, not incidental. Don't add a parent because it is usually associated with a child; add it because it is <i>always</i> associated with a child.</p>
<p>I recommend you only put your time into parents that will actually add a lot of tags. You can create a complicated tree like the firearms diagram on my parents page, but if it only adds seven tags, you probably wasted your time. Start with 'person:scarlett johansson'->'female' type stuff, which is guaranteed to make hundreds or thousands of changes.</p>
<p>Be shy about adding character:blah->series:whatever unless you are certain the character name is unique. 'character:harry potter'->'series:harry potter' seems fairly uncontroversial, for instance.</p>
<p>Remember that parents define a relationship that is always true. Don't add 'blonde hair' to 'character:elsa', even though it is true in most files--add 'animal ears' to 'cat ears', as cat ears are always animal ears, no matter what an artist can think up.</p>
<p>I recommend you only put your time into parents that will actually add a lot of tags and be useful when typing tags in future. You can create a complicated tree like the firearms diagram on my parents page, but if it only adds seven tags, you probably wasted your time.</p>
</li>
</ul>
</div>

View File

@ -1953,6 +1953,28 @@ class ServicesManager( object ):
self._controller.sub( self, 'RefreshServices', 'notify_new_services_data' )
def _GetService( self, service_key ):
try:
return self._keys_to_services[ service_key ]
except KeyError:
raise HydrusExceptions.DataMissing( 'That service was not found!' )
def Filter( self, service_keys, desired_types ):
with self._lock:
filtered_service_keys = [ service_key for service_key in service_keys if self._keys_to_services[ service_key ].GetServiceType() in desired_types ]
return filtered_service_keys
def FilterValidServiceKeys( self, service_keys ):
with self._lock:
@ -1963,26 +1985,39 @@ class ServicesManager( object ):
def GetName( self, service_key ):
with self._lock:
service = self._GetService( service_key )
return service.GetName()
def GetService( self, service_key ):
with self._lock:
try:
return self._keys_to_services[ service_key ]
except KeyError:
raise HydrusExceptions.DataMissing( 'That service was not found!' )
return self._GetService( service_key )
def GetServices( self, types = HC.ALL_SERVICES, randomised = True ):
def GetServiceKeys( self, desired_types = HC.ALL_SERVICES ):
with self._lock:
services = [ service for service in self._services_sorted if service.GetServiceType() in types ]
filtered_service_keys = [ service_key for ( service_key, service ) in self._keys_to_services.items() if service.GetServiceType() in desired_types ]
return filtered_service_keys
def GetServices( self, desired_types = HC.ALL_SERVICES, randomised = True ):
with self._lock:
services = [ service for service in self._services_sorted if service.GetServiceType() in desired_types ]
if randomised:

View File

@ -692,9 +692,9 @@ class Controller( HydrusController.HydrusController ):
loaded_into_disk_cache = HydrusGlobals.client_controller.Read( 'load_into_disk_cache', stop_time = disk_cache_stop_time, caller_limit = disk_cache_maintenance_mb * 1024 * 1024 )
self.WriteInterruptable( 'vacuum', stop_time = stop_time )
self.WriteInterruptable( 'maintain_similar_files', stop_time = stop_time )
self.pub( 'splash_set_status_text', 'analyzing' )
self.WriteInterruptable( 'vacuum', stop_time = stop_time )
self.WriteInterruptable( 'analyze', stop_time = stop_time )

View File

@ -1276,7 +1276,7 @@ class DB( HydrusDB.HydrusDB ):
for db_name in db_names:
all_names.update( ( name for ( name, ) in self._c.execute( 'SELECT name FROM ' + db_name + '.sqlite_master;' ) ) )
all_names.update( ( name for ( name, ) in self._c.execute( 'SELECT name FROM ' + db_name + '.sqlite_master WHERE type = ?;', ( 'table', ) ) ) )
all_names.discard( 'sqlite_stat1' )
@ -1666,13 +1666,27 @@ class DB( HydrusDB.HydrusDB ):
median_index = len( children ) / 2
radius = children[ median_index ][0]
median_radius = children[ median_index ][0]
inner_children = [ ( child_id, child_phash ) for ( distance, child_id, child_phash ) in children if distance <= radius ]
outer_children = [ ( child_id, child_phash ) for ( distance, child_id, child_phash ) in children if distance > radius ]
inner_children = [ ( child_id, child_phash ) for ( distance, child_id, child_phash ) in children if distance < median_radius ]
radius_children = [ ( child_id, child_phash ) for ( distance, child_id, child_phash ) in children if distance == median_radius ]
outer_children = [ ( child_id, child_phash ) for ( distance, child_id, child_phash ) in children if distance > median_radius ]
inner_population = len( inner_children )
outer_population = len( outer_children )
if len( inner_children ) <= len( outer_children ):
radius = median_radius
inner_children.extend( radius_children )
else:
radius = median_radius - 1
outer_children.extend( radius_children )
inner_population = len( inner_children )
outer_population = len( outer_children )
( inner_id, inner_phash ) = self._CacheSimilarFilesPopBestRootNode( inner_children ) #HydrusData.MedianPop( inner_children )
@ -1763,9 +1777,9 @@ class DB( HydrusDB.HydrusDB ):
job_key.SetVariable( 'popup_title', 'similar files metadata maintenance' )
self._controller.pub( 'message', job_key )
job_key_pubbed = False
hash_ids = [ hash_id for ( hash_id, ) in self._c.execute( 'SELECT hash_id FROM shape_maintenance_regen_phash;' ) ]
hash_ids = [ hash_id for ( hash_id, ) in self._c.execute( 'SELECT hash_id FROM shape_maintenance_phash_regen;' ) ]
client_files_manager = self._controller.GetClientFilesManager()
@ -1773,6 +1787,13 @@ class DB( HydrusDB.HydrusDB ):
for ( i, hash_id ) in enumerate( hash_ids ):
if not job_key_pubbed:
self._controller.pub( 'message', job_key )
job_key_pubbed = True
( i_paused, should_quit ) = job_key.WaitIfNeeded()
if should_quit or HydrusData.TimeHasPassed( stop_time ):
@ -1829,7 +1850,7 @@ class DB( HydrusDB.HydrusDB ):
self._CacheSimilarFilesDelete( hash_id, deletee_phash_ids )
self._c.execute( 'DELETE FROM shape_maintenance_regen_phash WHERE hash_id = ?;', ( hash_id, ) )
self._c.execute( 'DELETE FROM shape_maintenance_phash_regen WHERE hash_id = ?;', ( hash_id, ) )
rebalance_phash_ids = [ phash_id for ( phash_id, ) in self._c.execute( 'SELECT phash_id FROM shape_maintenance_branch_regen;' ) ]
@ -1838,6 +1859,13 @@ class DB( HydrusDB.HydrusDB ):
while len( rebalance_phash_ids ) > 0:
if not job_key_pubbed:
self._controller.pub( 'message', job_key )
job_key_pubbed = True
( i_paused, should_quit ) = job_key.WaitIfNeeded()
if should_quit or HydrusData.TimeHasPassed( stop_time ):
@ -1855,7 +1883,7 @@ class DB( HydrusDB.HydrusDB ):
with HydrusDB.TemporaryIntegerTable( self._c, rebalance_phash_ids, 'phash_id' ) as temp_table_name:
( biggest_phash_id, ) = self._c.execute( 'SELECT phash_id FROM shape_vptree, ' + temp_table_name + ' USING ( phash_id ) ORDER BY left_population + right_population DESC;' ).fetchone()
( biggest_phash_id, ) = self._c.execute( 'SELECT phash_id FROM shape_vptree, ' + temp_table_name + ' USING ( phash_id ) ORDER BY inner_population + outer_population DESC;' ).fetchone()
self._CacheSimilarFilesRegenerateBranch( job_key, biggest_phash_id )
@ -1863,6 +1891,8 @@ class DB( HydrusDB.HydrusDB ):
rebalance_phash_ids = [ phash_id for ( phash_id, ) in self._c.execute( 'SELECT phash_id FROM shape_maintenance_branch_regen;' ) ]
job_key.SetVariable( 'popup_text_1', 'done!' )
job_key.DeleteVariable( 'popup_gauge_1' )
job_key.DeleteVariable( 'popup_text_2' )
job_key.Finish()
@ -1926,15 +1956,25 @@ class DB( HydrusDB.HydrusDB ):
views.sort()
# let's figure out the ratio of left_children to right_children, preferring 1:1, and convert it to a discreet integer score
# let's figure out the ratio of left_children to right_children, preferring 1:1, and convert it to a discrete integer score
median_index = len( views ) / 2
radius = views[ median_index ]
num_left = float( len( [ 1 for view in views if view <= radius ] ) )
num_left = float( len( [ 1 for view in views if view < radius ] ) )
num_radius = float( len( [ 1 for view in views if view == radius ] ) )
num_right = float( len( [ 1 for view in views if view > radius ] ) )
if num_left <= num_right:
num_left += num_radius
else:
num_right += num_radius
smaller = min( num_left, num_right )
larger = max( num_left, num_right )
@ -1974,7 +2014,7 @@ class DB( HydrusDB.HydrusDB ):
# grab everything in the branch
( parent_id, ) = self._c.execute( 'SELECT parent_id FROM shape_vptree WHERE phash_id = ?;', ( phash_id ) ).fetchone()
( parent_id, ) = self._c.execute( 'SELECT parent_id FROM shape_vptree WHERE phash_id = ?;', ( phash_id, ) ).fetchone()
cte_table_name = 'branch ( branch_phash_id )'
initial_select = 'SELECT ?'
@ -1982,32 +2022,41 @@ class DB( HydrusDB.HydrusDB ):
with_clause = 'WITH RECURSIVE ' + cte_table_name + ' AS ( ' + initial_select + ' UNION ALL ' + recursive_select + ')'
rebalance_nodes = self._c.execute( with_clause + ' SELECT branch_phash_id, phash FROM branch, shape_perceptual_hashes ON phash_id = branch_phash_id;', ( phash_id, ) ).fetchall()
unbalanced_nodes = self._c.execute( with_clause + ' SELECT branch_phash_id, phash FROM branch, shape_perceptual_hashes ON phash_id = branch_phash_id;', ( phash_id, ) ).fetchall()
# removal of old branch, maintenance schedule, and orphan phashes
job_key.SetVariable( 'popup_text_2', HydrusData.ConvertIntToPrettyString( len( rebalance_nodes ) ) + ' leaves found--now clearing out old branch' )
job_key.SetVariable( 'popup_text_2', HydrusData.ConvertIntToPrettyString( len( unbalanced_nodes ) ) + ' leaves found--now clearing out old branch' )
rebalance_phash_ids = { p_id for ( p_id, p_h ) in rebalance_nodes }
unbalanced_phash_ids = { p_id for ( p_id, p_h ) in unbalanced_nodes }
self._c.executemany( 'DELETE FROM shape_vptree WHERE phash_id = ?;', ( ( p_id, ) for p_id in rebalance_phash_ids ) )
self._c.executemany( 'DELETE FROM shape_vptree WHERE phash_id = ?;', ( ( p_id, ) for p_id in unbalanced_phash_ids ) )
self._c.executemany( 'DELETE FROM shape_maintenance_branch_regen WHERE phash_id = ?;', ( ( p_id, ) for p_id in rebalance_phash_ids ) )
self._c.executemany( 'DELETE FROM shape_maintenance_branch_regen WHERE phash_id = ?;', ( ( p_id, ) for p_id in unbalanced_phash_ids ) )
with HydrusDB.TemporaryIntegerTable( self._c, rebalance_phash_ids, 'phash_id' ) as temp_table_name:
with HydrusDB.TemporaryIntegerTable( self._c, unbalanced_phash_ids, 'phash_id' ) as temp_table_name:
useful_phash_ids = { p_id for ( p_id, ) in self._c.execute( 'SELECT phash_id FROM shape_perceptual_hash_map, ' + temp_table_name + ' USING ( phash_id );' ) }
orphan_phash_ids = rebalance_phash_ids.difference( useful_phash_ids )
orphan_phash_ids = unbalanced_phash_ids.difference( useful_phash_ids )
self._c.executemany( 'DELETE FROM shape_perceptual_hashes WHERE phash_id = ?;', ( ( p_id, ) for p_id in orphan_phash_ids ) )
rebalance_nodes = [ row for row in rebalance_nodes if row[0] in rebalance_phash_ids ]
useful_nodes = [ row for row in unbalanced_nodes if row[0] in useful_phash_ids ]
useful_population = len( useful_nodes )
# now create the new branch, starting by choosing a new root and updating the parent's left/right reference to that
( new_phash_id, new_phash ) = self._CacheSimilarFilesPopBestRootNode( rebalance_nodes ) #HydrusData.RandomPop( rebalance_nodes )
if useful_population > 0:
( new_phash_id, new_phash ) = self._CacheSimilarFilesPopBestRootNode( useful_nodes ) #HydrusData.RandomPop( useful_nodes )
else:
new_phash_id = None
if parent_id is not None:
@ -2015,17 +2064,20 @@ class DB( HydrusDB.HydrusDB ):
if parent_inner_id == phash_id:
column_name = 'inner_id'
query = 'UPDATE shape_vptree SET inner_id = ?, inner_population = ? WHERE phash_id = ?;'
else:
column_name = 'outer_id'
query = 'UPDATE shape_vptree SET outer_id = ?, outer_population = ? WHERE phash_id = ?;'
self._c.execute( 'UPDATE shape_vptree SET ' + column_name + ' = ? WHERE phash_id = ?;', ( new_phash_id, parent_id ) )
self._c.execute( query, ( new_phash_id, useful_population, parent_id ) )
self._CacheSimilarFilesGenerateBranch( job_key, parent_id, new_phash_id, new_phash, rebalance_nodes )
if useful_population > 0:
self._CacheSimilarFilesGenerateBranch( job_key, parent_id, new_phash_id, new_phash, useful_nodes )
def _CacheSimilarFilesRegenerateTree( self ):
@ -2056,7 +2108,7 @@ class DB( HydrusDB.HydrusDB ):
if hash_ids is None:
hash_ids = { hash_id for ( hash_id, ) in self._c.execute( 'SELECT hash_id FROM files_info, current_files USING ( hash_id ) WHERE service_id = ? AND mime IN ' + HydrusData.SplayListForDB( HC.MIMES_WE_CAN_PHASH ) + ';', ( self._combined_local_file_service_id, ) ) }
hash_ids = [ hash_id for ( hash_id, ) in self._c.execute( 'SELECT hash_id FROM files_info, current_files USING ( hash_id ) WHERE service_id = ? AND mime IN ' + HydrusData.SplayListForDB( HC.MIMES_WE_CAN_PHASH ) + ';', ( self._combined_local_file_service_id, ) ) ]
self._c.executemany( 'INSERT OR IGNORE INTO shape_maintenance_phash_regen ( hash_id ) VALUES ( ? );', ( ( hash_id, ) for hash_id in hash_ids ) )
@ -3799,6 +3851,40 @@ class DB( HydrusDB.HydrusDB ):
simple_preds = system_predicates.GetSimpleInfo()
# This now overrides any other predicates, including file domain
if 'hash' in simple_preds:
query_hash_ids = set()
( search_hash, search_hash_type ) = simple_preds[ 'hash' ]
if search_hash_type != 'sha256':
result = self._GetFileHashes( [ search_hash ], search_hash_type, 'sha256' )
if len( result ) > 0:
( search_hash, ) = result
hash_id = self._GetHashId( search_hash )
query_hash_ids = { hash_id }
else:
if self._HashExists( search_hash ):
hash_id = self._GetHashId( search_hash )
query_hash_ids = { hash_id }
return query_hash_ids
if 'min_size' in simple_preds: files_info_predicates.append( 'size > ' + str( simple_preds[ 'min_size' ] ) )
if 'size' in simple_preds: files_info_predicates.append( 'size = ' + str( simple_preds[ 'size' ] ) )
if 'max_size' in simple_preds: files_info_predicates.append( 'size < ' + str( simple_preds[ 'max_size' ] ) )
@ -3938,37 +4024,6 @@ class DB( HydrusDB.HydrusDB ):
#
if 'hash' in simple_preds:
( search_hash, search_hash_type ) = simple_preds[ 'hash' ]
if search_hash_type != 'sha256':
result = self._GetFileHashes( [ search_hash ], search_hash_type, 'sha256' )
if len( result ) == 0:
query_hash_ids = set()
else:
( search_hash, ) = result
hash_id = self._GetHashId( search_hash )
query_hash_ids.intersection_update( { hash_id } )
else:
hash_id = self._GetHashId( search_hash )
query_hash_ids.intersection_update( { hash_id } )
#
if system_predicates.HasSimilarTo():
@ -6166,7 +6221,7 @@ class DB( HydrusDB.HydrusDB ):
for db_name in db_names:
all_names.update( ( name for ( name, ) in self._c.execute( 'SELECT name FROM ' + db_name + '.sqlite_master;' ) ) )
all_names.update( ( name for ( name, ) in self._c.execute( 'SELECT name FROM ' + db_name + '.sqlite_master WHERE type = ?;', ( 'table', ) ) ) )
names_to_analyze = { name for name in all_names if name not in existing_names_to_timestamps or HydrusData.TimeHasPassed( existing_names_to_timestamps[ name ] + stale_time_delta ) }
@ -6178,7 +6233,7 @@ class DB( HydrusDB.HydrusDB ):
return True
return False
return self._CacheSimilarFilesMaintenanceDue()
def _ManageDBError( self, job, e ):
@ -6204,6 +6259,19 @@ class DB( HydrusDB.HydrusDB ):
def _OverwriteJSONDumps( self, dump_types, objs ):
for dump_type in dump_types:
self._DeleteJSONDumpNamed( dump_type )
for obj in objs:
self._SetJSONDump( obj )
def _ProcessContentUpdatePackage( self, service_key, content_update_package, job_key ):
( previous_journal_mode, ) = self._c.execute( 'PRAGMA journal_mode;' ).fetchone()
@ -9451,6 +9519,7 @@ class DB( HydrusDB.HydrusDB ):
elif action == 'imageboard': result = self._SetYAMLDump( YAML_DUMP_ID_IMAGEBOARD, *args, **kwargs )
elif action == 'import_file': result = self._ImportFile( *args, **kwargs )
elif action == 'local_booru_share': result = self._SetYAMLDump( YAML_DUMP_ID_LOCAL_BOORU, *args, **kwargs )
elif action == 'maintain_similar_files': result = self._CacheSimilarFilesMaintain( *args, **kwargs )
elif action == 'push_recent_tags': result = self._PushRecentTags( *args, **kwargs )
elif action == 'regenerate_ac_cache': result = self._RegenerateACCache( *args, **kwargs )
elif action == 'regenerate_similar_files': result = self._CacheSimilarFilesRegenerateTree( *args, **kwargs )
@ -9460,6 +9529,7 @@ class DB( HydrusDB.HydrusDB ):
elif action == 'save_options': result = self._SaveOptions( *args, **kwargs )
elif action == 'serialisable_simple': result = self._SetJSONSimple( *args, **kwargs )
elif action == 'serialisable': result = self._SetJSONDump( *args, **kwargs )
elif action == 'serialisables_overwrite': result = self._OverwriteJSONDumps( *args, **kwargs )
elif action == 'service_updates': result = self._ProcessServiceUpdates( *args, **kwargs )
elif action == 'set_password': result = self._SetPassword( *args, **kwargs )
elif action == 'sync_hashes_to_tag_archive': result = self._SyncHashesToTagArchive( *args, **kwargs )

View File

@ -114,7 +114,7 @@ def ConvertServiceKeysToContentUpdatesToPrettyString( service_keys_to_content_up
if len( content_updates ) > 0:
name = HydrusGlobals.client_controller.GetServicesManager().GetService( service_key ).GetName()
name = HydrusGlobals.client_controller.GetServicesManager().GetName( service_key )
locations.add( name )

View File

@ -1041,6 +1041,11 @@ class GalleryBooru( Gallery ):
for link in links:
if link.string is None:
continue
if link.string not in ( '?', '-', '+' ):
if namespace == '': tags.append( link.string )

336
include/ClientExporting.py Normal file
View File

@ -0,0 +1,336 @@
import ClientConstants as CC
import ClientData
import ClientFiles
import ClientSearch
import HydrusConstants as HC
import HydrusData
import HydrusGlobals
import HydrusPaths
import HydrusSerialisable
import os
import re
import stat
def GenerateExportFilename( media, terms ):
mime = media.GetMime()
filename = ''
for ( term_type, term ) in terms:
tags_manager = media.GetTagsManager()
if term_type == 'string':
filename += term
elif term_type == 'namespace':
tags = tags_manager.GetNamespaceSlice( ( term, ) )
filename += ', '.join( [ tag.split( ':' )[1] for tag in tags ] )
elif term_type == 'predicate':
if term in ( 'tags', 'nn tags' ):
current = tags_manager.GetCurrent()
pending = tags_manager.GetPending()
tags = list( current.union( pending ) )
if term == 'nn tags': tags = [ tag for tag in tags if ':' not in tag ]
else: tags = [ tag if ':' not in tag else tag.split( ':' )[1] for tag in tags ]
tags.sort()
filename += ', '.join( tags )
elif term == 'hash':
hash = media.GetHash()
filename += hash.encode( 'hex' )
elif term_type == 'tag':
if ':' in term: term = term.split( ':' )[1]
if tags_manager.HasTag( term ): filename += term
if HC.PLATFORM_WINDOWS:
filename = re.sub( '\\\\|/|:|\\*|\\?|"|<|>|\\|', '_', filename, flags = re.UNICODE )
else:
filename = re.sub( '/', '_', filename, flags = re.UNICODE )
ext = HC.mime_ext_lookup[ mime ]
if not filename.endswith( ext ):
filename += ext
return filename
def GetExportPath():
options = HydrusGlobals.client_controller.GetOptions()
portable_path = options[ 'export_path' ]
if portable_path is None:
path = os.path.join( os.path.expanduser( '~' ), 'hydrus_export' )
HydrusPaths.MakeSureDirectoryExists( path )
else:
path = HydrusPaths.ConvertPortablePathToAbsPath( portable_path )
return path
def ParseExportPhrase( phrase ):
try:
terms = [ ( 'string', phrase ) ]
new_terms = []
for ( term_type, term ) in terms:
if term_type == 'string':
while '[' in term:
( pre, term ) = term.split( '[', 1 )
( namespace, term ) = term.split( ']', 1 )
new_terms.append( ( 'string', pre ) )
new_terms.append( ( 'namespace', namespace ) )
new_terms.append( ( term_type, term ) )
terms = new_terms
new_terms = []
for ( term_type, term ) in terms:
if term_type == 'string':
while '{' in term:
( pre, term ) = term.split( '{', 1 )
( predicate, term ) = term.split( '}', 1 )
new_terms.append( ( 'string', pre ) )
new_terms.append( ( 'predicate', predicate ) )
new_terms.append( ( term_type, term ) )
terms = new_terms
new_terms = []
for ( term_type, term ) in terms:
if term_type == 'string':
while '(' in term:
( pre, term ) = term.split( '(', 1 )
( tag, term ) = term.split( ')', 1 )
new_terms.append( ( 'string', pre ) )
new_terms.append( ( 'tag', tag ) )
new_terms.append( ( term_type, term ) )
terms = new_terms
except: raise Exception( 'Could not parse that phrase!' )
return terms
class ExportFolder( HydrusSerialisable.SerialisableBaseNamed ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_EXPORT_FOLDER
SERIALISABLE_VERSION = 2
def __init__( self, name, path = '', export_type = HC.EXPORT_FOLDER_TYPE_REGULAR, file_search_context = None, period = 3600, phrase = '{hash}' ):
HydrusSerialisable.SerialisableBaseNamed.__init__( self, name )
if file_search_context is None:
file_search_context = ClientSearch.FileSearchContext( file_service_key = CC.LOCAL_FILE_SERVICE_KEY )
self._path = path
self._export_type = export_type
self._file_search_context = file_search_context
self._period = period
self._phrase = phrase
self._last_checked = 0
def _GetSerialisableInfo( self ):
serialisable_file_search_context = self._file_search_context.GetSerialisableTuple()
return ( self._path, self._export_type, serialisable_file_search_context, self._period, self._phrase, self._last_checked )
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
( self._path, self._export_type, serialisable_file_search_context, self._period, self._phrase, self._last_checked ) = serialisable_info
self._file_search_context = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_file_search_context )
def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
if version == 1:
( export_type, serialisable_file_search_context, period, phrase, last_checked ) = old_serialisable_info
path = self._name
new_serialisable_info = ( path, export_type, serialisable_file_search_context, period, phrase, last_checked )
return ( 2, new_serialisable_info )
def DoWork( self ):
if HydrusData.TimeHasPassed( self._last_checked + self._period ):
folder_path = HydrusData.ToUnicode( self._path )
if folder_path != '' and os.path.exists( folder_path ) and os.path.isdir( folder_path ):
query_hash_ids = HydrusGlobals.client_controller.Read( 'file_query_ids', self._file_search_context )
media_results = []
i = 0
base = 256
while i < len( query_hash_ids ):
if HC.options[ 'pause_export_folders_sync' ]:
return
if i == 0: ( last_i, i ) = ( 0, base )
else: ( last_i, i ) = ( i, i + base )
sub_query_hash_ids = query_hash_ids[ last_i : i ]
more_media_results = HydrusGlobals.client_controller.Read( 'media_results_from_ids', sub_query_hash_ids )
media_results.extend( more_media_results )
#
terms = ParseExportPhrase( self._phrase )
previous_filenames = set( os.listdir( folder_path ) )
sync_filenames = set()
client_files_manager = HydrusGlobals.client_controller.GetClientFilesManager()
num_copied = 0
for media_result in media_results:
hash = media_result.GetHash()
mime = media_result.GetMime()
size = media_result.GetSize()
source_path = client_files_manager.GetFilePath( hash, mime )
filename = GenerateExportFilename( media_result, terms )
dest_path = os.path.join( folder_path, filename )
if filename not in sync_filenames:
copied = HydrusPaths.MirrorFile( source_path, dest_path )
if copied:
num_copied += 1
try: os.chmod( dest_path, stat.S_IWRITE | stat.S_IREAD )
except: pass
sync_filenames.add( filename )
if num_copied > 0:
HydrusData.Print( 'Export folder ' + self._name + ' exported ' + HydrusData.ConvertIntToPrettyString( num_copied ) + ' files.' )
if self._export_type == HC.EXPORT_FOLDER_TYPE_SYNCHRONISE:
deletee_filenames = previous_filenames.difference( sync_filenames )
for deletee_filename in deletee_filenames:
deletee_path = os.path.join( folder_path, deletee_filename )
ClientData.DeletePath( deletee_path )
if len( deletee_filenames ) > 0:
HydrusData.Print( 'Export folder ' + self._name + ' deleted ' + HydrusData.ConvertIntToPrettyString( len( deletee_filenames ) ) + ' files.' )
self._last_checked = HydrusData.GetNow()
HydrusGlobals.client_controller.WriteSynchronous( 'serialisable', self )
def ToTuple( self ):
return ( self._name, self._path, self._export_type, self._file_search_context, self._period, self._phrase )
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_EXPORT_FOLDER ] = ExportFolder

View File

@ -16,75 +16,6 @@ import shutil
import stat
import wx
def GenerateExportFilename( media, terms ):
mime = media.GetMime()
filename = ''
for ( term_type, term ) in terms:
tags_manager = media.GetTagsManager()
if term_type == 'string':
filename += term
elif term_type == 'namespace':
tags = tags_manager.GetNamespaceSlice( ( term, ) )
filename += ', '.join( [ tag.split( ':' )[1] for tag in tags ] )
elif term_type == 'predicate':
if term in ( 'tags', 'nn tags' ):
current = tags_manager.GetCurrent()
pending = tags_manager.GetPending()
tags = list( current.union( pending ) )
if term == 'nn tags': tags = [ tag for tag in tags if ':' not in tag ]
else: tags = [ tag if ':' not in tag else tag.split( ':' )[1] for tag in tags ]
tags.sort()
filename += ', '.join( tags )
elif term == 'hash':
hash = media.GetHash()
filename += hash.encode( 'hex' )
elif term_type == 'tag':
if ':' in term: term = term.split( ':' )[1]
if tags_manager.HasTag( term ): filename += term
if HC.PLATFORM_WINDOWS:
filename = re.sub( '\\\\|/|:|\\*|\\?|"|<|>|\\|', '_', filename, flags = re.UNICODE )
else:
filename = re.sub( '/', '_', filename, flags = re.UNICODE )
ext = HC.mime_ext_lookup[ mime ]
if not filename.endswith( ext ):
filename += ext
return filename
def GetAllPaths( raw_paths ):
file_paths = []
@ -116,25 +47,6 @@ def GetAllPaths( raw_paths ):
return file_paths
def GetExportPath():
options = HydrusGlobals.client_controller.GetOptions()
portable_path = options[ 'export_path' ]
if portable_path is None:
path = os.path.join( os.path.expanduser( '~' ), 'hydrus_export' )
HydrusPaths.MakeSureDirectoryExists( path )
else:
path = HydrusPaths.ConvertPortablePathToAbsPath( portable_path )
return path
def GetExpectedContentUpdatePackagePath( service_key, begin, subindex ):
return os.path.join( GetExpectedUpdateDir( service_key ), str( begin ) + '_' + str( subindex ) + '.json' )
@ -149,226 +61,3 @@ def GetExpectedUpdateDir( service_key ):
return os.path.join( updates_dir, service_key.encode( 'hex' ) )
def ParseExportPhrase( phrase ):
try:
terms = [ ( 'string', phrase ) ]
new_terms = []
for ( term_type, term ) in terms:
if term_type == 'string':
while '[' in term:
( pre, term ) = term.split( '[', 1 )
( namespace, term ) = term.split( ']', 1 )
new_terms.append( ( 'string', pre ) )
new_terms.append( ( 'namespace', namespace ) )
new_terms.append( ( term_type, term ) )
terms = new_terms
new_terms = []
for ( term_type, term ) in terms:
if term_type == 'string':
while '{' in term:
( pre, term ) = term.split( '{', 1 )
( predicate, term ) = term.split( '}', 1 )
new_terms.append( ( 'string', pre ) )
new_terms.append( ( 'predicate', predicate ) )
new_terms.append( ( term_type, term ) )
terms = new_terms
new_terms = []
for ( term_type, term ) in terms:
if term_type == 'string':
while '(' in term:
( pre, term ) = term.split( '(', 1 )
( tag, term ) = term.split( ')', 1 )
new_terms.append( ( 'string', pre ) )
new_terms.append( ( 'tag', tag ) )
new_terms.append( ( term_type, term ) )
terms = new_terms
except: raise Exception( 'Could not parse that phrase!' )
return terms
class ExportFolder( HydrusSerialisable.SerialisableBaseNamed ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_EXPORT_FOLDER
SERIALISABLE_VERSION = 1
def __init__( self, name, export_type = HC.EXPORT_FOLDER_TYPE_REGULAR, file_search_context = None, period = 3600, phrase = '{hash}' ):
HydrusSerialisable.SerialisableBaseNamed.__init__( self, name )
self._export_type = export_type
self._file_search_context = file_search_context
self._period = period
self._phrase = phrase
self._last_checked = 0
def _GetSerialisableInfo( self ):
serialisable_file_search_context = self._file_search_context.GetSerialisableTuple()
return ( self._export_type, serialisable_file_search_context, self._period, self._phrase, self._last_checked )
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
( self._export_type, serialisable_file_search_context, self._period, self._phrase, self._last_checked ) = serialisable_info
self._file_search_context = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_file_search_context )
def DoWork( self ):
if HydrusData.TimeHasPassed( self._last_checked + self._period ):
folder_path = HydrusData.ToUnicode( self._name )
if os.path.exists( folder_path ) and os.path.isdir( folder_path ):
query_hash_ids = HydrusGlobals.client_controller.Read( 'file_query_ids', self._file_search_context )
media_results = []
i = 0
base = 256
while i < len( query_hash_ids ):
if HC.options[ 'pause_export_folders_sync' ]:
return
if i == 0: ( last_i, i ) = ( 0, base )
else: ( last_i, i ) = ( i, i + base )
sub_query_hash_ids = query_hash_ids[ last_i : i ]
more_media_results = HydrusGlobals.client_controller.Read( 'media_results_from_ids', sub_query_hash_ids )
media_results.extend( more_media_results )
#
terms = ParseExportPhrase( self._phrase )
previous_filenames = set( os.listdir( folder_path ) )
sync_filenames = set()
client_files_manager = HydrusGlobals.client_controller.GetClientFilesManager()
num_copied = 0
for media_result in media_results:
hash = media_result.GetHash()
mime = media_result.GetMime()
size = media_result.GetSize()
source_path = client_files_manager.GetFilePath( hash, mime )
filename = GenerateExportFilename( media_result, terms )
dest_path = os.path.join( folder_path, filename )
if filename not in sync_filenames:
copied = HydrusPaths.MirrorFile( source_path, dest_path )
if copied:
num_copied += 1
try: os.chmod( dest_path, stat.S_IWRITE | stat.S_IREAD )
except: pass
sync_filenames.add( filename )
if num_copied > 0:
HydrusData.Print( 'Export folder ' + self._name + ' exported ' + HydrusData.ConvertIntToPrettyString( num_copied ) + ' files.' )
if self._export_type == HC.EXPORT_FOLDER_TYPE_SYNCHRONISE:
deletee_filenames = previous_filenames.difference( sync_filenames )
for deletee_filename in deletee_filenames:
deletee_path = os.path.join( folder_path, deletee_filename )
ClientData.DeletePath( deletee_path )
if len( deletee_filenames ) > 0:
HydrusData.Print( 'Export folder ' + self._name + ' deleted ' + HydrusData.ConvertIntToPrettyString( len( deletee_filenames ) ) + ' files.' )
self._last_checked = HydrusData.GetNow()
HydrusGlobals.client_controller.WriteSynchronous( 'serialisable', self )
def ToTuple( self ):
return ( self._name, self._export_type, self._file_search_context, self._period, self._phrase )
def SetTuple( self, folder_path, export_type, file_search_context, period, phrase ):
self._name = folder_path
self._export_type = export_type
self._file_search_context = file_search_context
self._period = period
self._phrase = phrase
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_EXPORT_FOLDER ] = ExportFolder

View File

@ -5,6 +5,7 @@ import ClientCaches
import ClientFiles
import ClientData
import ClientDragDrop
import ClientExporting
import ClientGUICommon
import ClientGUIDialogs
import ClientGUIDialogsManage
@ -1109,24 +1110,38 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
def database():
ClientGUIMenus.AppendMenuItem( menu, 'set a password', 'Set a simple password for the database so only you can open it in the client.', self, self._SetPassword )
menu.AppendSeparator()
ClientGUIMenus.AppendMenuItem( menu, 'create a database backup', 'Back the database up to an external location.', self, self._controller.BackupDatabase )
ClientGUIMenus.AppendMenuItem( menu, 'restore a database backup', 'Restore the database from an external location.', self, self._controller.RestoreDatabase )
menu.AppendSeparator()
submenu = wx.Menu()
ClientGUIMenus.AppendMenuItem( submenu, 'vacuum', 'Defrag the database by completely rebuilding it.', self, self._VacuumDatabase )
ClientGUIMenus.AppendMenuItem( submenu, 'analyze', 'Optimise slow queries by running statistical analyses on the database.', self, self._AnalyzeDatabase )
ClientGUIMenus.AppendMenuItem( submenu, 'similar files search data', 'Rebalance and update the data the database uses to find similar files.', self, self._MaintainSimilarFilesData )
ClientGUIMenus.AppendMenuItem( submenu, 'rebalance file storage', 'Move your files around your chosen storage directories until they satisfy the weights you have set in the options.', self, self._RebalanceClientFiles )
ClientGUIMenus.AppendMenuItem( submenu, 'regenerate autocomplete cache', 'Delete and recreate the tag autocomplete cache, fixing any miscounts.', self, self._RegenerateACCache )
ClientGUIMenus.AppendMenuItem( submenu, 'regenerate similar files search data', 'Delete and recreate the similar files search tree.', self, self._RegenerateSimilarFilesData )
ClientGUIMenus.AppendMenuItem( submenu, 'regenerate thumbnails', 'Delete all thumbnails and regenerate them from their original files.', self, self._RegenerateThumbnails )
ClientGUIMenus.AppendMenuItem( submenu, 'check database integrity', 'Have the database examine all its records for internal consistency.', self, self._CheckDBIntegrity )
ClientGUIMenus.AppendMenuItem( submenu, 'check file integrity', 'Have the database check if it truly has the files it thinks it does, and remove records when not.', self, self._CheckFileIntegrity )
ClientGUIMenus.AppendMenuItem( submenu, 'clear orphans', 'Clear out surplus files that have found their way into the file structure.', self, self._ClearOrphans )
ClientGUIMenus.AppendMenu( menu, submenu, 'maintenance' )
ClientGUIMenus.AppendMenu( menu, submenu, 'maintain' )
submenu = wx.Menu()
ClientGUIMenus.AppendMenuItem( submenu, 'database integrity', 'Have the database examine all its records for internal consistency.', self, self._CheckDBIntegrity )
ClientGUIMenus.AppendMenuItem( submenu, 'file integrity', 'Have the database check if it truly has the files it thinks it does, and remove records when not.', self, self._CheckFileIntegrity )
ClientGUIMenus.AppendMenu( menu, submenu, 'check' )
submenu = wx.Menu()
ClientGUIMenus.AppendMenuItem( submenu, 'autocomplete cache', 'Delete and recreate the tag autocomplete cache, fixing any miscounts.', self, self._RegenerateACCache )
ClientGUIMenus.AppendMenuItem( submenu, 'similar files search data', 'Delete and recreate the similar files search tree.', self, self._RegenerateSimilarFilesData )
ClientGUIMenus.AppendMenuItem( submenu, 'all thumbnails', 'Delete all thumbnails and regenerate them from their original files.', self, self._RegenerateThumbnails )
ClientGUIMenus.AppendMenu( menu, submenu, 'regenerate' )
return ( menu, p( '&Database' ), True )
@ -1497,6 +1512,27 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
self._controller.CallToThread( do_it )
def _MaintainSimilarFilesData( self ):
text = 'This will do up to ten minutes\' maintenance on the similar files search data.'
text += os.linesep * 2
text += 'It will rebalance the search tree and (re)generate any outstanding file search data.'
text += os.linesep * 2
text += 'If there is work to do, it will report its status through a popup message. The gui may hang until it is done.'
with ClientGUIDialogs.DialogYesNo( self, text ) as dlg:
result = dlg.ShowModal()
if result == wx.ID_YES:
stop_time = HydrusData.GetNow() + 60 * 10
self._controller.Write( 'maintain_similar_files', stop_time )
def _ManageAccountTypes( self, service_key ):
with ClientGUIDialogsManage.DialogManageAccountTypes( self, service_key ) as dlg: dlg.ShowModal()
@ -1753,7 +1789,7 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
def _OpenExportFolder( self ):
export_path = ClientFiles.GetExportPath()
export_path = ClientExporting.GetExportPath()
HydrusPaths.LaunchDirectory( export_path )
@ -2869,9 +2905,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
def NewSimilarTo( self, file_service_key, hash ):
hamming_distance = HC.options[ 'file_system_predicates' ][ 'hamming_distance' ]
def NewSimilarTo( self, file_service_key, hash, hamming_distance ):
initial_predicates = [ ClientSearch.Predicate( HC.PREDICATE_TYPE_SYSTEM_SIMILAR_TO, ( hash, hamming_distance ) ) ]

View File

@ -231,6 +231,24 @@ class BetterChoice( wx.Choice ):
class BetterRadioBox( wx.RadioBox ):
def __init__( self, *args, **kwargs ):
self._indices_to_data = { i : data for ( i, ( s, data ) ) in enumerate( kwargs[ 'choices' ] ) }
kwargs[ 'choices' ] = [ s for ( s, data ) in kwargs[ 'choices' ] ]
wx.RadioBox.__init__( self, *args, **kwargs )
def GetChoice( self ):
index = self.GetSelection()
return self._indices_to_data[ index ]
class BufferedWindow( wx.Window ):
def __init__( self, *args, **kwargs ):
@ -3484,16 +3502,6 @@ class PopupMessage( PopupWindow ):
self._tb_text.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
self._tb_text.Hide()
self._show_caller_tb_button = wx.Button( self, label = 'show caller traceback' )
self._show_caller_tb_button.Bind( wx.EVT_BUTTON, self.EventShowCallerTBButton )
self._show_caller_tb_button.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
self._show_caller_tb_button.Hide()
self._caller_tb_text = FitResistantStaticText( self )
self._caller_tb_text.Wrap( 380 )
self._caller_tb_text.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
self._caller_tb_text.Hide()
self._copy_tb_button = wx.Button( self, label = 'copy traceback information' )
self._copy_tb_button.Bind( wx.EVT_BUTTON, self.EventCopyTBButton )
self._copy_tb_button.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
@ -3523,8 +3531,6 @@ class PopupMessage( PopupWindow ):
vbox.AddF( self._show_files_button, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( self._show_tb_button, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( self._tb_text, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( self._show_caller_tb_button, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( self._caller_tb_text, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( self._copy_tb_button, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( hbox, CC.FLAGS_BUTTON_SIZER )
@ -3586,24 +3592,6 @@ class PopupMessage( PopupWindow ):
def EventShowCallerTBButton( self, event ):
if self._caller_tb_text.IsShown():
self._show_caller_tb_button.SetLabelText( 'show caller traceback' )
self._caller_tb_text.Hide()
else:
self._show_caller_tb_button.SetLabelText( 'hide caller traceback' )
self._caller_tb_text.Show()
self.GetParent().MakeSureEverythingFits()
def EventShowFilesButton( self, event ):
result = self._job_key.GetIfHasVariable( 'popup_files' )
@ -3805,9 +3793,8 @@ class PopupMessage( PopupWindow ):
popup_traceback = self._job_key.GetIfHasVariable( 'popup_traceback' )
popup_caller_traceback = self._job_key.GetIfHasVariable( 'popup_traceback' )
if popup_traceback is not None or popup_caller_traceback is not None:
if popup_traceback is not None:
self._copy_tb_button.Show()
@ -3833,23 +3820,6 @@ class PopupMessage( PopupWindow ):
self._tb_text.Hide()
if popup_caller_traceback is not None:
text = popup_caller_traceback
if self._caller_tb_text.GetLabelText() != text:
self._caller_tb_text.SetLabelText( self._ProcessText( HydrusData.ToUnicode( text ) ) )
self._show_caller_tb_button.Show()
else:
self._show_caller_tb_button.Hide()
self._caller_tb_text.Hide()
if self._job_key.IsPausable():
self._pause_button.Show()
@ -3959,6 +3929,25 @@ class PopupMessageManager( wx.Frame ):
def _DisplayingError( self ):
sizer_items = self._message_vbox.GetChildren()
for sizer_item in sizer_items:
message_window = sizer_item.GetWindow()
job_key = message_window.GetJobKey()
if job_key.HasVariable( 'popup_traceback' ):
return True
return False
def _SizeAndPositionAndShow( self ):
try:
@ -3980,12 +3969,12 @@ class PopupMessageManager( wx.Frame ):
current_focus_tlp = wx.GetTopLevelParent( wx.Window.FindFocus() )
gui_is_active = current_focus_tlp in ( self, parent )
if new_options.GetBoolean( 'hide_message_manager_on_gui_deactive' ):
current_focus_tlp = wx.GetTopLevelParent( wx.Window.FindFocus() )
gui_is_active = current_focus_tlp in ( self, parent )
if gui_is_active:
# gui can have focus even while minimised to the taskbar--let's not show in this case
@ -4031,8 +4020,14 @@ class PopupMessageManager( wx.Frame ):
self.SetPosition( parent.ClientToScreenXY( my_x, my_y ) )
if not going_to_bug_out_at_hide_or_show:
# Unhiding tends to raise the main gui tlp, which is annoying if a media viewer window has focus
show_is_not_annoying = gui_is_active or self._DisplayingError()
ok_to_show = show_is_not_annoying and not going_to_bug_out_at_hide_or_show
if ok_to_show:
was_hidden = not self.IsShown()
@ -4148,7 +4143,10 @@ class PopupMessageManager( wx.Frame ):
event.Skip()
def MakeSureEverythingFits( self ): self._SizeAndPositionAndShow()
def MakeSureEverythingFits( self ):
self._SizeAndPositionAndShow()
def TIMEREvent( self, event ):
@ -4890,7 +4888,7 @@ class SaneMultilineTextCtrl( wx.TextCtrl ):
class SaneListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin, ColumnSorterMixin ):
def __init__( self, parent, height, columns, delete_key_callback = None, activation_callback = None, use_display_tuple_for_sort = False ):
def __init__( self, parent, height, columns, delete_key_callback = None, activation_callback = None ):
num_columns = len( columns )
@ -4899,9 +4897,9 @@ class SaneListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin, ColumnSorterMixin ):
ColumnSorterMixin.__init__( self, num_columns )
self.itemDataMap = {}
self._data_indices_to_sort_indices = {}
self._data_indices_to_sort_indices_dirty = False
self._next_data_index = 0
self._use_display_tuple_for_sort = use_display_tuple_for_sort
self._custom_client_data = {}
resize_column = 1
@ -4928,26 +4926,48 @@ class SaneListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin, ColumnSorterMixin ):
self.Bind( wx.EVT_LIST_COL_BEGIN_DRAG, self.EventBeginColDrag )
def Append( self, display_tuple, client_data ):
_GetDataIndex = wx.ListCtrl.GetItemData
def _GetIndexFromDataIndex( self, data_index ):
if self._data_indices_to_sort_indices_dirty:
self._data_indices_to_sort_indices = { self._GetDataIndex( index ) : index for index in range( self.GetItemCount() ) }
self._data_indices_to_sort_indices_dirty = False
try:
return self._data_indices_to_sort_indices[ data_index ]
except KeyError:
raise HydrusExceptions.DataMissing( 'Data not found!' )
def Append( self, display_tuple, sort_tuple ):
index = wx.ListCtrl.Append( self, display_tuple )
self.SetItemData( index, self._next_data_index )
data_index = self._next_data_index
if self._use_display_tuple_for_sort:
self.itemDataMap[ self._next_data_index ] = list( display_tuple )
self._custom_client_data[ self._next_data_index ] = client_data
else:
self.itemDataMap[ self._next_data_index ] = list( client_data )
self.SetItemData( index, data_index )
self.itemDataMap[ data_index ] = list( sort_tuple )
self._data_indices_to_sort_indices[ data_index ] = index
self._next_data_index += 1
def DeleteItem( self, *args, **kwargs ):
wx.ListCtrl.DeleteItem( self, *args, **kwargs )
self._data_indices_to_sort_indices_dirty = True
def EventBeginColDrag( self, event ):
# resizeCol is not zero-indexed
@ -5022,31 +5042,17 @@ class SaneListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin, ColumnSorterMixin ):
if index is None:
data_indicies = [ self.GetItemData( index ) for index in range( self.GetItemCount() ) ]
data_indicies = [ self._GetDataIndex( index ) for index in range( self.GetItemCount() ) ]
if self._use_display_tuple_for_sort:
datas = [ self._custom_client_data[ data_index ] for data_index in data_indicies ]
else:
datas = [ tuple( self.itemDataMap[ data_index ] ) for data_index in data_indicies ]
datas = [ tuple( self.itemDataMap[ data_index ] ) for data_index in data_indicies ]
return datas
else:
data_index = self.GetItemData( index )
data_index = self._GetDataIndex( index )
if self._use_display_tuple_for_sort:
return self._custom_client_data[ data_index ]
else:
return tuple( self.itemDataMap[ data_index ] )
return tuple( self.itemDataMap[ data_index ] )
@ -5065,7 +5071,10 @@ class SaneListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin, ColumnSorterMixin ):
comparison_data = client_data[ column_index ]
if comparison_data == data: return index
if comparison_data == data:
return index
raise HydrusExceptions.DataMissing( 'Data not found!' )
@ -5092,7 +5101,10 @@ class SaneListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin, ColumnSorterMixin ):
def GetListCtrl( self ): return self
def GetListCtrl( self ):
return self
def GetSelectedClientData( self ):
@ -5108,13 +5120,21 @@ class SaneListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin, ColumnSorterMixin ):
return results
def OnSortOrderChanged( self ):
self._data_indices_to_sort_indices_dirty = True
def RemoveAllSelected( self ):
indices = self.GetAllSelected()
indices.reverse() # so we don't screw with the indices of deletees below
for index in indices: self.DeleteItem( index )
for index in indices:
self.DeleteItem( index )
def SelectAll( self ):
@ -5129,16 +5149,7 @@ class SaneListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin, ColumnSorterMixin ):
def UpdateValue( self, index, column, display_value, data_value ):
self.SetStringItem( index, column, display_value )
data_index = self.GetItemData( index )
self.itemDataMap[ data_index ][ column ] = data_value
def UpdateRow( self, index, display_tuple, client_data ):
def UpdateRow( self, index, display_tuple, sort_tuple ):
column = 0
@ -5149,34 +5160,129 @@ class SaneListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin, ColumnSorterMixin ):
column += 1
data_index = self.GetItemData( index )
data_index = self._GetDataIndex( index )
if self._use_display_tuple_for_sort:
self.itemDataMap[ data_index ] = list( sort_tuple )
class SaneListCtrlForSingleObject( SaneListCtrl ):
def __init__( self, *args, **kwargs ):
# this could one day just take column parameters that the user can pick
# it could just take obj in append or whatever and generate column tuples off that
self._data_indices_to_objects = {}
self._objects_to_data_indices = {}
SaneListCtrl.__init__( self, *args, **kwargs )
def Append( self, display_tuple, sort_tuple, obj ):
self._data_indices_to_objects[ self._next_data_index ] = obj
self._objects_to_data_indices[ obj ] = self._next_data_index
SaneListCtrl.Append( self, display_tuple, sort_tuple )
def GetClientData( self, index = None ):
if index is None:
self.itemDataMap[ data_index ] = list( display_tuple )
data_indicies = [ self._GetDataIndex( index ) for index in range( self.GetItemCount() ) ]
self._custom_client_data[ data_index ] = client_data
datas = [ self._data_indices_to_objects[ data_index ] for data_index in data_indicies ]
return datas
else:
self.itemDataMap[ data_index ] = list( client_data )
data_index = self._GetDataIndex( index )
return self._data_indices_to_objects[ data_index ]
class SeedCacheControl( SaneListCtrl ):
def GetIndexFromClientData( self, obj ):
try:
data_index = self._objects_to_data_indices[ obj ]
index = self._GetIndexFromDataIndex( data_index )
return index
except KeyError:
raise HydrusExceptions.DataMissing( 'Data not found!' )
def HasClientData( self, data ):
try:
index = self.GetIndexFromClientData( data )
return True
except HydrusExceptions.DataMissing:
return False
def SetNonDupeName( self, obj ):
# when column population is handled here, we can tuck this into normal append/update calls internally
name = obj.GetName()
current_names = { obj.GetName() for obj in self.GetClientData() }
if name in current_names:
i = 1
original_name = name
while name in current_names:
name = original_name + ' (' + str( i ) + ')'
i += 1
obj.SetName( name )
def UpdateRow( self, index, display_tuple, sort_tuple, obj ):
SaneListCtrl.UpdateRow( self, index, display_tuple, sort_tuple )
data_index = self._GetDataIndex( index )
self._data_indices_to_objects[ data_index ] = obj
self._objects_to_data_indices[ obj ] = data_index
class SeedCacheControl( SaneListCtrlForSingleObject ):
def __init__( self, parent, seed_cache ):
height = 300
columns = [ ( 'source', -1 ), ( 'status', 90 ), ( 'added', 150 ), ( 'last modified', 150 ), ( 'note', 200 ) ]
SaneListCtrl.__init__( self, parent, height, columns )
SaneListCtrlForSingleObject.__init__( self, parent, height, columns )
self._seed_cache = seed_cache
for info_tuple in self._seed_cache.GetSeedsWithInfo():
for seed in self._seed_cache.GetSeeds():
self._AddSeed( info_tuple )
self._AddSeed( seed )
self.Bind( wx.EVT_MENU, self.EventMenu )
@ -5185,16 +5291,20 @@ class SeedCacheControl( SaneListCtrl ):
HydrusGlobals.client_controller.sub( self, 'NotifySeedUpdated', 'seed_cache_seed_updated' )
def _AddSeed( self, info_tuple ):
def _AddSeed( self, seed ):
pretty_tuple = self._GetPrettyTuple( info_tuple )
sort_tuple = self._seed_cache.GetSeedInfo( seed )
self.Append( pretty_tuple, info_tuple )
( display_tuple, sort_tuple ) = self._GetListCtrlTuples( seed )
self.Append( display_tuple, sort_tuple, seed )
def _GetPrettyTuple( self, info_tuple ):
def _GetListCtrlTuples( self, seed ):
( seed, status, added_timestamp, last_modified_timestamp, note ) = info_tuple
sort_tuple = self._seed_cache.GetSeedInfo( seed )
( seed, status, added_timestamp, last_modified_timestamp, note ) = sort_tuple
pretty_seed = HydrusData.ToUnicode( seed )
pretty_status = CC.status_string_lookup[ status ]
@ -5202,14 +5312,18 @@ class SeedCacheControl( SaneListCtrl ):
pretty_modified = HydrusData.ConvertTimestampToPrettyAgo( last_modified_timestamp )
pretty_note = note.split( os.linesep )[0]
return ( pretty_seed, pretty_status, pretty_added, pretty_modified, pretty_note )
display_tuple = ( pretty_seed, pretty_status, pretty_added, pretty_modified, pretty_note )
return ( display_tuple, sort_tuple )
def _CopySelectedNotes( self ):
notes = []
for ( seed, status, added_timestamp, last_modified_timestamp, note ) in self.GetSelectedClientData():
for seed in self.GetSelectedClientData():
( seed, status, added_timestamp, last_modified_timestamp, note ) = self._seed_cache.GetSeedInfo( seed )
if note != '':
@ -5229,7 +5343,7 @@ class SeedCacheControl( SaneListCtrl ):
def _CopySelectedSeeds( self ):
seeds = [ seed for ( seed, status, added_timestamp, last_modified_timestamp, note ) in self.GetSelectedClientData() ]
seeds = self.GetSelectedClientData()
if len( seeds ) > 0:
@ -5243,15 +5357,7 @@ class SeedCacheControl( SaneListCtrl ):
def _SetSelected( self, status_to_set ):
seeds_to_reset = set()
for ( seed, status, added_timestamp, last_modified_timestamp, note ) in self.GetSelectedClientData():
if status != status_to_set:
seeds_to_reset.add( seed )
seeds_to_reset = self.GetSelectedClientData()
for seed in seeds_to_reset:
@ -5290,38 +5396,28 @@ class SeedCacheControl( SaneListCtrl ):
HydrusGlobals.client_controller.PopupMenu( self, menu )
def NotifySeedAdded( self, seed ):
if self._seed_cache.HasSeed( seed ):
info_tuple = self._seed_cache
def NotifySeedUpdated( self, seed ):
if self._seed_cache.HasSeed( seed ):
info_tuple = self._seed_cache.GetSeedInfo( seed )
if self.HasClientData( seed, 0 ):
if self.HasClientData( seed ):
index = self.GetIndexFromClientData( seed, 0 )
index = self.GetIndexFromClientData( seed )
pretty_tuple = self._GetPrettyTuple( info_tuple )
( display_tuple, sort_tuple ) = self._GetListCtrlTuples( seed )
self.UpdateRow( index, pretty_tuple, info_tuple )
self.UpdateRow( index, display_tuple, sort_tuple, seed )
else:
self._AddSeed( info_tuple )
self._AddSeed( seed )
else:
if self.HasClientData( seed, 0 ):
if self.HasClientData( seed ):
index = self.GetIndexFromClientData( seed, 0 )
index = self.GetIndexFromClientData( seed )
self.DeleteItem( index )

View File

@ -4,6 +4,7 @@ import ClientData
import ClientDefaults
import ClientDownloading
import ClientDragDrop
import ClientExporting
import ClientCaches
import ClientFiles
import ClientGUIACDropdown
@ -4364,13 +4365,13 @@ class DialogSetupExport( Dialog ):
mime = media.GetMime()
pretty_tuple = ( str( i + 1 ), HC.mime_string_lookup[ mime ], '' )
data_tuple = ( ( i, media ), mime, '' )
display_tuple = ( str( i + 1 ), HC.mime_string_lookup[ mime ], '' )
sort_tuple = ( ( i, media ), mime, '' )
self._paths.Append( pretty_tuple, data_tuple )
self._paths.Append( display_tuple, sort_tuple )
export_path = ClientFiles.GetExportPath()
export_path = ClientExporting.GetExportPath()
self._directory_picker.SetPath( export_path )
@ -4426,7 +4427,7 @@ class DialogSetupExport( Dialog ):
directory = HydrusData.ToUnicode( self._directory_picker.GetPath() )
filename = ClientFiles.GenerateExportFilename( media, terms )
filename = ClientExporting.GenerateExportFilename( media, terms )
return os.path.join( directory, filename )
@ -4435,7 +4436,7 @@ class DialogSetupExport( Dialog ):
pattern = self._pattern.GetValue()
terms = ClientFiles.ParseExportPhrase( pattern )
terms = ClientExporting.ParseExportPhrase( pattern )
all_paths = set()
@ -4482,7 +4483,7 @@ class DialogSetupExport( Dialog ):
pattern = self._pattern.GetValue()
terms = ClientFiles.ParseExportPhrase( pattern )
terms = ClientExporting.ParseExportPhrase( pattern )
client_files_manager = HydrusGlobals.client_controller.GetClientFilesManager()
@ -4508,7 +4509,7 @@ class DialogSetupExport( Dialog ):
tags = tags_manager.GetCurrent()
filename = ClientFiles.GenerateExportFilename( media, terms )
filename = ClientExporting.GenerateExportFilename( media, terms )
txt_path = os.path.join( directory, filename + '.txt' )
@ -4877,9 +4878,9 @@ class DialogShortcuts( Dialog ):
if dlg.ShowModal() == wx.ID_OK:
( pretty_tuple, data_tuple ) = dlg.GetInfo()
( pretty_tuple, sort_tuple ) = dlg.GetInfo()
self._shortcuts.UpdateRow( index, pretty_tuple, data_tuple )
self._shortcuts.UpdateRow( index, pretty_tuple, sort_tuple )
self._SortListCtrl()
@ -4893,9 +4894,9 @@ class DialogShortcuts( Dialog ):
if dlg.ShowModal() == wx.ID_OK:
( pretty_tuple, data_tuple ) = dlg.GetInfo()
( pretty_tuple, sort_tuple ) = dlg.GetInfo()
self._shortcuts.Append( pretty_tuple, data_tuple )
self._shortcuts.Append( pretty_tuple, sort_tuple )
self._SortListCtrl()

View File

@ -3,6 +3,7 @@ import ClientConstants as CC
import ClientData
import ClientDefaults
import ClientDragDrop
import ClientExporting
import ClientFiles
import ClientGUIACDropdown
import ClientGUICollapsible
@ -1390,21 +1391,17 @@ class DialogManageExportFolders( ClientGUIDialogs.Dialog ):
ClientGUIDialogs.Dialog.__init__( self, parent, 'manage export folders' )
self._export_folders = ClientGUICommon.SaneListCtrl( self, 120, [ ( 'path', -1 ), ( 'type', 120 ), ( 'query', 120 ), ( 'period', 120 ), ( 'phrase', 120 ) ], delete_key_callback = self.Delete, activation_callback = self.Edit, use_display_tuple_for_sort = True )
self._export_folders = ClientGUICommon.SaneListCtrlForSingleObject( self, 120, [ ( 'name', 120 ), ( 'path', -1 ), ( 'type', 120 ), ( 'query', 120 ), ( 'period', 120 ), ( 'phrase', 120 ) ], delete_key_callback = self.Delete, activation_callback = self.Edit )
export_folders = HydrusGlobals.client_controller.Read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_EXPORT_FOLDER )
self._original_paths = []
for export_folder in export_folders:
path = export_folder.GetName()
self._original_paths.append( path )
( display_tuple, sort_tuple ) = self._ConvertExportFolderToTuples( export_folder )
( pretty_path, pretty_export_type, pretty_file_search_context, pretty_period, pretty_phrase ) = self._GetPrettyVariables( export_folder )
self._export_folders.Append( ( pretty_path, pretty_export_type, pretty_file_search_context, pretty_period, pretty_phrase ), export_folder )
self._export_folders.Append( display_tuple, sort_tuple, export_folder )
self._add_button = wx.Button( self, label = 'add' )
@ -1455,51 +1452,16 @@ class DialogManageExportFolders( ClientGUIDialogs.Dialog ):
wx.CallAfter( self._ok.SetFocus )
def _AddFolder( self, path ):
export_folders = self._export_folders.GetClientData()
for export_folder in export_folders:
existing_path = export_folder.GetName()
test_path = os.path.join( path, '' )
test_existing_path = os.path.join( existing_path, '' )
if test_path == test_existing_path:
text = 'That directory already exists as an export folder--at current, there can only be one export folder per destination.'
wx.MessageBox( text )
return
if test_path.startswith( test_existing_path ):
text = 'You have entered a subdirectory of an existing path--at current, this is not permitted.'
wx.MessageBox( text )
return
if test_existing_path.startswith( test_path ):
text = 'You have entered a parent directory of an existing path--at current, this is not permitted.'
wx.MessageBox( text )
return
def _AddFolder( self ):
name = 'export folder'
path = ''
export_type = HC.EXPORT_FOLDER_TYPE_REGULAR
file_search_context = ClientSearch.FileSearchContext( file_service_key = CC.LOCAL_FILE_SERVICE_KEY )
period = 15 * 60
phrase = '{hash}'
export_folder = ClientFiles.ExportFolder( path, export_type = export_type, file_search_context = file_search_context, period = period, phrase = phrase )
export_folder = ClientExporting.ExportFolder( name, path, export_type = export_type, file_search_context = file_search_context, period = period, phrase = phrase )
with DialogManageExportFoldersEdit( self, export_folder ) as dlg:
@ -1507,16 +1469,18 @@ class DialogManageExportFolders( ClientGUIDialogs.Dialog ):
export_folder = dlg.GetInfo()
( pretty_path, pretty_export_type, pretty_file_search_context, pretty_period, pretty_phrase ) = self._GetPrettyVariables( export_folder )
self._export_folders.SetNonDupeName( export_folder )
self._export_folders.Append( ( pretty_path, pretty_export_type, pretty_file_search_context, pretty_period, pretty_phrase ), export_folder )
( display_tuple, sort_tuple ) = self._ConvertExportFolderToTuples( export_folder )
self._export_folders.Append( display_tuple, sort_tuple, export_folder )
def _GetPrettyVariables( self, export_folder ):
def _ConvertExportFolderToTuples( self, export_folder ):
( path, export_type, file_search_context, period, phrase ) = export_folder.ToTuple()
( name, path, export_type, file_search_context, period, phrase ) = export_folder.ToTuple()
if export_type == HC.EXPORT_FOLDER_TYPE_REGULAR:
@ -1533,7 +1497,11 @@ class DialogManageExportFolders( ClientGUIDialogs.Dialog ):
pretty_phrase = phrase
return ( path, pretty_export_type, pretty_file_search_context, pretty_period, pretty_phrase )
display_tuple = ( name, path, pretty_export_type, pretty_file_search_context, pretty_period, pretty_phrase )
sort_tuple = ( name, path, pretty_export_type, pretty_file_search_context, period, phrase )
return ( display_tuple, sort_tuple )
def Delete( self ):
@ -1549,15 +1517,22 @@ class DialogManageExportFolders( ClientGUIDialogs.Dialog ):
export_folder = self._export_folders.GetClientData( index )
original_name = export_folder.GetName()
with DialogManageExportFoldersEdit( self, export_folder ) as dlg:
if dlg.ShowModal() == wx.ID_OK:
export_folder = dlg.GetInfo()
( pretty_path, pretty_export_type, pretty_file_search_context, pretty_period, pretty_phrase ) = self._GetPrettyVariables( export_folder )
if export_folder.GetName() != original_name:
self._export_folders.SetNonDupeName( export_folder )
self._export_folders.UpdateRow( index, ( pretty_path, pretty_export_type, pretty_file_search_context, pretty_period, pretty_phrase ), export_folder )
( display_tuple, sort_tuple ) = self._ConvertExportFolderToTuples( export_folder )
self._export_folders.UpdateRow( index, display_tuple, sort_tuple, export_folder )
@ -1565,15 +1540,7 @@ class DialogManageExportFolders( ClientGUIDialogs.Dialog ):
def EventAdd( self, event ):
with wx.DirDialog( self, 'Select a folder to add.' ) as dlg:
if dlg.ShowModal() == wx.ID_OK:
path = HydrusData.ToUnicode( dlg.GetPath() )
self._AddFolder( path )
self._AddFolder()
def EventDelete( self, event ):
@ -1588,26 +1555,24 @@ class DialogManageExportFolders( ClientGUIDialogs.Dialog ):
def EventOK( self, event ):
client_data = self._export_folders.GetClientData()
existing_db_names = set( HydrusGlobals.client_controller.Read( 'serialisable_names', HydrusSerialisable.SERIALISABLE_TYPE_EXPORT_FOLDER ) )
export_folders = []
export_folders = self._export_folders.GetClientData()
paths_set = set()
good_names = set()
for export_folder in client_data:
for export_folder in export_folders:
HydrusGlobals.client_controller.Write( 'serialisable', export_folder )
path = export_folder.GetName()
paths_set.add( path )
good_names.add( export_folder.GetName() )
deletees = set( self._original_paths ) - paths_set
names_to_delete = existing_db_names - good_names
for deletee in deletees:
for name in names_to_delete:
HydrusGlobals.client_controller.Write( 'delete_serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_EXPORT_FOLDER, deletee )
HydrusGlobals.client_controller.Write( 'delete_serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_EXPORT_FOLDER, name )
HydrusGlobals.client_controller.pub( 'notify_new_export_folders' )
@ -1623,24 +1588,22 @@ class DialogManageExportFoldersEdit( ClientGUIDialogs.Dialog ):
self._export_folder = export_folder
( path, export_type, file_search_context, period, phrase ) = self._export_folder.ToTuple()
( name, path, export_type, file_search_context, period, phrase ) = self._export_folder.ToTuple()
self._path_box = ClientGUICommon.StaticBox( self, 'export path' )
self._path_box = ClientGUICommon.StaticBox( self, 'name and location' )
self._name = wx.TextCtrl( self._path_box)
self._path = wx.DirPickerCtrl( self._path_box, style = wx.DIRP_USE_TEXTCTRL )
self._path.SetPath( path )
#
self._type_box = ClientGUICommon.StaticBox( self, 'type of export folder' )
self._type_box = ClientGUICommon.StaticBox( self, 'type of export' )
self._type = ClientGUICommon.BetterChoice( self._type_box )
self._type.Append( 'regular', HC.EXPORT_FOLDER_TYPE_REGULAR )
self._type.Append( 'synchronise', HC.EXPORT_FOLDER_TYPE_SYNCHRONISE )
self._type.SelectClientData( export_type )
#
self._query_box = ClientGUICommon.StaticBox( self, 'query to export' )
@ -1659,16 +1622,12 @@ class DialogManageExportFoldersEdit( ClientGUIDialogs.Dialog ):
self._period = ClientGUICommon.TimeDeltaButton( self._period_box, min = 3 * 60, days = True, hours = True, minutes = True )
self._period.SetValue( period )
#
self._phrase_box = ClientGUICommon.StaticBox( self, 'filenames' )
self._pattern = wx.TextCtrl( self._phrase_box )
self._pattern.SetValue( phrase )
self._examples = ClientGUICommon.ExportPatternButton( self._phrase_box )
#
@ -1682,7 +1641,28 @@ class DialogManageExportFoldersEdit( ClientGUIDialogs.Dialog ):
#
self._path_box.AddF( self._path, CC.FLAGS_EXPAND_PERPENDICULAR )
self._name.SetValue( name )
self._path.SetPath( path )
self._type.SelectClientData( export_type )
self._period.SetValue( period )
self._pattern.SetValue( phrase )
#
rows = []
rows.append( ( 'name: ', self._name ) )
rows.append( ( 'folder path: ', self._path ) )
gridbox = ClientGUICommon.WrapInGrid( self._path_box, rows )
self._path_box.AddF( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
#
text = '''regular - try to export the files to the directory, overwriting if the filesize if different
@ -1734,9 +1714,19 @@ If you select synchronise, be careful!'''
def EventOK( self, event ):
if self._path.GetPath() in ( '', None ):
wx.MessageBox( 'You must enter a folder path to export to!' )
return
phrase = self._pattern.GetValue()
try: ClientFiles.ParseExportPhrase( phrase )
try:
ClientExporting.ParseExportPhrase( phrase )
except:
wx.MessageBox( 'Could not parse that export phrase!' )
@ -1749,6 +1739,8 @@ If you select synchronise, be careful!'''
def GetInfo( self ):
name = self._name.GetValue()
path = HydrusData.ToUnicode( self._path.GetPath() )
export_type = self._type.GetChoice()
@ -1763,9 +1755,9 @@ If you select synchronise, be careful!'''
phrase = self._pattern.GetValue()
self._export_folder.SetTuple( path, export_type, file_search_context, period, phrase )
export_folder = ClientExporting.ExportFolder( name, path, export_type, file_search_context, period, phrase )
return self._export_folder
return export_folder
'''
class DialogManageImageboards( ClientGUIDialogs.Dialog ):
@ -2492,7 +2484,7 @@ class DialogManageImportFolders( ClientGUIDialogs.Dialog ):
ClientGUIDialogs.Dialog.__init__( self, parent, 'manage import folders' )
self._import_folders = ClientGUICommon.SaneListCtrl( self, 120, [ ( 'name', 120 ), ( 'path', -1 ), ( 'check period', 120 ) ], delete_key_callback = self.Delete, activation_callback = self.Edit )
self._import_folders = ClientGUICommon.SaneListCtrlForSingleObject( self, 120, [ ( 'name', 120 ), ( 'path', -1 ), ( 'check period', 120 ) ], delete_key_callback = self.Delete, activation_callback = self.Edit )
self._add_button = wx.Button( self, label = 'add' )
self._add_button.Bind( wx.EVT_BUTTON, self.EventAdd )
@ -2512,19 +2504,13 @@ class DialogManageImportFolders( ClientGUIDialogs.Dialog ):
#
self._names_to_import_folders = {}
import_folders = HydrusGlobals.client_controller.Read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_IMPORT_FOLDER )
for import_folder in import_folders:
( name, path, check_period ) = import_folder.ToListBoxTuple()
( display_tuple, sort_tuple ) = self._ConvertImportFolderToTuples( import_folder )
pretty_check_period = self._GetPrettyVariables( check_period )
self._import_folders.Append( ( name, path, pretty_check_period ), ( name, path, check_period ) )
self._names_to_import_folders[ name ] = import_folder
self._import_folders.Append( display_tuple, sort_tuple, import_folder )
#
@ -2563,35 +2549,36 @@ class DialogManageImportFolders( ClientGUIDialogs.Dialog ):
wx.CallAfter( self._ok.SetFocus )
def _AddImportFolder( self, name ):
def _AddImportFolder( self ):
if name not in self._names_to_import_folders:
import_folder = ClientImporting.ImportFolder( 'import folder' )
with DialogManageImportFoldersEdit( self, import_folder ) as dlg:
import_folder = ClientImporting.ImportFolder( name )
with DialogManageImportFoldersEdit( self, import_folder ) as dlg:
if dlg.ShowModal() == wx.ID_OK:
if dlg.ShowModal() == wx.ID_OK:
import_folder = dlg.GetInfo()
( name, path, check_period ) = import_folder.ToListBoxTuple()
pretty_check_period = self._GetPrettyVariables( check_period )
self._import_folders.Append( ( name, path, pretty_check_period ), ( name, path, check_period ) )
self._names_to_import_folders[ name ] = import_folder
import_folder = dlg.GetInfo()
self._import_folders.SetNonDupeName( import_folder )
( display_tuple, sort_tuple ) = self._ConvertImportFolderToTuples( import_folder )
self._import_folders.Append( display_tuple, sort_tuple, import_folder )
def _GetPrettyVariables( self, check_period ):
def _ConvertImportFolderToTuples( self, import_folder ):
sort_tuple = import_folder.ToListBoxTuple()
( name, path, check_period ) = sort_tuple
pretty_check_period = HydrusData.ConvertTimeDeltaToPrettyString( check_period )
return pretty_check_period
display_tuple = ( name, path, pretty_check_period )
return ( display_tuple, sort_tuple )
def Delete( self ):
@ -2605,9 +2592,9 @@ class DialogManageImportFolders( ClientGUIDialogs.Dialog ):
for index in indices:
( name, path, check_period ) = self._import_folders.GetClientData( index )
import_folder = self._import_folders.GetClientData( index )
import_folder = self._names_to_import_folders[ name ]
original_name = import_folder.GetName()
with DialogManageImportFoldersEdit( self, import_folder ) as dlg:
@ -2615,13 +2602,14 @@ class DialogManageImportFolders( ClientGUIDialogs.Dialog ):
import_folder = dlg.GetInfo()
( name, path, check_period ) = import_folder.ToListBoxTuple()
if import_folder.GetName() != original_name:
self._import_folders.SetNonDupeName( import_folder )
pretty_check_period = self._GetPrettyVariables( check_period )
( display_tuple, sort_tuple ) = self._ConvertImportFolderToTuples( import_folder )
self._import_folders.UpdateRow( index, ( name, path, pretty_check_period ), ( name, path, check_period ) )
self._names_to_import_folders[ name ] = import_folder
self._import_folders.UpdateRow( index, display_tuple, sort_tuple, import_folder )
@ -2629,37 +2617,7 @@ class DialogManageImportFolders( ClientGUIDialogs.Dialog ):
def EventAdd( self, event ):
client_data = self._import_folders.GetClientData()
existing_names = set()
for ( name, path, check_period ) in client_data:
existing_names.add( name )
with ClientGUIDialogs.DialogTextEntry( self, 'Enter a name for the import folder.' ) as dlg:
if dlg.ShowModal() == wx.ID_OK:
try:
name = dlg.GetValue()
if name in existing_names: raise HydrusExceptions.NameException( 'That name is already in use!' )
if name == '': raise HydrusExceptions.NameException( 'Please enter a nickname for the import folder.' )
self._AddImportFolder( name )
except HydrusExceptions.NameException as e:
wx.MessageBox( str( e ) )
self.EventAdd( event )
self._AddImportFolder()
def EventDelete( self, event ):
@ -2674,29 +2632,26 @@ class DialogManageImportFolders( ClientGUIDialogs.Dialog ):
def EventOK( self, event ):
client_data = self._import_folders.GetClientData()
existing_db_names = set( HydrusGlobals.client_controller.Read( 'serialisable_names', HydrusSerialisable.SERIALISABLE_TYPE_IMPORT_FOLDER ) )
names_to_save = set()
good_names = set()
for ( name, path, check_period ) in client_data:
import_folders = self._import_folders.GetClientData()
for import_folder in import_folders:
names_to_save.add( name )
good_names.add( import_folder.GetName() )
HydrusGlobals.client_controller.Write( 'serialisable', import_folder )
names_to_delete = { name for name in self._names_to_import_folders if name not in names_to_save }
names_to_delete = existing_db_names - good_names
for name in names_to_delete:
HydrusGlobals.client_controller.Write( 'delete_serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_IMPORT_FOLDER, name )
for name in names_to_save:
import_folder = self._names_to_import_folders[ name ]
HydrusGlobals.client_controller.Write( 'serialisable', import_folder )
HydrusGlobals.client_controller.pub( 'notify_new_import_folders' )
self.EndModal( wx.ID_OK )

View File

@ -3,10 +3,11 @@ import ClientConstants as CC
import ClientCaches
import ClientData
import ClientFiles
import ClientGUICanvas
import ClientGUICommon
import ClientGUIDialogs
import ClientGUIDialogsManage
import ClientGUICanvas
import ClientGUIMenus
import ClientGUIScrolledPanelsManagement
import ClientGUITopLevelWindows
import ClientMedia
@ -32,7 +33,7 @@ import HydrusGlobals
ID_TIMER_ANIMATION = wx.NewId()
def AddServiceKeysToMenu( menu, service_keys, phrase, action ):
def AddServiceKeyLabelsToMenu( menu, service_keys, phrase ):
services_manager = HydrusGlobals.client_controller.GetServicesManager()
@ -40,12 +41,39 @@ def AddServiceKeysToMenu( menu, service_keys, phrase, action ):
( service_key, ) = service_keys
if action == CC.ID_NULL: id = CC.ID_NULL
else: id = ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( action, service_key )
name = services_manager.GetName( service_key )
file_service = services_manager.GetService( service_key )
label = phrase + ' ' + name
menu.Append( id, phrase + ' ' + file_service.GetName() )
ClientGUIMenus.AppendMenuLabel( menu, label )
else:
submenu = wx.Menu()
for service_key in service_keys:
name = services_manager.GetName( service_key )
ClientGUIMenus.AppendMenuLabel( submenu, name )
ClientGUIMenus.AppendMenu( menu, submenu, phrase + u'\u2026' )
def AddServiceKeysToMenu( menu, service_keys, phrase, description, event_handler, callable ):
services_manager = HydrusGlobals.client_controller.GetServicesManager()
if len( service_keys ) == 1:
( service_key, ) = service_keys
name = services_manager.GetName( service_key )
label = phrase + ' ' + name
ClientGUIMenus.AppendMenuItem( menu, label, description, event_handler, callable, service_key )
else:
@ -53,15 +81,12 @@ def AddServiceKeysToMenu( menu, service_keys, phrase, action ):
for service_key in service_keys:
if action == CC.ID_NULL: id = CC.ID_NULL
else: id = ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( action, service_key )
name = services_manager.GetName( service_key )
file_service = services_manager.GetService( service_key )
submenu.Append( id, file_service.GetName() )
ClientGUIMenus.AppendMenuItem( submenu, name, description, event_handler, callable, service_key )
menu.AppendMenu( CC.ID_NULL, phrase + u'\u2026', submenu )
ClientGUIMenus.AppendMenu( menu, submenu, phrase + u'\u2026' )
class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
@ -131,7 +156,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
client_files_manager = HydrusGlobals.client_controller.GetClientFilesManager()
hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_LOCAL )
hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_LOCAL, ordered = True )
paths = [ client_files_manager.GetFilePath( hash ) for hash in hashes ]
@ -171,11 +196,11 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
if hash_type == 'sha256':
hex_hashes = os.linesep.join( [ hash.encode( 'hex' ) for hash in self._GetSelectedHashes() ] )
hex_hashes = os.linesep.join( [ hash.encode( 'hex' ) for hash in self._GetSelectedHashes( ordered = True ) ] )
else:
sha256_hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_LOCAL )
sha256_hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_LOCAL, ordered = True )
if len( sha256_hashes ) > 0:
@ -230,6 +255,24 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
HydrusGlobals.client_controller.pub( 'clipboard', 'text', path )
def _CopyPathsToClipboard( self ):
media_results = self.GenerateMediaResults( discriminant = CC.DISCRIMINANT_LOCAL, selected_media = set( self._selected_media ) )
client_files_manager = HydrusGlobals.client_controller.GetClientFilesManager()
paths = []
for media_result in media_results:
paths.append( client_files_manager.GetFilePath( media_result.GetHash(), media_result.GetMime() ) )
text = os.linesep.join( paths )
HydrusGlobals.client_controller.pub( 'clipboard', 'text', text )
def _CopyServiceFilenameToClipboard( self, service_key ):
display_media = self._focussed_media.GetDisplayMedia()
@ -430,7 +473,14 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
self._PublishSelectionChange()
def _Download( self, hashes ):
def _DownloadSelected( self ):
hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_NOT_LOCAL )
self._DownloadHashes( hashes )
def _DownloadHashes( self, hashes ):
HydrusGlobals.client_controller.Write( 'content_updates', { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_PEND, hashes ) ] } )
@ -618,19 +668,22 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
result = set()
for media in self._selected_media: result.update( media.GetHashes( has_location, discriminant, not_uploaded_to, ordered ) )
for media in self._selected_media:
result.update( media.GetHashes( has_location, discriminant, not_uploaded_to, ordered ) )
return result
def _GetSimilarTo( self ):
def _GetSimilarTo( self, max_hamming ):
if self._focussed_media is not None:
hash = self._focussed_media.GetDisplayMedia().GetHash()
HydrusGlobals.client_controller.pub( 'new_similar_to', self._file_service_key, hash )
HydrusGlobals.client_controller.pub( 'new_similar_to', CC.COMBINED_LOCAL_FILE_SERVICE_KEY, hash, max_hamming )
@ -949,7 +1002,10 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
service = HydrusGlobals.client_controller.GetServicesManager().GetService( service_key )
if service.GetServiceType() == HC.LOCAL_RATING_LIKE: ClientGUICanvas.RatingsFilterFrameLike( self.GetTopLevelParent(), self._page_key, service_key, media_results )
if service.GetServiceType() == HC.LOCAL_RATING_LIKE:
ClientGUICanvas.RatingsFilterFrameLike( self.GetTopLevelParent(), self._page_key, service_key, media_results )
@ -957,6 +1013,13 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
def _RedrawMedia( self, media ): pass
def _RescindDownloadSelected( self ):
hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_NOT_LOCAL )
HydrusGlobals.client_controller.Write( 'content_updates', { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_RESCIND_PEND, hashes ) ] } )
def _RescindPetitionFiles( self, file_service_key ):
hashes = self._GetSelectedHashes()
@ -977,16 +1040,22 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
def _Select( self, select_type ):
def _Select( self, select_type, extra_info = None ):
if select_type == 'all': self._DeselectSelect( [], self._sorted_media )
if select_type == 'all':
self._DeselectSelect( [], self._sorted_media )
else:
if select_type == 'invert':
( media_to_deselect, media_to_select ) = ( self._selected_media, set( self._sorted_media ) - self._selected_media )
( media_to_deselect, media_to_select ) = ( self._selected_media, { m for m in self._sorted_media if m not in self._selected_media } )
elif select_type == 'none':
( media_to_deselect, media_to_select ) = ( self._selected_media, [] )
elif select_type == 'none': ( media_to_deselect, media_to_select ) = ( self._selected_media, [] )
elif select_type in ( 'inbox', 'archive' ):
inbox_media = { m for m in self._sorted_media if m.HasInbox() }
@ -1003,21 +1072,12 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
media_to_select = [ m for m in archive_media if m not in self._selected_media ]
elif select_type in ( 'local', 'trash' ):
elif select_type == 'file_service':
local_media = { media for media in self._sorted_media if CC.LOCAL_FILE_SERVICE_KEY in media.GetLocationsManager().GetCurrent() }
trash_media = { media for media in self._sorted_media if CC.TRASH_SERVICE_KEY in media.GetLocationsManager().GetCurrent() }
file_service_key = extra_info
if select_type == 'local':
media_to_deselect = [ m for m in trash_media if m in self._selected_media ]
media_to_select = [ m for m in local_media if m not in self._selected_media ]
elif select_type == 'trash':
media_to_deselect = [ m for m in local_media if m in self._selected_media ]
media_to_select = [ m for m in trash_media if m not in self._selected_media ]
media_to_deselect = [ m for m in self._selected_media if file_service_key not in m.GetLocationsManager().GetCurrent() ]
media_to_select = [ m for m in self._sorted_media if m not in self._selected_media and file_service_key in m.GetLocationsManager().GetCurrent() ]
if self._focussed_media in media_to_deselect: self._SetFocussedMedia( None )
@ -2073,6 +2133,7 @@ class MediaPanelThumbnails( MediaPanel ):
elif command == 'copy_service_filename': self._CopyServiceFilenameToClipboard( data )
elif command == 'copy_service_filenames': self._CopyServiceFilenamesToClipboard( data )
elif command == 'copy_path': self._CopyPathToClipboard()
elif command == 'copy_paths': self._CopyPathsToClipboard()
elif command == 'ctrl-space':
if self._focussed_media is not None: self._HitMedia( self._focussed_media, True, False )
@ -2092,15 +2153,10 @@ class MediaPanelThumbnails( MediaPanel ):
self._Delete( data )
elif command == 'download':
self._Download( self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_NOT_LOCAL ) )
elif command == 'export_files': self._ExportFiles()
elif command == 'export_tags': self._ExportTags()
elif command == 'filter': self._Filter()
elif command == 'fullscreen': self._FullScreen()
elif command == 'get_similar_to': self._GetSimilarTo()
elif command == 'inbox': self._Inbox()
elif command == 'manage_ratings': self._ManageRatings()
elif command == 'manage_tags': self._ManageTags()
@ -2109,7 +2165,6 @@ class MediaPanelThumbnails( MediaPanel ):
elif command == 'open_externally': self._OpenExternally()
elif command == 'petition': self._PetitionFiles( data )
elif command == 'remove': self._Remove()
elif command == 'rescind_download': HydrusGlobals.client_controller.Write( 'content_updates', { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_RESCIND_PEND, self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_DOWNLOADING ) ) ] } )
elif command == 'rescind_petition': self._RescindPetitionFiles( data )
elif command == 'rescind_upload': self._RescindUploadFiles( data )
elif command == 'scroll_end': self._ScrollEnd( False )
@ -2147,7 +2202,7 @@ class MediaPanelThumbnails( MediaPanel ):
if len( locations_manager.GetCurrentRemote() ) > 0:
self._Download( t.GetHashes() )
self._DownloadHashes( t.GetHashes() )
@ -2252,6 +2307,8 @@ class MediaPanelThumbnails( MediaPanel ):
def EventShowMenu( self, event ):
services_manager = HydrusGlobals.client_controller.GetServicesManager()
thumbnail = self._GetThumbnailUnderMouse( event )
if thumbnail is not None: self._HitMedia( thumbnail, event.CmdDown(), event.ShiftDown() )
@ -2265,8 +2322,11 @@ class MediaPanelThumbnails( MediaPanel ):
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 )
media_has_local_file_domain = True in ( CC.LOCAL_FILE_SERVICE_KEY in locations_manager.GetCurrent() for locations_manager in all_locations_managers )
media_has_trash = True in ( CC.TRASH_SERVICE_KEY in locations_manager.GetCurrent() for locations_manager in all_locations_managers )
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 } )
all_local_file_domains = services_manager.Filter( all_specific_file_domains, ( HC.LOCAL_FILE_DOMAIN, ) )
all_file_repos = services_manager.Filter( all_specific_file_domains, ( HC.FILE_REPOSITORY, ) )
media_has_inbox = True in ( media.HasInbox() for media in self._sorted_media )
media_has_archive = True in ( media.HasArchive() for media in self._sorted_media )
@ -2274,39 +2334,52 @@ class MediaPanelThumbnails( MediaPanel ):
if thumbnail is None:
menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'refresh' ), 'refresh' )
ClientGUIMenus.AppendMenuItem( menu, 'refresh', 'Refresh the current search.', self, HydrusGlobals.client_controller.pub, 'refresh_query', self._page_key )
if len( self._sorted_media ) > 0:
if menu.GetMenuItemCount() > 0:
menu.AppendSeparator()
menu.AppendSeparator()
select_menu = wx.Menu()
if len( self._selected_media ) < len( self._sorted_media ):
select_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select', 'all' ), 'all' )
ClientGUIMenus.AppendMenuItem( select_menu, 'all', 'Select everything.', self, self._Select, 'all' )
select_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select', 'invert' ), 'invert' )
ClientGUIMenus.AppendMenuItem( select_menu, 'invert', 'Swap what is and is not selected.', self, self._Select, 'invert' )
if media_has_archive and media_has_inbox:
select_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select', 'inbox' ), 'inbox' )
select_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select', 'archive' ), 'archive' )
ClientGUIMenus.AppendMenuItem( select_menu, 'inbox', 'Select everything in the inbox.', self, self._Select, 'inbox' )
ClientGUIMenus.AppendMenuItem( select_menu, 'archive', 'Select everything that is archived.', self, self._Select, 'archive' )
if media_has_local_file_domain and media_has_trash:
if len( all_specific_file_domains ) > 1:
select_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select', 'local' ), 'my files' )
select_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select', 'trash' ), 'trash' )
selectable_file_domains = list( all_local_file_domains )
if CC.TRASH_SERVICE_KEY in all_specific_file_domains:
selectable_file_domains.append( CC.TRASH_SERVICE_KEY )
selectable_file_domains.extend( all_file_repos )
for service_key in selectable_file_domains:
name = services_manager.GetName( service_key )
ClientGUIMenus.AppendMenuItem( select_menu, name, 'Select everything in ' + name + '.', self, self._Select, 'file_service', service_key )
select_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select', 'none' ), 'none' )
if len( self._selected_media ) > 0:
ClientGUIMenus.AppendMenuItem( select_menu, 'none', 'Deselect everything.', self, self._Select, 'none' )
menu.AppendMenu( CC.ID_NULL, 'select', select_menu )
ClientGUIMenus.AppendMenu( menu, select_menu, 'select' )
else:
@ -2413,17 +2486,15 @@ class MediaPanelThumbnails( MediaPanel ):
# info about the files
def MassUnion( lists ): return { item for item in itertools.chain.from_iterable( lists ) }
groups_of_current_remote_service_keys = [ locations_manager.GetCurrentRemote() for locations_manager in selected_locations_managers ]
groups_of_pending_remote_service_keys = [ locations_manager.GetPendingRemote() for locations_manager in selected_locations_managers ]
groups_of_petitioned_remote_service_keys = [ locations_manager.GetPetitionedRemote() for locations_manager in selected_locations_managers ]
groups_of_deleted_remote_service_keys = [ locations_manager.GetDeletedRemote() for locations_manager in selected_locations_managers ]
current_remote_service_keys = MassUnion( groups_of_current_remote_service_keys )
pending_remote_service_keys = MassUnion( groups_of_pending_remote_service_keys )
petitioned_remote_service_keys = MassUnion( groups_of_petitioned_remote_service_keys )
deleted_remote_service_keys = MassUnion( groups_of_deleted_remote_service_keys )
current_remote_service_keys = HydrusData.MassUnion( groups_of_current_remote_service_keys )
pending_remote_service_keys = HydrusData.MassUnion( groups_of_pending_remote_service_keys )
petitioned_remote_service_keys = HydrusData.MassUnion( groups_of_petitioned_remote_service_keys )
deleted_remote_service_keys = HydrusData.MassUnion( groups_of_deleted_remote_service_keys )
common_current_remote_service_keys = HydrusData.IntelligentMassIntersect( groups_of_current_remote_service_keys )
common_pending_remote_service_keys = HydrusData.IntelligentMassIntersect( groups_of_pending_remote_service_keys )
@ -2495,10 +2566,9 @@ class MediaPanelThumbnails( MediaPanel ):
downloadable_file_service_keys.update( download_permission_file_service_keys & locations_manager.GetCurrentRemote() )
# we can petition when we have permission and a file is current
# we can re-petition an already petitioned file
# we can petition when we have permission and a file is current and it is not already petitioned
petitionable_file_service_keys.update( petition_permission_file_service_keys & locations_manager.GetCurrentRemote() )
petitionable_file_service_keys.update( ( petition_permission_file_service_keys & locations_manager.GetCurrentRemote() ) - locations_manager.GetPetitionedRemote() )
# we can delete remote when we have permission and a file is current and it is not already petitioned
@ -2526,49 +2596,85 @@ class MediaPanelThumbnails( MediaPanel ):
if multiple_selected:
menu.Append( CC.ID_NULL, HydrusData.ConvertIntToPrettyString( num_selected ) + ' files, ' + self._GetPrettyTotalSelectedSize() )
ClientGUIMenus.AppendMenuLabel( menu, HydrusData.ConvertIntToPrettyString( num_selected ) + ' files, ' + self._GetPrettyTotalSelectedSize() )
else:
for line in thumbnail.GetPrettyInfoLines():
menu.Append( CC.ID_NULL, line )
ClientGUIMenus.AppendMenuLabel( menu, line )
if len( disparate_current_file_service_keys ) > 0: AddServiceKeysToMenu( menu, disparate_current_file_service_keys, 'some uploaded to', CC.ID_NULL )
if multiple_selected:
if len( disparate_current_file_service_keys ) > 0:
if len( common_current_file_service_keys ) > 0: AddServiceKeysToMenu( menu, common_current_file_service_keys, 'selected uploaded to', CC.ID_NULL )
AddServiceKeyLabelsToMenu( menu, disparate_current_file_service_keys, 'some uploaded to' )
if len( disparate_pending_file_service_keys ) > 0: AddServiceKeysToMenu( menu, disparate_pending_file_service_keys, 'some pending to', CC.ID_NULL )
if len( common_pending_file_service_keys ) > 0: AddServiceKeysToMenu( menu, common_pending_file_service_keys, 'pending to', CC.ID_NULL )
if len( disparate_petitioned_file_service_keys ) > 0: AddServiceKeysToMenu( menu, disparate_petitioned_file_service_keys, 'some petitioned from', CC.ID_NULL )
if len( common_petitioned_file_service_keys ) > 0: AddServiceKeysToMenu( menu, common_petitioned_file_service_keys, 'petitioned from', CC.ID_NULL )
if len( disparate_deleted_file_service_keys ) > 0: AddServiceKeysToMenu( menu, disparate_deleted_file_service_keys, 'some deleted from', CC.ID_NULL )
if len( common_deleted_file_service_keys ) > 0: AddServiceKeysToMenu( menu, common_deleted_file_service_keys, 'deleted from', CC.ID_NULL )
if len( disparate_current_ipfs_service_keys ) > 0: AddServiceKeysToMenu( menu, disparate_current_ipfs_service_keys, 'some pinned to', CC.ID_NULL )
if multiple_selected:
if multiple_selected and len( common_current_file_service_keys ) > 0:
if len( common_current_ipfs_service_keys ) > 0: AddServiceKeysToMenu( menu, common_current_ipfs_service_keys, 'selected pinned to', CC.ID_NULL )
AddServiceKeyLabelsToMenu( menu, common_current_file_service_keys, 'selected uploaded to' )
if len( disparate_pending_ipfs_service_keys ) > 0: AddServiceKeysToMenu( menu, disparate_pending_ipfs_service_keys, 'some to be pinned to', CC.ID_NULL )
if len( disparate_pending_file_service_keys ) > 0:
AddServiceKeyLabelsToMenu( menu, disparate_pending_file_service_keys, 'some pending to' )
if len( common_pending_ipfs_service_keys ) > 0: AddServiceKeysToMenu( menu, common_pending_ipfs_service_keys, 'to be pinned to', CC.ID_NULL )
if len( common_pending_file_service_keys ) > 0:
AddServiceKeyLabelsToMenu( menu, common_pending_file_service_keys, 'pending to' )
if len( disparate_petitioned_ipfs_service_keys ) > 0: AddServiceKeysToMenu( menu, disparate_petitioned_ipfs_service_keys, 'some to be unpinned from', CC.ID_NULL )
if len( disparate_petitioned_file_service_keys ) > 0:
AddServiceKeyLabelsToMenu( menu, disparate_petitioned_file_service_keys, 'some petitioned from' )
if len( common_petitioned_ipfs_service_keys ) > 0: AddServiceKeysToMenu( menu, common_petitioned_ipfs_service_keys, unpin_phrase, CC.ID_NULL )
if len( common_petitioned_file_service_keys ) > 0:
AddServiceKeyLabelsToMenu( menu, common_petitioned_file_service_keys, 'petitioned from' )
if len( disparate_deleted_file_service_keys ) > 0:
AddServiceKeyLabelsToMenu( menu, disparate_deleted_file_service_keys, 'some deleted from' )
if len( common_deleted_file_service_keys ) > 0:
AddServiceKeyLabelsToMenu( menu, common_deleted_file_service_keys, 'deleted from' )
if len( disparate_current_ipfs_service_keys ) > 0:
AddServiceKeyLabelsToMenu( menu, disparate_current_ipfs_service_keys, 'some pinned to' )
if multiple_selected and len( common_current_ipfs_service_keys ) > 0:
AddServiceKeyLabelsToMenu( menu, common_current_ipfs_service_keys, 'selected pinned to' )
if len( disparate_pending_ipfs_service_keys ) > 0:
AddServiceKeyLabelsToMenu( menu, disparate_pending_ipfs_service_keys, 'some to be pinned to' )
if len( common_pending_ipfs_service_keys ) > 0:
AddServiceKeyLabelsToMenu( menu, common_pending_ipfs_service_keys, 'to be pinned to' )
if len( disparate_petitioned_ipfs_service_keys ) > 0:
AddServiceKeyLabelsToMenu( menu, disparate_petitioned_ipfs_service_keys, 'some to be unpinned from' )
if len( common_petitioned_ipfs_service_keys ) > 0:
AddServiceKeyLabelsToMenu( menu, common_petitioned_ipfs_service_keys, unpin_phrase )
menu.AppendSeparator()
@ -2597,36 +2703,72 @@ class MediaPanelThumbnails( MediaPanel ):
remote_action_menu = wx.Menu()
if len( downloadable_file_service_keys ) > 0: remote_action_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'download' ), download_phrase )
if len( downloadable_file_service_keys ) > 0:
ClientGUIMenus.AppendMenuItem( remote_action_menu, download_phrase, 'Download all possible selected files.', self, self._DownloadSelected )
if some_downloading: remote_action_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'rescind_download' ), rescind_download_phrase )
if some_downloading:
ClientGUIMenus.AppendMenuItem( remote_action_menu, rescind_download_phrase, 'Stop downloading any of the selected files.', self, self._RescindDownloadSelected )
if len( uploadable_file_service_keys ) > 0: AddServiceKeysToMenu( remote_action_menu, uploadable_file_service_keys, upload_phrase, 'upload' )
if len( uploadable_file_service_keys ) > 0:
AddServiceKeysToMenu( remote_action_menu, uploadable_file_service_keys, upload_phrase, 'Upload all selected files to the file repository.', self, self._UploadFiles )
if len( pending_file_service_keys ) > 0: AddServiceKeysToMenu( remote_action_menu, pending_file_service_keys, rescind_upload_phrase, 'rescind_upload' )
if len( pending_file_service_keys ) > 0:
AddServiceKeysToMenu( remote_action_menu, pending_file_service_keys, rescind_upload_phrase, 'Rescind the pending upload to the file repository.', self, self._RescindUploadFiles )
if len( petitionable_file_service_keys ) > 0: AddServiceKeysToMenu( remote_action_menu, petitionable_file_service_keys, petition_phrase, 'petition' )
if len( petitionable_file_service_keys ) > 0:
AddServiceKeysToMenu( remote_action_menu, petitionable_file_service_keys, petition_phrase, 'Petition these files for deletion from the file repository.', self, self._PetitionFiles )
if len( petitioned_file_service_keys ) > 0: AddServiceKeysToMenu( remote_action_menu, petitioned_file_service_keys, rescind_petition_phrase, 'rescind_petition' )
if len( petitioned_file_service_keys ) > 0:
AddServiceKeysToMenu( remote_action_menu, petitioned_file_service_keys, rescind_petition_phrase, 'Rescind the petition to delete these files from the file repository.', self, self._RescindPetitionFiles )
if len( deletable_file_service_keys ) > 0: AddServiceKeysToMenu( remote_action_menu, deletable_file_service_keys, remote_delete_phrase, 'delete' )
if len( deletable_file_service_keys ) > 0:
AddServiceKeysToMenu( remote_action_menu, deletable_file_service_keys, remote_delete_phrase, 'Delete these files from the file repository.', self, self._Delete )
if len( modifyable_file_service_keys ) > 0: AddServiceKeysToMenu( remote_action_menu, modifyable_file_service_keys, modify_account_phrase, 'modify_account' )
if len( modifyable_file_service_keys ) > 0:
AddServiceKeysToMenu( remote_action_menu, modifyable_file_service_keys, modify_account_phrase, 'Modify the account(s) that uploaded these files to the file repository.', self, self._ModifyUploaders )
if len( pinnable_ipfs_service_keys ) > 0: AddServiceKeysToMenu( remote_action_menu, pinnable_ipfs_service_keys, pin_phrase, 'upload' )
if len( pinnable_ipfs_service_keys ) > 0:
AddServiceKeysToMenu( remote_action_menu, pinnable_ipfs_service_keys, pin_phrase, 'Pin these files to the ipfs service.', self, self._UploadFiles )
if len( pending_ipfs_service_keys ) > 0: AddServiceKeysToMenu( remote_action_menu, pending_ipfs_service_keys, rescind_pin_phrase, 'rescind_upload' )
if len( pending_ipfs_service_keys ) > 0:
AddServiceKeysToMenu( remote_action_menu, pending_ipfs_service_keys, rescind_pin_phrase, 'Rescind the pending pin to the ipfs service.', self, self._RescindUploadFiles )
if len( unpinnable_ipfs_service_keys ) > 0: AddServiceKeysToMenu( remote_action_menu, unpinnable_ipfs_service_keys, unpin_phrase, 'petition' )
if len( unpinnable_ipfs_service_keys ) > 0:
AddServiceKeysToMenu( remote_action_menu, unpinnable_ipfs_service_keys, unpin_phrase, 'Unpin these files from the ipfs service.', self, self._PetitionFiles )
if len( petitioned_ipfs_service_keys ) > 0: AddServiceKeysToMenu( remote_action_menu, petitioned_ipfs_service_keys, rescind_unpin_phrase, 'rescind_petition' )
if len( petitioned_ipfs_service_keys ) > 0:
AddServiceKeysToMenu( remote_action_menu, petitioned_ipfs_service_keys, rescind_unpin_phrase, 'Rescind the pending unpin from the ipfs service.', self, self._RescindPetitionFiles )
if multiple_selected and len( ipfs_service_keys ) > 0:
AddServiceKeysToMenu( remote_action_menu, ipfs_service_keys, 'pin new directory to', 'upload_directory' )
AddServiceKeysToMenu( remote_action_menu, ipfs_service_keys, 'pin new directory to', 'Pin these files as a directory to the ipfs service.', self, self._UploadDirectory )
menu.AppendMenu( CC.ID_NULL, 'remote services', remote_action_menu )
ClientGUIMenus.AppendMenu( menu, remote_action_menu, 'remote services' )
#
@ -2635,10 +2777,10 @@ class MediaPanelThumbnails( MediaPanel ):
manage_menu = wx.Menu()
manage_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'manage_tags' ), manage_tags_phrase )
manage_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'manage_ratings' ), manage_ratings_phrase )
ClientGUIMenus.AppendMenuItem( manage_menu, manage_tags_phrase, 'Manage tags for the selected files.', self, self._ManageTags )
ClientGUIMenus.AppendMenuItem( manage_menu, manage_ratings_phrase, 'Manage ratings for the selected files.', self, self._ManageRatings )
menu.AppendMenu( CC.ID_NULL, 'manage', manage_menu )
ClientGUIMenus.AppendMenu( menu, manage_menu, 'manage' )
else:
@ -2651,7 +2793,7 @@ class MediaPanelThumbnails( MediaPanel ):
phrase = 'manage file\'s tags'
menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'manage_tags' ), phrase )
ClientGUIMenus.AppendMenuItem( menu, phrase, 'Manage tags for the selected files.', self, self._ManageTags )
#
@ -2780,6 +2922,14 @@ class MediaPanelThumbnails( MediaPanel ):
copy_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'copy_path' ) , 'path' )
if multiple_selected and selection_has_local:
copy_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'copy_paths' ) , 'paths' )
if focussed_is_local:
if HC.options[ 'local_port' ] is not None:
copy_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'copy_local_url' ) , 'local url' )
@ -2818,8 +2968,7 @@ class MediaPanelThumbnails( MediaPanel ):
menu.AppendSeparator()
menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'refresh' ), 'refresh' )
ClientGUIMenus.AppendMenuItem( menu, 'refresh', 'Refresh the current search.', self, HydrusGlobals.client_controller.pub, 'refresh_query', self._page_key )
if len( self._sorted_media ) > 0:
@ -2827,25 +2976,44 @@ class MediaPanelThumbnails( MediaPanel ):
select_menu = wx.Menu()
select_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select', 'all' ), 'all' )
if len( self._selected_media ) < len( self._sorted_media ):
ClientGUIMenus.AppendMenuItem( select_menu, 'all', 'Select everything.', self, self._Select, 'all' )
select_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select', 'invert' ), 'invert' )
ClientGUIMenus.AppendMenuItem( select_menu, 'invert', 'Swap what is and is not selected.', self, self._Select, 'invert' )
if media_has_archive and media_has_inbox:
select_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select', 'inbox' ), 'inbox' )
select_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select', 'archive' ), 'archive' )
ClientGUIMenus.AppendMenuItem( select_menu, 'inbox', 'Select everything in the inbox.', self, self._Select, 'inbox' )
ClientGUIMenus.AppendMenuItem( select_menu, 'archive', 'Select everything that is archived.', self, self._Select, 'archive' )
if media_has_local_file_domain and media_has_trash:
if len( all_specific_file_domains ) > 1:
select_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select', 'local' ), 'my files' )
select_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select', 'trash' ), 'trash' )
selectable_file_domains = list( all_local_file_domains )
if CC.TRASH_SERVICE_KEY in all_specific_file_domains:
selectable_file_domains.append( CC.TRASH_SERVICE_KEY )
selectable_file_domains.extend( all_file_repos )
for service_key in selectable_file_domains:
name = services_manager.GetName( service_key )
ClientGUIMenus.AppendMenuItem( select_menu, name, 'Select everything in ' + name + '.', self, self._Select, 'file_service', service_key )
select_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select', 'none' ), 'none' )
if len( self._selected_media ) > 0:
ClientGUIMenus.AppendMenuItem( select_menu, 'none', 'Deselect everything.', self, self._Select, 'none' )
menu.AppendMenu( CC.ID_NULL, 'select', select_menu )
ClientGUIMenus.AppendMenu( menu, select_menu, 'select' )
menu.AppendSeparator()
@ -2856,7 +3024,14 @@ class MediaPanelThumbnails( MediaPanel ):
menu.AppendSeparator()
menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'get_similar_to' ) , 'find very similar images' )
similar_menu = wx.Menu()
ClientGUIMenus.AppendMenuItem( similar_menu, 'exact match', 'Search the database for files that look precisely like this one.', self, self._GetSimilarTo, 0 )
ClientGUIMenus.AppendMenuItem( similar_menu, 'very similar', 'Search the database for files that look just like this one.', self, self._GetSimilarTo, 2 )
ClientGUIMenus.AppendMenuItem( similar_menu, 'similar', 'Search the database for files that look generally like this one.', self, self._GetSimilarTo, 4 )
ClientGUIMenus.AppendMenuItem( similar_menu, 'speculative', 'Search the database for files that probably look like this one. This is sometimes useful for symbols with sharp edges or lines.', self, self._GetSimilarTo, 8 )
ClientGUIMenus.AppendMenu( menu, similar_menu, 'find similar files' )

View File

@ -51,6 +51,17 @@ def AppendMenuItem( menu, label, description, event_handler, callable, *args, **
return menu_item
def AppendMenuLabel( menu, label, description = None ):
if description is None:
description = ''
menu_item = menu.Append( wx.ID_ANY, label, description )
return menu_item
def BindMenuItem( menu, event_handler, menu_item, callable, *args, **kwargs ):
l_callable = GetLambdaCallable( callable, *args, **kwargs )

View File

@ -402,7 +402,7 @@ class EditNodes( wx.Panel ):
self._referral_url_callable = referral_url_callable
self._example_data_callable = example_data_callable
self._nodes = ClientGUICommon.SaneListCtrl( self, 200, [ ( 'name', 120 ), ( 'node type', 80 ), ( 'produces', -1 ) ], delete_key_callback = self.Delete, activation_callback = self.Edit, use_display_tuple_for_sort = True )
self._nodes = ClientGUICommon.SaneListCtrlForSingleObject( self, 200, [ ( 'name', 120 ), ( 'node type', 80 ), ( 'produces', -1 ) ], delete_key_callback = self.Delete, activation_callback = self.Edit )
menu_items = []
@ -425,9 +425,9 @@ class EditNodes( wx.Panel ):
for node in nodes:
( display_tuple, data_tuple ) = self._ConvertNodeToTuples( node )
( display_tuple, sort_tuple ) = self._ConvertNodeToTuples( node )
self._nodes.Append( display_tuple, data_tuple )
self._nodes.Append( display_tuple, sort_tuple, node )
#
@ -453,7 +453,56 @@ class EditNodes( wx.Panel ):
( name, node_type, produces ) = node.ToPrettyStrings()
return ( ( name, node_type, produces ), ( node, node_type, produces ) )
return ( ( name, node_type, produces ), ( name, node_type, produces ) )
def _GetExportObject( self ):
to_export = HydrusSerialisable.SerialisableList()
for node in self._nodes.GetSelectedClientData():
to_export.append( node )
if len( to_export ) == 0:
return None
elif len( to_export ) == 1:
return to_export[0]
else:
return to_export
def _ImportObject( self, obj ):
if isinstance( obj, HydrusSerialisable.SerialisableList ):
for sub_obj in obj:
self._ImportObject( sub_obj )
else:
if isinstance( obj, ( ClientParsing.ParseNodeContent, ClientParsing.ParseNodeContentLink ) ):
node = obj
( display_tuple, sort_tuple ) = self._ConvertNodeToTuples( node )
self._nodes.Append( display_tuple, sort_tuple, node )
else:
wx.MessageBox( 'That was not a script--it was a: ' + type( obj ).__name__ )
def AddContentNode( self ):
@ -493,22 +542,22 @@ class EditNodes( wx.Panel ):
new_node = panel.GetValue()
( display_tuple, data_tuple ) = self._ConvertNodeToTuples( new_node )
( display_tuple, sort_tuple ) = self._ConvertNodeToTuples( new_node )
self._nodes.Append( display_tuple, data_tuple )
self._nodes.Append( display_tuple, sort_tuple, new_node )
def Copy( self ):
for i in self._nodes.GetAllSelected():
export_object = self._GetExportObject()
if export_object is not None:
( node, node_type, produces ) = self._nodes.GetClientData( i )
json = export_object.DumpToString()
node_json = node.DumpToString()
HydrusGlobals.client_controller.pub( 'clipboard', 'text', node_json )
HydrusGlobals.client_controller.pub( 'clipboard', 'text', json )
@ -519,22 +568,15 @@ class EditNodes( wx.Panel ):
def Duplicate( self ):
nodes_to_dupe = []
for i in self._nodes.GetAllSelected():
( node, node_type, produces ) = self._nodes.GetClientData( i )
nodes_to_dupe.append( node )
nodes_to_dupe = self._nodes.GetSelectedClientData()
for node in nodes_to_dupe:
dupe_node = node.Duplicate()
( display_tuple, data_tuple ) = self._ConvertNodeToTuples( dupe_node )
( display_tuple, sort_tuple ) = self._ConvertNodeToTuples( dupe_node )
self._nodes.Append( display_tuple, data_tuple )
self._nodes.Append( display_tuple, sort_tuple, dupe_node )
@ -542,7 +584,7 @@ class EditNodes( wx.Panel ):
for i in self._nodes.GetAllSelected():
( node, node_type, produces ) = self._nodes.GetClientData( i )
node = self._nodes.GetClientData( i )
with ClientGUITopLevelWindows.DialogEdit( self, 'edit node' ) as dlg:
@ -566,9 +608,9 @@ class EditNodes( wx.Panel ):
edited_node = panel.GetValue()
( display_tuple, data_tuple ) = self._ConvertNodeToTuples( edited_node )
( display_tuple, sort_tuple ) = self._ConvertNodeToTuples( edited_node )
self._nodes.UpdateRow( i, display_tuple, data_tuple )
self._nodes.UpdateRow( i, display_tuple, sort_tuple, edited_node )
@ -577,9 +619,7 @@ class EditNodes( wx.Panel ):
def GetValue( self ):
nodes = [ node for ( node, node_type, produces ) in self._nodes.GetClientData() ]
return nodes
return self._nodes.GetClientData()
def Paste( self ):
@ -598,14 +638,7 @@ class EditNodes( wx.Panel ):
obj = HydrusSerialisable.CreateFromString( raw_text )
if isinstance( obj, ( ClientParsing.ParseNodeContent, ClientParsing.ParseNodeContentLink ) ):
node = obj
( display_tuple, data_tuple ) = self._ConvertNodeToTuples( node )
self._nodes.Append( display_tuple, data_tuple )
self._ImportObject( obj )
except:
@ -1514,11 +1547,15 @@ And pass that html to a number of 'parsing children' that will each look through
class ManageParsingScriptsPanel( ClientGUIScrolledPanels.ManagePanel ):
SCRIPT_TYPES = []
SCRIPT_TYPES.append( HydrusSerialisable.SERIALISABLE_TYPE_PARSE_ROOT_FILE_LOOKUP )
def __init__( self, parent ):
ClientGUIScrolledPanels.ManagePanel.__init__( self, parent )
self._scripts = ClientGUICommon.SaneListCtrl( self, 200, [ ( 'name', 140 ), ( 'query type', 80 ), ( 'script type', 80 ), ( 'produces', -1 ) ], delete_key_callback = self.Delete, activation_callback = self.Edit, use_display_tuple_for_sort = True )
self._scripts = ClientGUICommon.SaneListCtrlForSingleObject( self, 200, [ ( 'name', 140 ), ( 'query type', 80 ), ( 'script type', 80 ), ( 'produces', -1 ) ], delete_key_callback = self.Delete, activation_callback = self.Edit )
menu_items = []
@ -1548,13 +1585,18 @@ class ManageParsingScriptsPanel( ClientGUIScrolledPanels.ManagePanel ):
#
scripts = HydrusGlobals.client_controller.Read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_PARSE_ROOT_FILE_LOOKUP )
scripts = []
for script_type in self.SCRIPT_TYPES:
scripts.extend( HydrusGlobals.client_controller.Read( 'serialisable_named', script_type ) )
for script in scripts:
( display_tuple, data_tuple ) = self._ConvertScriptToTuples( script )
( display_tuple, sort_tuple ) = self._ConvertScriptToTuples( script )
self._scripts.Append( display_tuple, data_tuple )
self._scripts.Append( display_tuple, sort_tuple, script )
#
@ -1580,18 +1622,16 @@ class ManageParsingScriptsPanel( ClientGUIScrolledPanels.ManagePanel ):
( name, query_type, script_type, produces ) = script.ToPrettyStrings()
return ( ( name, query_type, script_type, produces ), ( script, query_type, script_type, produces ) )
return ( ( name, query_type, script_type, produces ), ( name, query_type, script_type, produces ) )
def _GetExportObject( self ):
to_export = HydrusSerialisable.SerialisableList()
for i in self._scripts.GetAllSelected():
for script in self._scripts.GetSelectedClientData():
single_object = self._scripts.GetClientData( i )[0]
to_export.append( single_object )
to_export.append( script )
if len( to_export ) == 0:
@ -1623,11 +1663,11 @@ class ManageParsingScriptsPanel( ClientGUIScrolledPanels.ManagePanel ):
script = obj
self._SetNonDupeName( script )
self._scripts.SetNonDupeName( script )
( display_tuple, data_tuple ) = self._ConvertScriptToTuples( script )
( display_tuple, sort_tuple ) = self._ConvertScriptToTuples( script )
self._scripts.Append( display_tuple, data_tuple )
self._scripts.Append( display_tuple, sort_tuple, script )
else:
@ -1636,29 +1676,6 @@ class ManageParsingScriptsPanel( ClientGUIScrolledPanels.ManagePanel ):
def _SetNonDupeName( self, script ):
name = script.GetName()
current_names = { script.GetName() for ( script, query_type, script_type, produces ) in self._scripts.GetClientData() }
if name in current_names:
i = 1
original_name = name
while name in current_names:
name = original_name + ' (' + str( i ) + ')'
i += 1
script.SetName( name )
def AddFileLookupScript( self ):
name = 'new script'
@ -1691,43 +1708,20 @@ class ManageParsingScriptsPanel( ClientGUIScrolledPanels.ManagePanel ):
new_script = panel.GetValue()
self._SetNonDupeName( new_script )
self._scripts.SetNonDupeName( new_script )
( display_tuple, data_tuple ) = self._ConvertScriptToTuples( new_script )
( display_tuple, sort_tuple ) = self._ConvertScriptToTuples( new_script )
self._scripts.Append( display_tuple, data_tuple )
self._scripts.Append( display_tuple, sort_tuple, new_script )
def CommitChanges( self ):
scripts = [ script for ( script, query_type, script_type, produces ) in self._scripts.GetClientData() ]
scripts = self._scripts.GetClientData()
file_lookup_scripts = [ script for script in scripts if isinstance( script, ClientParsing.ParseRootFileLookup ) ]
stuff_to_save = []
stuff_to_save.append( ( HydrusSerialisable.SERIALISABLE_TYPE_PARSE_ROOT_FILE_LOOKUP, file_lookup_scripts ) )
for ( serialisable_type, scripts ) in stuff_to_save:
existing_names = set( HydrusGlobals.client_controller.Read( 'serialisable_names', serialisable_type ) )
save_names = { script.GetName() for script in scripts }
for script in scripts:
HydrusGlobals.client_controller.Write( 'serialisable', script )
deletee_names = existing_names.difference( save_names )
for name in deletee_names:
HydrusGlobals.client_controller.Write( 'delete_serialisable_named', serialisable_type, name )
HydrusGlobals.client_controller.Write( 'serialisables_overwrite', self.SCRIPT_TYPES, scripts )
def Delete( self ):
@ -1737,24 +1731,17 @@ class ManageParsingScriptsPanel( ClientGUIScrolledPanels.ManagePanel ):
def Duplicate( self ):
scripts_to_dupe = []
for i in self._scripts.GetAllSelected():
( script, query_type, script_type, produces ) = self._scripts.GetClientData( i )
scripts_to_dupe.append( script )
scripts_to_dupe = self._scripts.GetSelectedClientData()
for script in scripts_to_dupe:
dupe_script = script.Duplicate()
self._SetNonDupeName( dupe_script )
self._scripts.SetNonDupeName( dupe_script )
( display_tuple, data_tuple ) = self._ConvertScriptToTuples( dupe_script )
( display_tuple, sort_tuple ) = self._ConvertScriptToTuples( dupe_script )
self._scripts.Append( display_tuple, data_tuple )
self._scripts.Append( display_tuple, sort_tuple, dupe_script )
@ -1762,7 +1749,7 @@ class ManageParsingScriptsPanel( ClientGUIScrolledPanels.ManagePanel ):
for i in self._scripts.GetAllSelected():
( script, query_type, script_type, produces ) = self._scripts.GetClientData( i )
script = self._scripts.GetClientData( i )
if isinstance( script, ClientParsing.ParseRootFileLookup ):
@ -1783,16 +1770,14 @@ class ManageParsingScriptsPanel( ClientGUIScrolledPanels.ManagePanel ):
edited_script = panel.GetValue()
name = edited_script.GetName()
if name != original_name:
if edited_script.GetName() != original_name:
self._SetNonDupeName( edited_script )
self._scripts.SetNonDupeName( edited_script )
( display_tuple, data_tuple ) = self._ConvertScriptToTuples( edited_script )
( display_tuple, sort_tuple ) = self._ConvertScriptToTuples( edited_script )
self._scripts.UpdateRow( i, display_tuple, data_tuple )
self._scripts.UpdateRow( i, display_tuple, sort_tuple, edited_script )

View File

@ -36,7 +36,7 @@ class PanelPredicateSystemAge( PanelPredicateSystem ):
PanelPredicateSystem.__init__( self, parent )
self._sign = wx.Choice( self, choices=[ '<', u'\u2248', '>' ] )
self._sign = wx.RadioBox( self, choices = [ '<', u'\u2248', '>' ] )
self._years = wx.SpinCtrl( self, max = 30, size = ( 60, -1 ) )
self._months = wx.SpinCtrl( self, max = 60, size = ( 60, -1 ) )
@ -87,7 +87,9 @@ class PanelPredicateSystemDuration( PanelPredicateSystem ):
PanelPredicateSystem.__init__( self, parent )
self._sign = wx.Choice( self, choices=[ '<', u'\u2248', '=', '>' ] )
choices = [ '<', u'\u2248', '=', '>' ]
self._sign = wx.RadioBox( self, choices = choices, style = wx.RA_SPECIFY_COLS )
self._duration_s = wx.SpinCtrl( self, max = 3599, size = ( 60, -1 ) )
self._duration_ms = wx.SpinCtrl( self, max = 999, size = ( 60, -1 ) )
@ -134,23 +136,15 @@ class PanelPredicateSystemFileService( PanelPredicateSystem ):
PanelPredicateSystem.__init__( self, parent )
self._sign = wx.Choice( self )
self._sign.Append( 'is', True )
self._sign.Append( 'is not', False )
self._sign = ClientGUICommon.BetterRadioBox( self, choices = [ ( 'is', True ), ( 'is not', False ) ], style = wx.RA_SPECIFY_ROWS )
self._current_pending = wx.Choice( self )
self._current_pending.Append( 'currently in', HC.CURRENT )
self._current_pending.Append( 'pending to', HC.PENDING )
self._file_service_key = wx.Choice( self )
self._sign.SetSelection( 0 )
self._current_pending.SetSelection( 0 )
self._current_pending = ClientGUICommon.BetterRadioBox( self, choices = [ ( 'currently in', HC.CURRENT ), ( 'pending to', HC.PENDING ) ], style = wx.RA_SPECIFY_ROWS )
services = HydrusGlobals.client_controller.GetServicesManager().GetServices( HC.FILE_SERVICES )
for service in services: self._file_service_key.Append( service.GetName(), service.GetServiceKey() )
self._file_service_key.SetSelection( 0 )
choices = [ ( service.GetName(), service.GetServiceKey() ) for service in services ]
self._file_service_key = ClientGUICommon.BetterRadioBox( self, choices = choices, style = wx.RA_SPECIFY_ROWS )
hbox = wx.BoxSizer( wx.HORIZONTAL )
@ -166,7 +160,8 @@ class PanelPredicateSystemFileService( PanelPredicateSystem ):
def GetInfo( self ):
info = ( self._sign.GetClientData( self._sign.GetSelection() ), self._current_pending.GetClientData( self._current_pending.GetSelection() ), self._file_service_key.GetClientData( self._file_service_key.GetSelection() ) )
info = ( self._sign.GetChoice(), self._current_pending.GetChoice(), self._file_service_key.GetChoice() )
return info
@ -178,15 +173,13 @@ class PanelPredicateSystemHash( PanelPredicateSystem ):
PanelPredicateSystem.__init__( self, parent )
self._hash = wx.TextCtrl( self )
self.SetToolTipString( 'As this can only ever return one result, it overrules the active file domain and any other active predicate.' )
self._hash_type = ClientGUICommon.BetterChoice( self )
self._hash_type.Append( 'sha256', 'sha256' )
self._hash_type.Append( 'md5', 'md5' )
self._hash_type.Append( 'sha1', 'sha1' )
self._hash_type.Append( 'sha512', 'sha512' )
self._hash = wx.TextCtrl( self, size = ( 200, -1 ) )
self._hash_type.SetSelection( 0 )
choices = [ 'sha256', 'md5', 'sha1', 'sha512' ]
self._hash_type = wx.RadioBox( self, choices = choices, style = wx.RA_SPECIFY_COLS )
hbox = wx.BoxSizer( wx.HORIZONTAL )
@ -210,7 +203,7 @@ class PanelPredicateSystemHash( PanelPredicateSystem ):
hash = hash.decode( 'hex' )
hash_type = self._hash_type.GetChoice()
hash_type = self._hash_type.GetStringSelection()
return ( hash, hash_type )
@ -223,7 +216,7 @@ class PanelPredicateSystemHeight( PanelPredicateSystem ):
PanelPredicateSystem.__init__( self, parent )
self._sign = wx.Choice( self, choices=[ '<', u'\u2248', '=', '>' ] )
self._sign = wx.RadioBox( self, choices = [ '<', u'\u2248', '=', '>' ] )
self._height = wx.SpinCtrl( self, max = 200000, size = ( 60, -1 ) )
@ -330,11 +323,11 @@ class PanelPredicateSystemNumPixels( PanelPredicateSystem ):
PanelPredicateSystem.__init__( self, parent )
self._sign = wx.Choice( self, choices = [ '<', u'\u2248', '=', '>' ] )
self._sign = wx.RadioBox( self, choices = [ '<', u'\u2248', '=', '>' ] )
self._num_pixels = wx.SpinCtrl( self, max = 1048576, size = ( 60, -1 ) )
self._unit = wx.Choice( self, choices = [ 'pixels', 'kilopixels', 'megapixels' ] )
self._unit = wx.RadioBox( self, choices = [ 'pixels', 'kilopixels', 'megapixels' ] )
system_predicates = HC.options[ 'file_system_predicates' ]
@ -373,7 +366,7 @@ class PanelPredicateSystemNumTags( PanelPredicateSystem ):
PanelPredicateSystem.__init__( self, parent )
self._sign = wx.Choice( self, choices=[ '<', u'\u2248', '=', '>' ] )
self._sign = wx.RadioBox( self, choices = [ '<', u'\u2248', '=', '>' ] )
self._num_tags = wx.SpinCtrl( self, max = 2000, size = ( 60, -1 ) )
@ -411,7 +404,7 @@ class PanelPredicateSystemNumWords( PanelPredicateSystem ):
PanelPredicateSystem.__init__( self, parent )
self._sign = wx.Choice( self, choices=[ '<', u'\u2248', '=', '>' ] )
self._sign = wx.RadioBox( self, choices = [ '<', u'\u2248', '=', '>' ] )
self._num_words = wx.SpinCtrl( self, max = 1000000, size = ( 60, -1 ) )
@ -457,9 +450,9 @@ class PanelPredicateSystemRating( PanelPredicateSystem ):
self._like_rating_ctrls = []
gridbox_like = wx.FlexGridSizer( 0, 4 )
gridbox = wx.FlexGridSizer( 0, 5 )
gridbox_like.AddGrowableCol( 0, 1 )
gridbox.AddGrowableCol( 0, 1 )
for service in local_like_services:
@ -474,10 +467,11 @@ class PanelPredicateSystemRating( PanelPredicateSystem ):
self._like_checkboxes_to_info[ not_rated_checkbox ] = ( service_key, ClientRatings.NULL )
self._like_rating_ctrls.append( rating_ctrl )
gridbox_like.AddF( wx.StaticText( self, label = name ), CC.FLAGS_VCENTER )
gridbox_like.AddF( rated_checkbox, CC.FLAGS_VCENTER )
gridbox_like.AddF( not_rated_checkbox, CC.FLAGS_VCENTER )
gridbox_like.AddF( rating_ctrl, CC.FLAGS_VCENTER )
gridbox.AddF( wx.StaticText( self, label = name ), CC.FLAGS_VCENTER )
gridbox.AddF( rated_checkbox, CC.FLAGS_VCENTER )
gridbox.AddF( not_rated_checkbox, CC.FLAGS_VCENTER )
gridbox.AddF( ( 20, 20 ), CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
gridbox.AddF( rating_ctrl, CC.FLAGS_VCENTER )
#
@ -488,10 +482,6 @@ class PanelPredicateSystemRating( PanelPredicateSystem ):
self._numerical_rating_ctrls_to_info = {}
gridbox_numerical = wx.FlexGridSizer( 0, 5 )
gridbox_numerical.AddGrowableCol( 0, 1 )
for service in local_numerical_services:
name = service.GetName()
@ -499,28 +489,27 @@ class PanelPredicateSystemRating( PanelPredicateSystem ):
rated_checkbox = wx.CheckBox( self, label = 'rated' )
not_rated_checkbox = wx.CheckBox( self, label = 'not rated' )
choice = wx.Choice( self, choices=[ '>', '<', '=', u'\u2248' ] )
choice = wx.RadioBox( self, choices = [ '>', '<', '=', u'\u2248' ] )
rating_ctrl = ClientGUICommon.RatingNumericalDialog( self, service_key )
choice.Select( 2 )
choice.SetSelection( 2 )
self._numerical_checkboxes_to_info[ rated_checkbox ] = ( service_key, ClientRatings.SET )
self._numerical_checkboxes_to_info[ not_rated_checkbox ] = ( service_key, ClientRatings.NULL )
self._numerical_rating_ctrls_to_info[ rating_ctrl ] = choice
gridbox_numerical.AddF( wx.StaticText( self, label = name ), CC.FLAGS_VCENTER )
gridbox_numerical.AddF( rated_checkbox, CC.FLAGS_VCENTER )
gridbox_numerical.AddF( not_rated_checkbox, CC.FLAGS_VCENTER )
gridbox_numerical.AddF( choice, CC.FLAGS_VCENTER )
gridbox_numerical.AddF( rating_ctrl, CC.FLAGS_VCENTER )
gridbox.AddF( wx.StaticText( self, label = name ), CC.FLAGS_VCENTER )
gridbox.AddF( rated_checkbox, CC.FLAGS_VCENTER )
gridbox.AddF( not_rated_checkbox, CC.FLAGS_VCENTER )
gridbox.AddF( choice, CC.FLAGS_VCENTER )
gridbox.AddF( rating_ctrl, CC.FLAGS_VCENTER )
#
vbox = wx.BoxSizer( wx.VERTICAL )
vbox.AddF( gridbox_like, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
vbox.AddF( gridbox_numerical, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
vbox.AddF( gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self.SetSizer( vbox )
@ -626,7 +615,7 @@ class PanelPredicateSystemRatio( PanelPredicateSystem ):
PanelPredicateSystem.__init__( self, parent )
self._sign = wx.Choice( self, choices = [ '=', 'wider than', 'taller than', u'\u2248' ] )
self._sign = wx.RadioBox( self, choices = [ '=', 'wider than', 'taller than', u'\u2248' ] )
self._width = wx.SpinCtrl( self, max = 50000, size = ( 60, -1 ) )
@ -716,11 +705,11 @@ class PanelPredicateSystemSize( PanelPredicateSystem ):
PanelPredicateSystem.__init__( self, parent )
self._sign = wx.Choice( self, choices = [ '<', u'\u2248', '=', '>' ] )
self._sign = wx.RadioBox( self, choices = [ '<', u'\u2248', '=', '>' ] )
self._size = wx.SpinCtrl( self, max = 1048576, size = ( 60, -1 ) )
self._unit = wx.Choice( self, choices = [ 'B', 'KB', 'MB', 'GB' ] )
self._unit = wx.RadioBox( self, choices = [ 'B', 'KB', 'MB', 'GB' ] )
system_predicates = HC.options[ 'file_system_predicates' ]
@ -759,7 +748,7 @@ class PanelPredicateSystemWidth( PanelPredicateSystem ):
PanelPredicateSystem.__init__( self, parent )
self._sign = wx.Choice( self, choices = [ '<', u'\u2248', '=', '>' ] )
self._sign = wx.RadioBox( self, choices = [ '<', u'\u2248', '=', '>' ] )
self._width = wx.SpinCtrl( self, max = 200000, size = ( 60, -1 ) )

View File

@ -422,12 +422,10 @@ class EditSubscriptionPanel( ClientGUIScrolledPanels.EditPanel ):
#
name = subscription.GetName()
( name, gallery_identifier, gallery_stream_identifiers, query, period, get_tags_if_redundant, initial_file_limit, periodic_file_limit, paused, import_file_options, import_tag_options, self._last_checked, self._last_error, self._check_now, self._seed_cache ) = subscription.ToTuple()
self._name.SetValue( name )
( gallery_identifier, gallery_stream_identifiers, query, period, get_tags_if_redundant, initial_file_limit, periodic_file_limit, paused, import_file_options, import_tag_options, self._last_checked, self._last_error, self._check_now, self._seed_cache ) = subscription.ToTuple()
site_type = gallery_identifier.GetSiteType()
self._site_type.SelectClientData( site_type )

View File

@ -1282,7 +1282,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._media_viewer_panel = ClientGUICommon.StaticBox( self, 'media viewer mime handling' )
self._media_viewer_options = ClientGUICommon.SaneListCtrl( self._media_viewer_panel, 300, [ ( 'mime', 150 ), ( 'media show action', 140 ), ( 'preview show action', 140 ), ( 'zoom info', -1 ) ], activation_callback = self.EditMediaViewerOptions, use_display_tuple_for_sort = True )
self._media_viewer_options = ClientGUICommon.SaneListCtrlForSingleObject( self._media_viewer_panel, 300, [ ( 'mime', 150 ), ( 'media show action', 140 ), ( 'preview show action', 140 ), ( 'zoom info', -1 ) ], activation_callback = self.EditMediaViewerOptions )
self._media_viewer_edit_button = wx.Button( self._media_viewer_panel, label = 'edit' )
self._media_viewer_edit_button.Bind( wx.EVT_BUTTON, self.EventEditMediaViewerOptions )
@ -1304,10 +1304,11 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
items = self._new_options.GetMediaViewOptions( mime )
listctrl_list = [ mime ] + list( items )
pretty_listctrl_list = self._GetPrettyMediaViewOptions( listctrl_list )
data = [ mime ] + list( items )
self._media_viewer_options.Append( pretty_listctrl_list, listctrl_list )
( display_tuple, sort_tuple, data ) = self._GetListCtrlData( data )
self._media_viewer_options.Append( display_tuple, sort_tuple, data )
self._media_viewer_options.SortListItems( col = 0 )
@ -1336,9 +1337,12 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self.SetSizer( vbox )
def _GetPrettyMediaViewOptions( self, listctrl_list ):
def _GetListCtrlData( self, data ):
( mime, media_show_action, preview_show_action, zoom_info ) = listctrl_list
( mime, media_show_action, preview_show_action, zoom_info ) = data
# can't store a list in the listctrl obj space, as it is unhashable
data = ( mime, media_show_action, preview_show_action, tuple( zoom_info ) )
pretty_mime = HC.mime_string_lookup[ mime ]
pretty_media_show_action = CC.media_viewer_action_string_lookup[ media_show_action ]
@ -1357,29 +1361,33 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
pretty_zoom_info = str( zoom_info )
return ( pretty_mime, pretty_media_show_action, pretty_preview_show_action, pretty_zoom_info )
display_tuple = ( pretty_mime, pretty_media_show_action, pretty_preview_show_action, pretty_zoom_info )
sort_tuple = ( pretty_mime, pretty_media_show_action, pretty_preview_show_action, pretty_zoom_info )
return ( display_tuple, sort_tuple, data )
def EditMediaViewerOptions( self ):
for i in self._media_viewer_options.GetAllSelected():
listctrl_list = self._media_viewer_options.GetClientData( i )
data = self._media_viewer_options.GetClientData( i )
title = 'set media view options information'
with ClientGUITopLevelWindows.DialogEdit( self, title ) as dlg:
panel = ClientGUIScrolledPanelsEdit.EditMediaViewOptionsPanel( dlg, listctrl_list )
panel = ClientGUIScrolledPanelsEdit.EditMediaViewOptionsPanel( dlg, data )
dlg.SetPanel( panel )
if dlg.ShowModal() == wx.ID_OK:
new_listctrl_list = panel.GetValue()
pretty_new_listctrl_list = self._GetPrettyMediaViewOptions( new_listctrl_list )
new_data = panel.GetValue()
self._media_viewer_options.UpdateRow( i, pretty_new_listctrl_list, new_listctrl_list )
( display_tuple, sort_tuple, new_data ) = self._GetListCtrlData( new_data )
self._media_viewer_options.UpdateRow( i, display_tuple, sort_tuple, new_data )
@ -1428,13 +1436,13 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
HydrusData.ShowText( 'Could not parse those zooms, so they were not saved!' )
for listctrl_list in self._media_viewer_options.GetClientData():
for data in self._media_viewer_options.GetClientData():
listctrl_list = list( listctrl_list )
data = list( data )
mime = listctrl_list[0]
mime = data[0]
value = listctrl_list[1:]
value = data[1:]
self._new_options.SetMediaViewOptions( mime, value )
@ -2358,7 +2366,7 @@ class ManageSubscriptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
columns = [ ( 'name', -1 ), ( 'site', 80 ), ( 'period', 80 ), ( 'last checked', 100 ), ( 'recent error?', 100 ), ( 'urls', 60 ), ( 'failures', 60 ), ( 'paused', 80 ), ( 'check now?', 100 ) ]
self._subscriptions = ClientGUICommon.SaneListCtrl( self, 300, columns, delete_key_callback = self.Delete, activation_callback = self.Edit, use_display_tuple_for_sort = True )
self._subscriptions = ClientGUICommon.SaneListCtrlForSingleObject( self, 300, columns, delete_key_callback = self.Delete, activation_callback = self.Edit )
self._add = ClientGUICommon.BetterButton( self, 'add', self.Add )
@ -2389,9 +2397,9 @@ class ManageSubscriptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
for subscription in subscriptions:
( display_tuple, data_tuple ) = self._ConvertSubscriptionToTuples( subscription )
( display_tuple, sort_tuple ) = self._ConvertSubscriptionToTuples( subscription )
self._subscriptions.Append( display_tuple, data_tuple )
self._subscriptions.Append( display_tuple, sort_tuple, subscription )
#
@ -2429,16 +2437,59 @@ class ManageSubscriptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
def _ConvertSubscriptionToTuples( self, subscription ):
( name, site, period, last_checked, recent_error, urls, failures, paused, check_now ) = subscription.ToPrettyStrings()
( name, gallery_identifier, gallery_stream_identifiers, query, period, get_tags_if_redundant, initial_file_limit, periodic_file_limit, paused, import_file_options, import_tag_options, last_checked, last_error, check_now, seed_cache ) = subscription.ToTuple()
return ( ( name, site, period, last_checked, recent_error, urls, failures, paused, check_now ), ( subscription, site, period, last_checked, recent_error, urls, failures, paused, check_now ) )
pretty_site = HC.site_type_string_lookup[ gallery_identifier.GetSiteType() ]
pretty_last_checked = HydrusData.ConvertTimestampToPrettySync( last_checked )
pretty_period = HydrusData.ConvertTimeDeltaToPrettyString( period )
error_next_check_time = last_error + HC.UPDATE_DURATION
if HydrusData.TimeHasPassed( error_next_check_time ):
pretty_error = ''
else:
pretty_error = 'yes'
num_urls = seed_cache.GetSeedCount()
pretty_urls = HydrusData.ConvertIntToPrettyString( num_urls )
num_failures = seed_cache.GetSeedCount( CC.STATUS_FAILED )
pretty_failures = HydrusData.ConvertIntToPrettyString( num_failures )
if paused:
pretty_paused = 'yes'
else:
pretty_paused = ''
if check_now:
pretty_check_now = 'yes'
else:
pretty_check_now = ''
display_tuple = ( name, pretty_site, pretty_period, pretty_last_checked, pretty_error, pretty_urls, pretty_failures, pretty_paused, pretty_check_now )
sort_tuple = ( name, pretty_site, period, last_checked, pretty_error, num_urls, num_failures, paused, check_now )
return ( display_tuple, sort_tuple )
def _GetExportObject( self ):
to_export = HydrusSerialisable.SerialisableList()
for subscription in self._GetSubscriptions( only_selected = True ):
for subscription in self._subscriptions.GetSelectedClientData():
to_export.append( subscription )
@ -2457,32 +2508,6 @@ class ManageSubscriptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
def _GetSubscriptions( self, only_selected = False ):
subscriptions = []
if only_selected:
for i in self._subscriptions.GetAllSelected():
subscription = self._subscriptions.GetClientData( i )[0]
subscriptions.append( subscription )
else:
for row in self._subscriptions.GetClientData():
subscription = row[0]
subscriptions.append( subscription )
return subscriptions
def _ImportObject( self, obj ):
if isinstance( obj, HydrusSerialisable.SerialisableList ):
@ -2498,11 +2523,11 @@ class ManageSubscriptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
subscription = obj
self._SetNonDupeName( subscription )
self._subscriptions.SetNonDupeName( subscription )
( display_tuple, data_tuple ) = self._ConvertSubscriptionToTuples( subscription )
( display_tuple, sort_tuple ) = self._ConvertSubscriptionToTuples( subscription )
self._subscriptions.Append( display_tuple, data_tuple )
self._subscriptions.Append( display_tuple, sort_tuple, subscription )
else:
@ -2511,29 +2536,6 @@ class ManageSubscriptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
def _SetNonDupeName( self, subscription ):
name = subscription.GetName()
current_names = { s.GetName() for s in self._GetSubscriptions() }
if name in current_names:
i = 1
original_name = name
while name in current_names:
name = original_name + ' (' + str( i ) + ')'
i += 1
subscription.SetName( name )
def Add( self ):
empty_subscription = ClientImporting.Subscription( 'new subscription' )
@ -2548,11 +2550,11 @@ class ManageSubscriptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
new_subscription = panel.GetValue()
self._SetNonDupeName( new_subscription )
self._subscriptions.SetNonDupeName( new_subscription )
( display_tuple, data_tuple ) = self._ConvertSubscriptionToTuples( new_subscription )
( display_tuple, sort_tuple ) = self._ConvertSubscriptionToTuples( new_subscription )
self._subscriptions.Append( display_tuple, data_tuple )
self._subscriptions.Append( display_tuple, sort_tuple, new_subscription )
@ -2561,35 +2563,21 @@ class ManageSubscriptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
for i in self._subscriptions.GetAllSelected():
subscription = self._subscriptions.GetClientData( i )[0]
subscription = self._subscriptions.GetClientData( i )
subscription.CheckNow()
( display_tuple, data_tuple ) = self._ConvertSubscriptionToTuples( subscription )
( display_tuple, sort_tuple ) = self._ConvertSubscriptionToTuples( subscription )
self._subscriptions.UpdateRow( i, display_tuple, data_tuple )
self._subscriptions.UpdateRow( i, display_tuple, sort_tuple, subscription )
def CommitChanges( self ):
existing_db_names = set( HydrusGlobals.client_controller.Read( 'serialisable_names', HydrusSerialisable.SERIALISABLE_TYPE_SUBSCRIPTION ) )
subscriptions = self._subscriptions.GetClientData()
subscriptions = self._GetSubscriptions()
save_names = { subscription.GetName() for subscription in subscriptions }
deletee_names = existing_db_names.difference( save_names )
for subscription in subscriptions:
HydrusGlobals.client_controller.Write( 'serialisable', subscription )
for name in deletee_names:
HydrusGlobals.client_controller.Write( 'delete_serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SUBSCRIPTION, name )
HydrusGlobals.client_controller.Write( 'serialisables_overwrite', [ HydrusSerialisable.SERIALISABLE_TYPE_SUBSCRIPTION ], subscriptions )
HydrusGlobals.client_controller.pub( 'notify_new_subscriptions' )
@ -2603,7 +2591,7 @@ class ManageSubscriptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
subs_to_dupe = []
for subscription in self._GetSubscriptions( only_selected = True ):
for subscription in self._subscriptions.GetSelectedClientData():
subs_to_dupe.append( subscription )
@ -2612,11 +2600,11 @@ class ManageSubscriptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
dupe_subscription = subscription.Duplicate()
self._SetNonDupeName( dupe_subscription )
self._subscriptions.SetNonDupeName( dupe_subscription )
( display_tuple, data_tuple ) = self._ConvertSubscriptionToTuples( dupe_subscription )
( display_tuple, sort_tuple ) = self._ConvertSubscriptionToTuples( dupe_subscription )
self._subscriptions.Append( display_tuple, data_tuple )
self._subscriptions.Append( display_tuple, sort_tuple, dupe_subscription )
@ -2624,7 +2612,7 @@ class ManageSubscriptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
for i in self._subscriptions.GetAllSelected():
subscription = self._subscriptions.GetClientData( i )[0]
subscription = self._subscriptions.GetClientData( i )
with ClientGUITopLevelWindows.DialogEdit( self, 'edit subscription' ) as dlg:
@ -2638,16 +2626,14 @@ class ManageSubscriptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
edited_subscription = panel.GetValue()
name = edited_subscription.GetName()
if name != original_name:
if edited_subscription.GetName() != original_name:
self._SetNonDupeName( edited_subscription )
self._subscriptions.SetNonDupeName( edited_subscription )
( display_tuple, data_tuple ) = self._ConvertSubscriptionToTuples( edited_subscription )
( display_tuple, sort_tuple ) = self._ConvertSubscriptionToTuples( edited_subscription )
self._subscriptions.UpdateRow( i, display_tuple, data_tuple )
self._subscriptions.UpdateRow( i, display_tuple, sort_tuple, edited_subscription )
@ -2749,13 +2735,13 @@ class ManageSubscriptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
for i in self._subscriptions.GetAllSelected():
subscription = self._subscriptions.GetClientData( i )[0]
subscription = self._subscriptions.GetClientData( i )
subscription.PauseResume()
( display_tuple, data_tuple ) = self._ConvertSubscriptionToTuples( subscription )
( display_tuple, sort_tuple ) = self._ConvertSubscriptionToTuples( subscription )
self._subscriptions.UpdateRow( i, display_tuple, data_tuple )
self._subscriptions.UpdateRow( i, display_tuple, sort_tuple, subscription )
@ -2769,13 +2755,13 @@ class ManageSubscriptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
for i in self._subscriptions.GetAllSelected():
subscription = self._subscriptions.GetClientData( i )[0]
subscription = self._subscriptions.GetClientData( i )
subscription.Reset()
( display_tuple, data_tuple ) = self._ConvertSubscriptionToTuples( subscription )
( display_tuple, sort_tuple ) = self._ConvertSubscriptionToTuples( subscription )
self._subscriptions.UpdateRow( i, display_tuple, data_tuple )
self._subscriptions.UpdateRow( i, display_tuple, sort_tuple, subscription )
@ -2785,7 +2771,7 @@ class ManageSubscriptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
for i in self._subscriptions.GetAllSelected():
subscription = self._subscriptions.GetClientData( i )[0]
subscription = self._subscriptions.GetClientData( i )
seed_cache = subscription.GetSeedCache()
@ -2795,9 +2781,9 @@ class ManageSubscriptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
seed_cache.UpdateSeedStatus( seed, CC.STATUS_UNKNOWN )
( display_tuple, data_tuple ) = self._ConvertSubscriptionToTuples( subscription )
( display_tuple, sort_tuple ) = self._ConvertSubscriptionToTuples( subscription )
self._subscriptions.UpdateRow( i, display_tuple, data_tuple )
self._subscriptions.UpdateRow( i, display_tuple, sort_tuple, subscription )

View File

@ -2558,51 +2558,9 @@ class Subscription( HydrusSerialisable.SerialisableBaseNamed ):
def ToPrettyStrings( self ):
site = HC.site_type_string_lookup[ self._gallery_identifier.GetSiteType() ]
last_checked = HydrusData.ConvertTimestampToPrettySync( self._last_checked )
period = HydrusData.ConvertTimeDeltaToPrettyString( self._period )
error_next_check_time = self._last_error + HC.UPDATE_DURATION
if HydrusData.TimeHasPassed( error_next_check_time ):
error_text = ''
else:
error_text = 'yes'
urls = HydrusData.ConvertIntToPrettyString( self._seed_cache.GetSeedCount() )
failures = HydrusData.ConvertIntToPrettyString( self._seed_cache.GetSeedCount( CC.STATUS_FAILED ) )
if self._paused:
paused_text = 'yes'
else:
paused_text = ''
if self._check_now:
check_now_text = 'yes'
else:
check_now_text = ''
return ( self._name, site, period, last_checked, error_text, urls, failures, paused_text, check_now_text )
def ToTuple( self ):
return ( self._gallery_identifier, self._gallery_stream_identifiers, self._query, self._period, self._get_tags_if_redundant, self._initial_file_limit, self._periodic_file_limit, self._paused, self._import_file_options, self._import_tag_options, self._last_checked, self._last_error, self._check_now, self._seed_cache )
return ( self._name, self._gallery_identifier, self._gallery_stream_identifiers, self._query, self._period, self._get_tags_if_redundant, self._initial_file_limit, self._periodic_file_limit, self._paused, self._import_file_options, self._import_tag_options, self._last_checked, self._last_error, self._check_now, self._seed_cache )
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_SUBSCRIPTION ] = Subscription

View File

@ -804,15 +804,19 @@ class MediaList( object ):
if action == HC.CONTENT_UPDATE_DELETE:
local_service_keys = ( CC.TRASH_SERVICE_KEY, CC.LOCAL_FILE_SERVICE_KEY )
local_file_domains = HydrusGlobals.client_controller.GetServicesManager().GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )
deleted_from_trash_and_local_view = service_key == CC.TRASH_SERVICE_KEY and self._file_service_key in local_service_keys
non_trash_local_file_services = list( local_file_domains ) + [ CC.COMBINED_LOCAL_FILE_SERVICE_KEY ]
deleted_from_local_and_option_set = HC.options[ 'remove_trashed_files' ] and service_key == CC.LOCAL_FILE_SERVICE_KEY and self._file_service_key in local_service_keys
local_file_services = list( non_trash_local_file_services ) + [ CC.TRASH_SERVICE_KEY ]
deleted_from_repo_and_repo_view = service_key not in local_service_keys and self._file_service_key == service_key
deleted_from_trash_and_local_view = service_key == CC.TRASH_SERVICE_KEY and self._file_service_key in local_file_services
if deleted_from_trash_and_local_view or deleted_from_local_and_option_set or deleted_from_repo_and_repo_view:
trashed_and_non_trash_local_view = HC.options[ 'remove_trashed_files' ] and service_key in non_trash_local_file_services and self._file_service_key in non_trash_local_file_services
deleted_from_repo_and_repo_view = service_key not in local_file_services and self._file_service_key == service_key
if deleted_from_trash_and_local_view or trashed_and_non_trash_local_view or deleted_from_repo_and_repo_view:
affected_singleton_media = self._GetMedia( hashes, 'singletons' )
affected_collected_media = [ media for media in self._collected_media if media.HasNoMedia() ]

View File

@ -287,8 +287,6 @@ class JobKey( object ):
if 'popup_traceback' in self._variables: stuff_to_print.append( self._variables[ 'popup_traceback' ] )
if 'popup_caller_traceback' in self._variables: stuff_to_print.append( self._variables[ 'popup_caller_traceback' ] )
stuff_to_print = [ HydrusData.ToUnicode( s ) for s in stuff_to_print ]
@ -347,4 +345,4 @@ class JobKey( object ):
return ( i_paused, should_quit )

View File

@ -46,7 +46,7 @@ options = {}
# Misc
NETWORK_VERSION = 17
SOFTWARE_VERSION = 238
SOFTWARE_VERSION = 239
UNSCALED_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -879,6 +879,10 @@ def LastShutdownWasBad( db_path, instance ):
return False
def MassUnion( lists ):
return { item for item in itertools.chain.from_iterable( lists ) }
def MedianPop( population ):
@ -907,7 +911,7 @@ def Print( text ):
ShowText = Print
def PrintException( e ):
def PrintException( e, do_wait = True ):
if isinstance( e, HydrusExceptions.ShutdownException ):
@ -929,7 +933,10 @@ def PrintException( e ):
DebugPrint( message )
time.sleep( 1 )
if do_wait:
time.sleep( 1 )
ShowException = PrintException

View File

@ -2,7 +2,9 @@ import Crypto.Cipher.AES
import Crypto.Cipher.PKCS1_OAEP
import Crypto.PublicKey.RSA
import HydrusConstants as HC
import OpenSSL
import os
import socket
import traceback
AES_KEY_LENGTH = 32
@ -137,6 +139,37 @@ def GenerateFilteredRandomBytes( byte_to_exclude, num_bytes ):
return ''.join( bytes )
def GenerateOpenSSLCertAndKeyFile( cert_path, key_path ):
key = OpenSSL.crypto.PKey()
key.generate_key( OpenSSL.crypto.TYPE_RSA, 2048 )
# create a self-signed cert
cert = OpenSSL.crypto.X509()
cert.get_subject().C = 'US'
cert.set_serial_number( 1 )
cert.gmtime_adj_notBefore( 0 )
cert.gmtime_adj_notAfter( 10*365*24*60*60 )
cert.set_issuer( cert.get_subject() )
cert.set_pubkey( key )
cert.sign( key, 'sha256' )
cert_text = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, cert )
with open( cert_path, 'wt' ) as f:
f.write( cert_text )
key_text = OpenSSL.crypto.dump_privatekey( OpenSSL.crypto.FILETYPE_PEM, key )
with open( key_path, 'wt' ) as f:
f.write( key_text )
def GenerateRSAKeyPair():
private_key = Crypto.PublicKey.RSA.generate( 2048 )

View File

@ -252,7 +252,10 @@ def GetMime( path ):
mime = HydrusVideoHandling.GetMimeFromFFMPEG( path )
return mime
if mime != HC.APPLICATION_UNKNOWN:
return mime
except HydrusExceptions.MimeException:
@ -261,7 +264,7 @@ def GetMime( path ):
except Exception as e:
HydrusData.Print( 'FFMPEG couldn\'t figure out the mime for: ' + path )
HydrusData.PrintException( e )
HydrusData.PrintException( e, do_wait = False )
hsaudio_object = hsaudiotag.auto.File( path )

View File

@ -162,7 +162,7 @@ def GetMimeFromFFMPEG( path ):
raise Exception( 'FFMPEG could not find mime in ' + path + '!' )
return HC.APPLICATION_UNKNOWN
def HasVideoStream( path ):

View File

@ -198,11 +198,13 @@ class Controller( HydrusController.HydrusController ):
elif service_type == HC.TAG_REPOSITORY: service_object = ServerServer.HydrusServiceRepositoryTag( service_key, service_type, message )
elif service_type == HC.MESSAGE_DEPOT: return
#context_factory = twisted.internet.ssl.DefaultOpenSSLContextFactory( 'muh_key.key', 'muh_crt.crt' )
( ssl_cert_path, ssl_key_path ) = self._db.GetSSLPaths()
#self._services[ service_key ] = reactor.listenSSL( port, service_object, context_factory )
context_factory = twisted.internet.ssl.DefaultOpenSSLContextFactory( ssl_key_path, ssl_cert_path )
self._services[ service_key ] = reactor.listenTCP( port, service_object )
self._services[ service_key ] = reactor.listenSSL( port, service_object, context_factory )
#self._services[ service_key ] = reactor.listenTCP( port, service_object )
try:

View File

@ -3,6 +3,7 @@ import hashlib
import httplib
import HydrusConstants as HC
import HydrusDB
import HydrusEncryption
import HydrusExceptions
import HydrusFileHandling
import HydrusNATPunch
@ -107,6 +108,12 @@ class DB( HydrusDB.HydrusDB ):
self._files_dir = os.path.join( db_dir, 'server_files' )
self._updates_dir = os.path.join( db_dir, 'server_updates' )
self._ssl_cert_filename = 'server.crt'
self._ssl_key_filename = 'server.key'
self._ssl_cert_path = os.path.join( db_dir, self._ssl_cert_filename )
self._ssl_key_path = os.path.join( db_dir, self._ssl_key_filename )
HydrusDB.HydrusDB.__init__( self, controller, db_dir, db_name, no_wal = no_wal )
@ -320,7 +327,7 @@ class DB( HydrusDB.HydrusDB ):
for db_name in db_names:
all_names.update( ( name for ( name, ) in self._c.execute( 'SELECT name FROM ' + db_name + '.sqlite_master;' ) ) )
all_names.update( ( name for ( name, ) in self._c.execute( 'SELECT name FROM ' + db_name + '.sqlite_master WHERE type = ?;', ( 'table', ) ) ) )
all_names.discard( 'sqlite_stat1' )
@ -495,6 +502,16 @@ class DB( HydrusDB.HydrusDB ):
HydrusPaths.MirrorFile( source, dest )
for filename in [ self._ssl_cert_filename, self._ssl_key_filename ]:
HydrusData.Print( 'backing up: copying ' + filename )
source = os.path.join( self._db_dir, filename )
dest = os.path.join( backup_path, filename )
HydrusPaths.MirrorFile( source, dest )
HydrusData.Print( 'backing up: copying files' )
HydrusPaths.MirrorTree( self._files_dir, os.path.join( backup_path, 'server_files' ) )
@ -742,6 +759,10 @@ class DB( HydrusDB.HydrusDB ):
self._c.execute( 'INSERT INTO version ( version, year, month ) VALUES ( ?, ?, ? );', ( HC.SOFTWARE_VERSION, current_year, current_month ) )
# create ssl keys
HydrusEncryption.GenerateOpenSSLCertAndKeyFile( self._ssl_cert_path, self._ssl_key_path )
# set up server admin
service_key = HC.SERVER_ADMIN_KEY
@ -2570,6 +2591,13 @@ class DB( HydrusDB.HydrusDB ):
if version == 238:
HydrusData.Print( 'Generating SSL cert and key' )
HydrusEncryption.GenerateOpenSSLCertAndKeyFile( self._ssl_cert_path, self._ssl_key_path )
HydrusData.Print( 'The server has updated to version ' + str( version + 1 ) )
self._c.execute( 'UPDATE version SET version = ?;', ( version + 1, ) )
@ -2619,8 +2647,13 @@ class DB( HydrusDB.HydrusDB ):
return self._files_dir
def GetSSLPaths( self ):
return ( self._ssl_cert_path, self._ssl_key_path )
def GetUpdatesDir( self ):
return self._updates_dir

View File

@ -3,6 +3,7 @@ import ClientData
import ClientDB
import ClientDefaults
import ClientDownloading
import ClientExporting
import ClientFiles
import ClientGUIManagement
import ClientGUIPages
@ -51,7 +52,7 @@ class TestClientDB( unittest.TestCase ):
c = db.cursor()
table_names = [ name for ( name, ) in c.execute( 'SELECT name FROM sqlite_master where type = "table";' ).fetchall() ]
table_names = [ name for ( name, ) in c.execute( 'SELECT name FROM sqlite_master WHERE type = ?;', ( 'table', ) ).fetchall() ]
for name in table_names:
@ -228,7 +229,7 @@ class TestClientDB( unittest.TestCase ):
file_search_context = ClientSearch.FileSearchContext(file_service_key = HydrusData.GenerateKey(), tag_service_key = HydrusData.GenerateKey(), predicates = [ ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, 'test' ) ] )
export_folder = ClientFiles.ExportFolder( 'test path', export_type = HC.EXPORT_FOLDER_TYPE_REGULAR, file_search_context = file_search_context, period = 3600, phrase = '{hash}' )
export_folder = ClientExporting.ExportFolder( 'test path', export_type = HC.EXPORT_FOLDER_TYPE_REGULAR, file_search_context = file_search_context, period = 3600, phrase = '{hash}' )
self._write( 'serialisable', export_folder )