703 lines
20 KiB
Python
703 lines
20 KiB
Python
import ClientConstants as CC
|
|
import ClientDefaults
|
|
import ClientDownloading
|
|
import ClientThreading
|
|
import collections
|
|
import HydrusConstants as HC
|
|
import HydrusData
|
|
import HydrusExceptions
|
|
import HydrusGlobals as HG
|
|
import HydrusPaths
|
|
import HydrusSerialisable
|
|
import HydrusTags
|
|
import threading
|
|
import traceback
|
|
import os
|
|
import sqlite3
|
|
import sys
|
|
import time
|
|
import wx
|
|
import wx.lib.colourutils
|
|
import yaml
|
|
|
|
def AddPaddingToDimensions( dimensions, padding ):
|
|
|
|
( x, y ) = dimensions
|
|
|
|
return ( x + padding, y + padding )
|
|
|
|
def CatchExceptionClient( etype, value, tb ):
|
|
|
|
try:
|
|
|
|
trace_list = traceback.format_tb( tb )
|
|
|
|
trace = ''.join( trace_list )
|
|
|
|
pretty_value = HydrusData.ToUnicode( value )
|
|
|
|
if os.linesep in pretty_value:
|
|
|
|
( first_line, anything_else ) = pretty_value.split( os.linesep, 1 )
|
|
|
|
trace = trace + os.linesep + anything_else
|
|
|
|
else:
|
|
|
|
first_line = pretty_value
|
|
|
|
|
|
trace = HydrusData.ToUnicode( trace )
|
|
|
|
job_key = ClientThreading.JobKey()
|
|
|
|
if etype == HydrusExceptions.ShutdownException:
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
try: job_key.SetVariable( 'popup_title', HydrusData.ToUnicode( etype.__name__ ) )
|
|
except: job_key.SetVariable( 'popup_title', HydrusData.ToUnicode( etype ) )
|
|
|
|
job_key.SetVariable( 'popup_text_1', first_line )
|
|
job_key.SetVariable( 'popup_traceback', trace )
|
|
|
|
|
|
text = job_key.ToString()
|
|
|
|
HydrusData.Print( 'Uncaught exception:' )
|
|
|
|
HydrusData.DebugPrint( text )
|
|
|
|
HG.client_controller.pub( 'message', job_key )
|
|
|
|
except:
|
|
|
|
text = 'Encountered an error I could not parse:'
|
|
|
|
text += os.linesep
|
|
|
|
text += HydrusData.ToUnicode( ( etype, value, tb ) )
|
|
|
|
try: text += traceback.format_exc()
|
|
except: pass
|
|
|
|
HydrusData.ShowText( text )
|
|
|
|
|
|
time.sleep( 1 )
|
|
|
|
def ColourIsBright( colour ):
|
|
|
|
( r, g, b, a ) = colour.Get()
|
|
|
|
brightness_estimate = ( r + g + b ) // 3
|
|
|
|
it_is_bright = brightness_estimate > 127
|
|
|
|
return it_is_bright
|
|
|
|
def ColourIsGreyish( colour ):
|
|
|
|
( r, g, b, a ) = colour.Get()
|
|
|
|
greyish = r // 16 == g // 16 and g // 16 == b // 16
|
|
|
|
return greyish
|
|
|
|
def ConvertServiceKeysToContentUpdatesToPrettyString( service_keys_to_content_updates ):
|
|
|
|
num_files = 0
|
|
actions = set()
|
|
locations = set()
|
|
|
|
extra_words = ''
|
|
|
|
for ( service_key, content_updates ) in service_keys_to_content_updates.items():
|
|
|
|
if len( content_updates ) > 0:
|
|
|
|
name = HG.client_controller.services_manager.GetName( service_key )
|
|
|
|
locations.add( name )
|
|
|
|
|
|
for content_update in content_updates:
|
|
|
|
( data_type, action, row ) = content_update.ToTuple()
|
|
|
|
if data_type == HC.CONTENT_TYPE_MAPPINGS:
|
|
|
|
extra_words = ' tags for'
|
|
|
|
|
|
actions.add( HC.content_update_string_lookup[ action ] )
|
|
|
|
if action in ( HC.CONTENT_UPDATE_ARCHIVE, HC.CONTENT_UPDATE_INBOX ):
|
|
|
|
locations = set()
|
|
|
|
|
|
num_files += len( content_update.GetHashes() )
|
|
|
|
|
|
|
|
s = ''
|
|
|
|
if len( locations ) > 0:
|
|
|
|
s += ', '.join( locations ) + '->'
|
|
|
|
|
|
s += ', '.join( actions ) + extra_words + ' ' + HydrusData.ConvertIntToPrettyString( num_files ) + ' files'
|
|
|
|
return s
|
|
|
|
def ConvertServiceKeysToTagsToServiceKeysToContentUpdates( hashes, service_keys_to_tags ):
|
|
|
|
service_keys_to_content_updates = {}
|
|
|
|
for ( service_key, tags ) in service_keys_to_tags.items():
|
|
|
|
if service_key == CC.LOCAL_TAG_SERVICE_KEY:
|
|
|
|
action = HC.CONTENT_UPDATE_ADD
|
|
|
|
else:
|
|
|
|
action = HC.CONTENT_UPDATE_PEND
|
|
|
|
|
|
content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, action, ( tag, hashes ) ) for tag in tags ]
|
|
|
|
service_keys_to_content_updates[ service_key ] = content_updates
|
|
|
|
|
|
return service_keys_to_content_updates
|
|
|
|
def ConvertZoomToPercentage( zoom ):
|
|
|
|
zoom_percent = zoom * 100
|
|
|
|
pretty_zoom = '%.2f' % zoom_percent + '%'
|
|
|
|
if pretty_zoom.endswith( '00%' ):
|
|
|
|
pretty_zoom = '%i' % zoom_percent + '%'
|
|
|
|
|
|
return pretty_zoom
|
|
|
|
def GetAlphaOfColour( colour, alpha ):
|
|
|
|
( r, g, b, a ) = colour.Get()
|
|
|
|
return wx.Colour( r, g, b, alpha )
|
|
|
|
def GetDifferentLighterDarkerColour( colour, intensity = 3 ):
|
|
|
|
( r, g, b, a ) = colour.Get()
|
|
|
|
if ColourIsGreyish( colour ):
|
|
|
|
if ColourIsBright( colour ):
|
|
|
|
colour = wx.Colour( int( g * ( 1 - 0.05 * intensity ) ), b, r )
|
|
|
|
else:
|
|
|
|
colour = wx.Colour( int( g * ( 1 + 0.05 * intensity ) ) / 2, b, r )
|
|
|
|
|
|
else:
|
|
|
|
colour = wx.Colour( g, b, r )
|
|
|
|
|
|
return GetLighterDarkerColour( colour, intensity )
|
|
|
|
def GetLighterDarkerColour( colour, intensity = 3 ):
|
|
|
|
if intensity is None or intensity == 0:
|
|
|
|
return colour
|
|
|
|
|
|
if ColourIsBright( colour ):
|
|
|
|
return wx.lib.colourutils.AdjustColour( colour, -5 * intensity )
|
|
|
|
else:
|
|
|
|
( r, g, b, a ) = colour.Get()
|
|
|
|
( r, g, b ) = [ max( value, 32 ) for value in ( r, g, b ) ]
|
|
|
|
colour = wx.Colour( r, g, b )
|
|
|
|
return wx.lib.colourutils.AdjustColour( colour, 5 * intensity )
|
|
|
|
|
|
def GetMediasTagCount( pool, tag_service_key = CC.COMBINED_TAG_SERVICE_KEY, collapse_siblings = False ):
|
|
|
|
siblings_manager = HG.client_controller.GetManager( 'tag_siblings' )
|
|
|
|
tags_managers = []
|
|
|
|
for media in pool:
|
|
|
|
if media.IsCollection():
|
|
|
|
tags_managers.extend( media.GetSingletonsTagsManagers() )
|
|
|
|
else:
|
|
|
|
tags_managers.append( media.GetTagsManager() )
|
|
|
|
|
|
|
|
current_tags_to_count = collections.Counter()
|
|
deleted_tags_to_count = collections.Counter()
|
|
pending_tags_to_count = collections.Counter()
|
|
petitioned_tags_to_count = collections.Counter()
|
|
|
|
for tags_manager in tags_managers:
|
|
|
|
statuses_to_tags = tags_manager.GetStatusesToTags( tag_service_key )
|
|
|
|
# combined is already collapsed
|
|
if tag_service_key != CC.COMBINED_TAG_SERVICE_KEY and collapse_siblings:
|
|
|
|
statuses_to_tags = siblings_manager.CollapseStatusesToTags( tag_service_key, statuses_to_tags )
|
|
|
|
|
|
current_tags_to_count.update( statuses_to_tags[ HC.CONTENT_STATUS_CURRENT ] )
|
|
deleted_tags_to_count.update( statuses_to_tags[ HC.CONTENT_STATUS_DELETED ] )
|
|
pending_tags_to_count.update( statuses_to_tags[ HC.CONTENT_STATUS_PENDING ] )
|
|
petitioned_tags_to_count.update( statuses_to_tags[ HC.CONTENT_STATUS_PETITIONED ] )
|
|
|
|
|
|
return ( current_tags_to_count, deleted_tags_to_count, pending_tags_to_count, petitioned_tags_to_count )
|
|
|
|
def GetSortTypeChoices():
|
|
|
|
sort_choices = list( CC.SORT_CHOICES )
|
|
|
|
for ( namespaces_text, namespaces_list ) in HC.options[ 'sort_by' ]:
|
|
|
|
sort_choices.append( ( namespaces_text, tuple( namespaces_list ) ) )
|
|
|
|
|
|
service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )
|
|
|
|
for service_key in service_keys:
|
|
|
|
sort_choices.append( ( 'rating', service_key ) )
|
|
|
|
|
|
return sort_choices
|
|
|
|
def MergeCounts( min_a, max_a, min_b, max_b ):
|
|
|
|
# 100-None and 100-None returns 100-200
|
|
# 1-None and 4-5 returns 5-6
|
|
# 1-2, and 5-7 returns 6, 9
|
|
|
|
if min_a == 0:
|
|
|
|
( min_answer, max_answer ) = ( min_b, max_b )
|
|
|
|
elif min_b == 0:
|
|
|
|
( min_answer, max_answer ) = ( min_a, max_a )
|
|
|
|
else:
|
|
|
|
if max_a is None:
|
|
|
|
max_a = min_a
|
|
|
|
|
|
if max_b is None:
|
|
|
|
max_b = min_b
|
|
|
|
|
|
min_answer = max( min_a, min_b )
|
|
max_answer = max_a + max_b
|
|
|
|
|
|
return ( min_answer, max_answer )
|
|
|
|
def MergePredicates( predicates, add_namespaceless = False ):
|
|
|
|
master_predicate_dict = {}
|
|
|
|
for predicate in predicates:
|
|
|
|
# this works because predicate.__hash__ exists
|
|
|
|
if predicate in master_predicate_dict:
|
|
|
|
master_predicate_dict[ predicate ].AddCounts( predicate )
|
|
|
|
else:
|
|
|
|
master_predicate_dict[ predicate ] = predicate
|
|
|
|
|
|
|
|
if add_namespaceless:
|
|
|
|
# we want to include the count for namespaced tags in the namespaceless version when:
|
|
# there exists more than one instance of the subtag with different namespaces, including '', that has nonzero count
|
|
|
|
unnamespaced_predicate_dict = {}
|
|
subtag_nonzero_instance_counter = collections.Counter()
|
|
|
|
for predicate in master_predicate_dict.values():
|
|
|
|
if predicate.HasNonZeroCount():
|
|
|
|
unnamespaced_predicate = predicate.GetUnnamespacedCopy()
|
|
|
|
subtag_nonzero_instance_counter[ unnamespaced_predicate ] += 1
|
|
|
|
if unnamespaced_predicate in unnamespaced_predicate_dict:
|
|
|
|
unnamespaced_predicate_dict[ unnamespaced_predicate ].AddCounts( unnamespaced_predicate )
|
|
|
|
else:
|
|
|
|
unnamespaced_predicate_dict[ unnamespaced_predicate ] = unnamespaced_predicate
|
|
|
|
|
|
|
|
|
|
for ( unnamespaced_predicate, count ) in subtag_nonzero_instance_counter.items():
|
|
|
|
# if there were indeed several instances of this subtag, overwrte the master dict's instance with our new count total
|
|
|
|
if count > 1:
|
|
|
|
master_predicate_dict[ unnamespaced_predicate ] = unnamespaced_predicate_dict[ unnamespaced_predicate ]
|
|
|
|
|
|
|
|
|
|
return master_predicate_dict.values()
|
|
|
|
def OrdIsSensibleASCII( o ):
|
|
|
|
return 32 <= o and o <= 127
|
|
|
|
def OrdIsAlphaLower( o ):
|
|
|
|
return 97 <= o and o <= 122
|
|
|
|
def OrdIsAlphaUpper( o ):
|
|
|
|
return 65 <= o and o <= 90
|
|
|
|
def OrdIsAlpha( o ):
|
|
|
|
return OrdIsAlphaLower( o ) or OrdIsAlphaUpper( o )
|
|
|
|
def OrdIsNumber( o ):
|
|
|
|
return 48 <= o and o <= 57
|
|
|
|
def ReportShutdownException():
|
|
|
|
text = 'A serious error occured while trying to exit the program. Its traceback may be shown next. It should have also been written to client.log. You may need to quit the program from task manager.'
|
|
|
|
HydrusData.DebugPrint( text )
|
|
|
|
HydrusData.DebugPrint( traceback.format_exc() )
|
|
|
|
wx.CallAfter( wx.MessageBox, traceback.format_exc() )
|
|
wx.CallAfter( wx.MessageBox, text )
|
|
|
|
def ShowExceptionClient( e, do_wait = True ):
|
|
|
|
( etype, value, tb ) = sys.exc_info()
|
|
|
|
if etype is None:
|
|
|
|
etype = type( e )
|
|
value = HydrusData.ToUnicode( e )
|
|
|
|
trace = 'No error trace--here is the stack:' + os.linesep + ''.join( traceback.format_stack() )
|
|
|
|
else:
|
|
|
|
trace = ''.join( traceback.format_exception( etype, value, tb ) )
|
|
|
|
|
|
trace = HydrusData.ToUnicode( trace )
|
|
|
|
pretty_value = HydrusData.ToUnicode( value )
|
|
|
|
if os.linesep in pretty_value:
|
|
|
|
( first_line, anything_else ) = HydrusData.ToUnicode( value ).split( os.linesep, 1 )
|
|
|
|
trace = trace + os.linesep + anything_else
|
|
|
|
else:
|
|
|
|
first_line = pretty_value
|
|
|
|
|
|
job_key = ClientThreading.JobKey()
|
|
|
|
if isinstance( e, HydrusExceptions.ShutdownException ):
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
if hasattr( etype, '__name__' ): title = HydrusData.ToUnicode( etype.__name__ )
|
|
else: title = HydrusData.ToUnicode( etype )
|
|
|
|
job_key.SetVariable( 'popup_title', title )
|
|
|
|
job_key.SetVariable( 'popup_text_1', first_line )
|
|
job_key.SetVariable( 'popup_traceback', trace )
|
|
|
|
|
|
text = job_key.ToString()
|
|
|
|
HydrusData.Print( 'Exception:' )
|
|
|
|
HydrusData.DebugPrint( text )
|
|
|
|
HG.client_controller.pub( 'message', job_key )
|
|
|
|
if do_wait:
|
|
|
|
time.sleep( 1 )
|
|
|
|
|
|
def ShowTextClient( text ):
|
|
|
|
job_key = ClientThreading.JobKey()
|
|
|
|
job_key.SetVariable( 'popup_text_1', HydrusData.ToUnicode( text ) )
|
|
|
|
text = job_key.ToString()
|
|
|
|
HydrusData.Print( text )
|
|
|
|
HG.client_controller.pub( 'message', job_key )
|
|
|
|
class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
|
|
|
|
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_APPLICATION_COMMAND
|
|
SERIALISABLE_NAME = 'Application Command'
|
|
SERIALISABLE_VERSION = 1
|
|
|
|
def __init__( self, command_type = None, data = None ):
|
|
|
|
if command_type is None:
|
|
|
|
command_type = CC.APPLICATION_COMMAND_TYPE_SIMPLE
|
|
|
|
|
|
if data is None:
|
|
|
|
data = 'archive_file'
|
|
|
|
|
|
HydrusSerialisable.SerialisableBase.__init__( self )
|
|
|
|
self._command_type = command_type
|
|
self._data = data
|
|
|
|
|
|
def __cmp__( self, other ):
|
|
|
|
return cmp( self.ToString(), other.ToString() )
|
|
|
|
|
|
def __repr__( self ):
|
|
|
|
return self.ToString()
|
|
|
|
|
|
def _GetSerialisableInfo( self ):
|
|
|
|
if self._command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
|
|
|
|
serialisable_data = self._data
|
|
|
|
elif self._command_type == CC.APPLICATION_COMMAND_TYPE_CONTENT:
|
|
|
|
( service_key, content_type, action, value ) = self._data
|
|
|
|
serialisable_data = ( service_key.encode( 'hex' ), content_type, action, value )
|
|
|
|
|
|
return ( self._command_type, serialisable_data )
|
|
|
|
|
|
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
|
|
|
|
( self._command_type, serialisable_data ) = serialisable_info
|
|
|
|
if self._command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
|
|
|
|
self._data = serialisable_data
|
|
|
|
elif self._command_type == CC.APPLICATION_COMMAND_TYPE_CONTENT:
|
|
|
|
( serialisable_service_key, content_type, action, value ) = serialisable_data
|
|
|
|
self._data = ( serialisable_service_key.decode( 'hex' ), content_type, action, value )
|
|
|
|
|
|
|
|
def GetCommandType( self ):
|
|
|
|
return self._command_type
|
|
|
|
|
|
def GetData( self ):
|
|
|
|
return self._data
|
|
|
|
|
|
def ToString( self ):
|
|
|
|
if self._command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
|
|
|
|
return self._data
|
|
|
|
elif self._command_type == CC.APPLICATION_COMMAND_TYPE_CONTENT:
|
|
|
|
( service_key, content_type, action, value ) = self._data
|
|
|
|
components = []
|
|
|
|
components.append( HC.content_update_string_lookup[ action ] )
|
|
components.append( HC.content_type_string_lookup[ content_type ] )
|
|
components.append( '"' + HydrusData.ToUnicode( value ) + '"' )
|
|
components.append( 'for' )
|
|
|
|
services_manager = HG.client_controller.services_manager
|
|
|
|
if services_manager.ServiceExists( service_key ):
|
|
|
|
service = services_manager.GetService( service_key )
|
|
|
|
components.append( service.GetName() )
|
|
|
|
else:
|
|
|
|
components.append( 'unknown service!' )
|
|
|
|
|
|
return ' '.join( components )
|
|
|
|
|
|
|
|
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_APPLICATION_COMMAND ] = ApplicationCommand
|
|
|
|
class Booru( HydrusData.HydrusYAMLBase ):
|
|
|
|
yaml_tag = u'!Booru'
|
|
|
|
def __init__( self, name, search_url, search_separator, advance_by_page_num, thumb_classname, image_id, image_data, tag_classnames_to_namespaces ):
|
|
|
|
self._name = name
|
|
self._search_url = search_url
|
|
self._search_separator = search_separator
|
|
self._advance_by_page_num = advance_by_page_num
|
|
self._thumb_classname = thumb_classname
|
|
self._image_id = image_id
|
|
self._image_data = image_data
|
|
self._tag_classnames_to_namespaces = tag_classnames_to_namespaces
|
|
|
|
|
|
def GetData( self ): return ( self._search_url, self._search_separator, self._advance_by_page_num, self._thumb_classname, self._image_id, self._image_data, self._tag_classnames_to_namespaces )
|
|
|
|
def GetGalleryParsingInfo( self ): return ( self._search_url, self._advance_by_page_num, self._search_separator, self._thumb_classname )
|
|
|
|
def GetName( self ): return self._name
|
|
|
|
def GetNamespaces( self ): return self._tag_classnames_to_namespaces.values()
|
|
|
|
sqlite3.register_adapter( Booru, yaml.safe_dump )
|
|
|
|
class Credentials( HydrusData.HydrusYAMLBase ):
|
|
|
|
yaml_tag = u'!Credentials'
|
|
|
|
def __init__( self, host, port, access_key = None ):
|
|
|
|
HydrusData.HydrusYAMLBase.__init__( self )
|
|
|
|
if host == 'localhost':
|
|
|
|
host = '127.0.0.1'
|
|
|
|
|
|
self._host = host
|
|
self._port = port
|
|
self._access_key = access_key
|
|
|
|
|
|
def __eq__( self, other ): return self.__hash__() == other.__hash__()
|
|
|
|
def __hash__( self ): return ( self._host, self._port, self._access_key ).__hash__()
|
|
|
|
def __ne__( self, other ): return self.__hash__() != other.__hash__()
|
|
|
|
def __repr__( self ): return 'Credentials: ' + HydrusData.ToUnicode( ( self._host, self._port, self._access_key.encode( 'hex' ) ) )
|
|
|
|
def GetAccessKey( self ): return self._access_key
|
|
|
|
def GetAddress( self ): return ( self._host, self._port )
|
|
|
|
def GetConnectionString( self ):
|
|
|
|
connection_string = ''
|
|
|
|
if self.HasAccessKey(): connection_string += self._access_key.encode( 'hex' ) + '@'
|
|
|
|
connection_string += self._host + ':' + str( self._port )
|
|
|
|
return connection_string
|
|
|
|
|
|
def HasAccessKey( self ): return self._access_key is not None and self._access_key is not ''
|
|
|
|
def SetAccessKey( self, access_key ): self._access_key = access_key
|
|
|
|
class Imageboard( HydrusData.HydrusYAMLBase ):
|
|
|
|
yaml_tag = u'!Imageboard'
|
|
|
|
def __init__( self, name, post_url, flood_time, form_fields, restrictions ):
|
|
|
|
self._name = name
|
|
self._post_url = post_url
|
|
self._flood_time = flood_time
|
|
self._form_fields = form_fields
|
|
self._restrictions = restrictions
|
|
|
|
|
|
def IsOKToPost( self, media_result ):
|
|
|
|
# deleted old code due to deprecation
|
|
|
|
return True
|
|
|
|
|
|
def GetBoardInfo( self ): return ( self._post_url, self._flood_time, self._form_fields, self._restrictions )
|
|
|
|
def GetName( self ): return self._name
|
|
|
|
sqlite3.register_adapter( Imageboard, yaml.safe_dump )
|