Version 275

This commit is contained in:
Hydrus Network Developer 2017-09-27 16:52:54 -05:00
parent 227d743f96
commit 781005f1d4
26 changed files with 2304 additions and 586 deletions

View File

@ -8,6 +8,40 @@
<div class="content">
<h3>changelog</h3>
<ul>
<ul>
<li><h3>version 275</h3></li>
<li>if you hold shift down while dropping a page tab, the client will not 'chase' that page to show it (try it out!)</li>
<li>the gui will be more snappy about dealing with drag-and-drop drops (of types file import, page tab rearrange, and url drop), returning the mouse to normal state instantly on drop and dealing with the event in a subsequent action</li>
<li>dropping url text on the client will specifically inform the source program not to delete the dragged text (that the operation was copy, not move), if it is interested in hearing that</li>
<li>page drag-and-drops should transition a little less flickerily</li>
<li>all file import status objects can now track 'source time', typically to represent upload time</li>
<li>file imports now populate 'source time' based on the earliest of creation/modified time! (this will be used later to parse as a tag)</li>
<li>thread watchers now populate 'source time' based on post time!</li>
<li>finished a watcher options object for the new thread checker system</li>
<li>wrote a panel to edit watcher options</li>
<li>converted the thread object to the new watcher system</li>
<li>thread watchers now have two pause buttons--one for the file queue, one for the checker</li>
<li>compressed thread watcher ui layout</li>
<li>converted the thread left-panel ui and options->downloading page to reflect the new watcher system</li>
<li>improved the watcher options to generate better timings for fresh threads</li>
<li>cleaned up some thread watcher check time code</li>
<li>the total/selected mime summary on the status bar is a little prettier and will now report by individual mime sometimes</li>
<li>generating the total/select mime summaries are now faster on pages with >1,000 files (it'll just use 'file')</li>
<li>a 'refresh' action on an import page now triggers a sort event</li>
<li>added 'flip_darkmode' shortcut command to 'main_gui' and 'media_viewer' shortcut sets</li>
<li>added copy_file/path/hash/bmp actions to the 'media' shortcut set, removed the hardcoded ctrl+c for copy_file. I added ctrl+c to the defaults, but existing users will have to re-add it manually if they want it!</li>
<li>simplified some page ui update code</li>
<li>import pages will no longer update their left-panel ui (which uses a bit of cpu) when they are not in view</li>
<li>polished some new string and url matching code the domain engine will be using</li>
<li>wrote a panel to edit string match objects</li>
<li>wrote a new panel to handle simple ordered lists of data in a better way</li>
<li>wrote most of a panel to edit url match objects</li>
<li>misc domain manager work</li>
<li>fixed an issue with the old listctrl where object name de-duplication was sometimes not permitting (1)-type names to be cleaned up</li>
<li>fixed some inelegant time duration->text conversions</li>
<li>improved complete process shutdown reliability when some downloads are waiting on bandwidth on shutdown</li>
<li>did some misc listctrl update work</li>
</ul>
<li><h3>version 274</h3></li>
<ul>
<li>the help menu now has an easy on/off check entry for the darkmode colourset</li>

View File

@ -333,10 +333,10 @@ SHORTCUTS_RESERVED_NAMES = [ 'archive_delete_filter', 'duplicate_filter', 'media
# shortcut commands
SHORTCUTS_MEDIA_ACTIONS = [ 'manage_file_tags', 'manage_file_ratings', 'archive_file', 'inbox_file', 'delete_file', 'remove_file_from_view', 'open_file_in_external_program', 'launch_the_archive_delete_filter' ]
SHORTCUTS_MEDIA_VIEWER_ACTIONS = [ 'move_animation_to_previous_frame', 'move_animation_to_next_frame', 'switch_between_fullscreen_borderless_and_regular_framed_window', 'pan_up', 'pan_down', 'pan_left', 'pan_right', 'zoom_in', 'zoom_out', 'switch_between_100_percent_and_canvas_zoom' ]
SHORTCUTS_MEDIA_ACTIONS = [ 'manage_file_tags', 'manage_file_ratings', 'archive_file', 'inbox_file', 'delete_file', 'remove_file_from_view', 'open_file_in_external_program', 'launch_the_archive_delete_filter', 'copy_bmp', 'copy_file', 'copy_path', 'copy_sha256_hash' ]
SHORTCUTS_MEDIA_VIEWER_ACTIONS = [ 'move_animation_to_previous_frame', 'move_animation_to_next_frame', 'switch_between_fullscreen_borderless_and_regular_framed_window', 'pan_up', 'pan_down', 'pan_left', 'pan_right', 'zoom_in', 'zoom_out', 'switch_between_100_percent_and_canvas_zoom', 'flip_darkmode' ]
SHORTCUTS_MEDIA_VIEWER_BROWSER_ACTIONS = [ 'view_next', 'view_first', 'view_last', 'view_previous' ]
SHORTCUTS_MAIN_GUI_ACTIONS = [ 'refresh', 'new_page', 'synchronised_wait_switch', 'set_media_focus', 'show_hide_splitters', 'set_search_focus', 'unclose_page', 'close_page', 'redo', 'undo' ]
SHORTCUTS_MAIN_GUI_ACTIONS = [ 'refresh', 'new_page', 'synchronised_wait_switch', 'set_media_focus', 'show_hide_splitters', 'set_search_focus', 'unclose_page', 'close_page', 'redo', 'undo', 'flip_darkmode' ]
SHORTCUTS_DUPLICATE_FILTER_ACTIONS = [ 'duplicate_filter_this_is_better', 'duplicate_filter_exactly_the_same', 'duplicate_filter_alternates', 'duplicate_filter_not_dupes', 'duplicate_filter_custom_action', 'duplicate_filter_skip', 'duplicate_filter_back' ]
SHORTCUTS_ARCHIVE_DELETE_FILTER_ACTIONS = [ 'archive_delete_filter_keep', 'archive_delete_filter_delete', 'archive_delete_filter_skip', 'archive_delete_filter_back' ]

View File

@ -1038,6 +1038,12 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
#
self._dictionary[ 'misc' ] = HydrusSerialisable.SerialisableDictionary()
self._dictionary[ 'misc' ][ 'default_thread_watcher_options' ] = WatcherOptions( intended_files_per_check = 8, never_faster_than = 300, never_slower_than = 86400, death_file_velocity = ( 1, 86400 ) )
#
self._dictionary[ 'suggested_tags' ] = HydrusSerialisable.SerialisableDictionary()
self._dictionary[ 'suggested_tags' ][ 'favourites' ] = {}
@ -1332,6 +1338,14 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
def GetDefaultThreadWatcherOptions( self ):
with self._lock:
return self._dictionary[ 'misc' ][ 'default_thread_watcher_options' ]
def GetDefaultSort( self ):
with self._lock:
@ -1585,6 +1599,14 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
def SetDefaultThreadWatcherOptions( self, watcher_options ):
with self._lock:
self._dictionary[ 'misc' ][ 'default_thread_watcher_options' ] = watcher_options
def SetDefaultSort( self, media_sort ):
with self._lock:
@ -2690,3 +2712,139 @@ class TagCensor( HydrusSerialisable.SerialisableBase ):
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_TAG_CENSOR ] = TagCensor
class WatcherOptions( HydrusSerialisable.SerialisableBase ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_WATCHER_OPTIONS
SERIALISABLE_VERSION = 1
def __init__( self, intended_files_per_check = 8, never_faster_than = 300, never_slower_than = 86400, death_file_velocity = ( 1, 86400 ) ):
HydrusSerialisable.SerialisableBase.__init__( self )
self._intended_files_per_check = intended_files_per_check
self._never_faster_than = never_faster_than
self._never_slower_than = never_slower_than
self._death_file_velocity = death_file_velocity
def _GetCurrentFilesVelocity( self, seed_cache, last_check_time ):
( death_files_found, death_time_delta ) = self._death_file_velocity
since = last_check_time - death_time_delta
current_files_found = seed_cache.GetNumNewFilesSince( since )
if len( seed_cache ) == 0:
current_time_delta = death_time_delta
else:
# when a thread is only 30mins old (i.e. first file was posted 30 mins ago), we don't want to calculate based on a longer delete time delta
# we want next check to be like 30mins from now, not 12 hours
# so we'll say "5 files in 30 mins" rather than "5 files in 24 hours"
earliest_file_time = seed_cache.GetEarliestTimestamp()
early_time_delta = max( last_check_time - earliest_file_time, 30 )
current_time_delta = min( early_time_delta, death_time_delta )
return ( current_files_found, current_time_delta )
def _GetSerialisableInfo( self ):
return ( self._intended_files_per_check, self._never_faster_than, self._never_slower_than, self._death_file_velocity )
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
( self._intended_files_per_check, self._never_faster_than, self._never_slower_than, self._death_file_velocity ) = serialisable_info
def GetNextCheckTime( self, seed_cache, last_check_time ):
if len( seed_cache ) == 0:
if last_check_time == 0:
return 0 # haven't checked yet, so should check immediately
else:
return HydrusData.GetNow() + self._never_slower_than
else:
( current_files_found, current_time_delta ) = self._GetCurrentFilesVelocity( seed_cache, last_check_time )
if current_files_found == 0:
check_period = self._never_slower_than
else:
approx_time_per_file = current_time_delta / current_files_found
ideal_check_period = self._intended_files_per_check * approx_time_per_file
check_period = min( max( self._never_faster_than, ideal_check_period ), self._never_slower_than )
return last_check_time + check_period
def GetPrettyCurrentVelocity( self, seed_cache, last_check_time ):
if len( seed_cache ) == 0:
if last_check_time == 0:
pretty_current_velocity = 'no files yet'
else:
pretty_current_velocity = 'no files, unable to determine velocity'
else:
( current_files_found, current_time_delta ) = self._GetCurrentFilesVelocity( seed_cache, last_check_time )
pretty_current_velocity = 'at last check, found ' + HydrusData.ConvertIntToPrettyString( current_files_found ) + ' files in previous ' + HydrusData.ConvertTimeDeltaToPrettyString( current_time_delta )
return pretty_current_velocity
def IsDead( self, seed_cache, last_check_time ):
if len( seed_cache ) == 0 and last_check_time == 0:
return False
else:
( current_files_found, current_time_delta ) = self._GetCurrentFilesVelocity( seed_cache, last_check_time )
( death_files_found, deleted_time_delta ) = self._death_file_velocity
current_file_velocity_float = current_files_found / float( current_time_delta )
death_file_velocity_float = death_files_found / float( deleted_time_delta )
return current_file_velocity_float < death_file_velocity_float
def ToTuple( self ):
return ( self._intended_files_per_check, self._never_faster_than, self._never_slower_than, self._death_file_velocity )
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_WATCHER_OPTIONS ] = WatcherOptions

View File

@ -628,6 +628,8 @@ def GetDefaultShortcuts():
media.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_F12, [] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'launch_the_archive_delete_filter' ) )
media.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'C' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'copy_file' ) )
shortcuts.append( media )
main_gui = ClientData.Shortcuts( 'main_gui' )

View File

@ -278,7 +278,7 @@ def Parse4chanPostScreen( html ):
except: return ( 'error', 'unknown error' )
def ParseImageboardFileURLFromPost( thread_url, post ):
def ParseImageboardFileURLFromPost( thread_url, post, source_timestamp ):
url_filename = str( post[ 'tim' ] )
url_ext = post[ 'ext' ]
@ -295,7 +295,7 @@ def ParseImageboardFileURLFromPost( thread_url, post ):
file_md5_base64 = None
return ( file_url, file_md5_base64, file_original_filename )
return ( file_url, file_md5_base64, file_original_filename, source_timestamp )
def ParseImageboardFileURLsFromJSON( thread_url, raw_json ):
@ -312,7 +312,16 @@ def ParseImageboardFileURLsFromJSON( thread_url, raw_json ):
continue
file_infos.append( ParseImageboardFileURLFromPost( thread_url, post ) )
if 'time' in post:
source_timestamp = post[ 'time' ]
else:
source_timestamp = HydrusData.GetNow()
file_infos.append( ParseImageboardFileURLFromPost( thread_url, post, source_timestamp ) )
if 'extra_files' in post:
@ -323,7 +332,7 @@ def ParseImageboardFileURLsFromJSON( thread_url, raw_json ):
continue
file_infos.append( ParseImageboardFileURLFromPost( thread_url, extra_file ) )
file_infos.append( ParseImageboardFileURLFromPost( thread_url, extra_file, source_timestamp ) )

View File

@ -38,13 +38,17 @@ class FileDropTarget( wx.PyDropTarget ):
paths = self._file_data_object.GetFilenames()
wx.CallAfter( self._filenames_callable, paths )
wx.CallAfter( self._filenames_callable, paths ) # callafter to terminate dnd event now
result = wx.DragNone
elif received_format_type in ( wx.DF_TEXT, wx.DF_UNICODETEXT ) and self._url_callable is not None:
text = self._text_data_object.GetText()
self._url_callable( text )
wx.CallAfter( self._url_callable, text ) # callafter to terminate dnd event now
result = wx.DragCopy
else:
@ -59,14 +63,16 @@ class FileDropTarget( wx.PyDropTarget ):
if format_id == 'application/hydrus-media':
pass
result = wx.DragCancel
if format_id == 'application/hydrus-page-tab' and self._page_callable is not None:
page_key = self._hydrus_page_tab_data_object.GetData()
self._page_callable( page_key )
wx.CallAfter( self._page_callable, page_key ) # callafter to terminate dnd event now
result = wx.DragMove
@ -74,8 +80,4 @@ class FileDropTarget( wx.PyDropTarget ):
return result
def OnDragOver( self, x, y, result ):
return wx.DragCopy
# setting OnDragOver to return copy gives Linux trouble with page tab drops with shift held down

View File

@ -834,22 +834,6 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
def _FlipDarkmode( self ):
current_colourset = self._new_options.GetString( 'current_colourset' )
if current_colourset == 'darkmode':
new_colourset = 'default'
elif current_colourset == 'default':
new_colourset = 'darkmode'
self._new_options.SetString( 'current_colourset', new_colourset )
def _GenerateMenuInfo( self, name ):
menu = wx.Menu()
@ -1419,7 +1403,7 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
currently_darkmode = self._new_options.GetString( 'current_colourset' ) == 'darkmode'
ClientGUIMenus.AppendMenuCheckItem( self, menu, 'darkmode', 'Set the \'darkmode\' colourset on and off.', currently_darkmode, self._FlipDarkmode )
ClientGUIMenus.AppendMenuCheckItem( self, menu, 'darkmode', 'Set the \'darkmode\' colourset on and off.', currently_darkmode, self.FlipDarkmode )
check_manager = ClientGUICommon.CheckboxManagerOptions( 'advanced_mode' )
@ -1954,6 +1938,10 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
self._UnclosePage()
elif action == 'flip_darkmode':
self.FlipDarkmode()
elif action == 'show_hide_splitters':
self._ShowHideSplitters()
@ -2972,6 +2960,22 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
return True
def FlipDarkmode( self ):
current_colourset = self._new_options.GetString( 'current_colourset' )
if current_colourset == 'darkmode':
new_colourset = 'default'
elif current_colourset == 'default':
new_colourset = 'darkmode'
self._new_options.SetString( 'current_colourset', new_colourset )
def FlushOutPredicates( self, predicates ):
good_predicates = []

View File

@ -1055,6 +1055,10 @@ class CanvasFrame( ClientGUITopLevelWindows.FrameThatResizes ):
self.FullscreenSwitch()
elif action == 'flip_darkmode':
HG.client_controller.gui.FlipDarkmode()
else:
command_processed = False
@ -1683,6 +1687,22 @@ class Canvas( wx.Window ):
self._Archive()
elif action == 'copy_bmp':
self._CopyBMPToClipboard()
elif action == 'copy_file':
self._CopyFileToClipboard()
elif action == 'copy_path':
self._CopyPathToClipboard()
elif action == 'copy_sha256_hash':
self._CopyHashToClipboard( 'sha256' )
elif action == 'delete_file':
self._Delete()
@ -3591,7 +3611,6 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
if modifier == wx.ACCEL_NORMAL and key in CC.DELETE_KEYS: self._Delete()
elif modifier == wx.ACCEL_SHIFT and key in CC.DELETE_KEYS: self._Undelete()
elif modifier == wx.ACCEL_CTRL and key == ord( 'C' ): self._CopyFileToClipboard()
else:
CanvasWithHovers.EventCharHook( self, event )
@ -4229,8 +4248,7 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if modifier == wx.ACCEL_CTRL and key == ord( 'C' ): self._CopyFileToClipboard()
elif modifier == wx.ACCEL_NORMAL and key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER, wx.WXK_ESCAPE ): self._Close()
if modifier == wx.ACCEL_NORMAL and key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER, wx.WXK_ESCAPE ): self._Close()
else:
CanvasMediaList.EventCharHook( self, event )
@ -4579,7 +4597,6 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
elif modifier == wx.ACCEL_SHIFT and key in CC.DELETE_KEYS: self._Undelete()
elif modifier == wx.ACCEL_NORMAL and key in ( wx.WXK_SPACE, wx.WXK_NUMPAD_SPACE ): wx.CallAfter( self._PausePlaySlideshow )
elif modifier == wx.ACCEL_NORMAL and key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER, wx.WXK_ESCAPE ): self._Close()
elif modifier == wx.ACCEL_CTRL and key == ord( 'C' ): self._CopyFileToClipboard()
else:
CanvasMediaListNavigable.EventCharHook( self, event )

View File

@ -623,6 +623,61 @@ class CheckboxCollect( wx.combo.ComboCtrl ):
class CheckboxManager( object ):
def GetCurrentValue( self ):
raise NotImplementedError()
def Invert( self ):
raise NotImplementedError()
class CheckboxManagerCalls( CheckboxManager ):
def __init__( self, invert_call, value_call ):
CheckboxManager.__init__( self )
self._invert_call = invert_call
self._value_call = value_call
def GetCurrentValue( self ):
return self._value_call()
def Invert( self ):
self._invert_call()
class CheckboxManagerOptions( CheckboxManager ):
def __init__( self, boolean_name ):
CheckboxManager.__init__( self )
self._boolean_name = boolean_name
def GetCurrentValue( self ):
new_options = HG.client_controller.GetNewOptions()
return new_options.GetBoolean( self._boolean_name )
def Invert( self ):
new_options = HG.client_controller.GetNewOptions()
new_options.InvertBoolean( self._boolean_name )
class ChoiceSort( wx.Panel ):
def __init__( self, parent, management_controller = None ):
@ -1360,61 +1415,6 @@ class ListBook( wx.Panel ):
class CheckboxManager( object ):
def GetCurrentValue( self ):
raise NotImplementedError()
def Invert( self ):
raise NotImplementedError()
class CheckboxManagerCalls( CheckboxManager ):
def __init__( self, invert_call, value_call ):
CheckboxManager.__init__( self )
self._invert_call = invert_call
self._value_call = value_call
def GetCurrentValue( self ):
return self._value_call()
def Invert( self ):
self._invert_call()
class CheckboxManagerOptions( CheckboxManager ):
def __init__( self, boolean_name ):
CheckboxManager.__init__( self )
self._boolean_name = boolean_name
def GetCurrentValue( self ):
new_options = HG.client_controller.GetNewOptions()
return new_options.GetBoolean( self._boolean_name )
def Invert( self ):
new_options = HG.client_controller.GetNewOptions()
new_options.InvertBoolean( self._boolean_name )
class MenuBitmapButton( BetterBitmapButton ):
def __init__( self, parent, bitmap, menu_items ):
@ -1554,8 +1554,13 @@ class NoneableSpinCtrl( wx.Panel ):
def Bind( self, event_type, callback ):
self._checkbox.Bind( wx.EVT_CHECKBOX, callback )
self._one.Bind( wx.EVT_SPINCTRL, callback )
if self._num_dimensions == 2: self._two.Bind( wx.EVT_SPINCTRL, callback )
if self._num_dimensions == 2:
self._two.Bind( wx.EVT_SPINCTRL, callback )
def EventCheckBox( self, event ):
@ -2539,6 +2544,50 @@ class StaticBoxSorterForListBoxTags( StaticBox ):
self._tags_box.SetTagsByMedia( media, force_reload = force_reload )
class RadioBox( StaticBox ):
def __init__( self, parent, title, choice_pairs, initial_index = None ):
StaticBox.__init__( self, parent, title )
self._indices_to_radio_buttons = {}
self._radio_buttons_to_data = {}
first_button = True
for ( index, ( text, data ) ) in enumerate( choice_pairs ):
if first_button:
style = wx.RB_GROUP
first_button = False
else: style = 0
radio_button = wx.RadioButton( self, label = text, style = style )
self.AddF( radio_button, CC.FLAGS_EXPAND_PERPENDICULAR )
self._indices_to_radio_buttons[ index ] = radio_button
self._radio_buttons_to_data[ radio_button ] = data
if initial_index is not None and initial_index in self._indices_to_radio_buttons: self._indices_to_radio_buttons[ initial_index ].SetValue( True )
def GetSelectedClientData( self ):
for radio_button in self._radio_buttons_to_data.keys():
if radio_button.GetValue() == True: return self._radio_buttons_to_data[ radio_button ]
def SetSelection( self, index ): self._indices_to_radio_buttons[ index ].SetValue( True )
def SetString( self, index, text ): self._indices_to_radio_buttons[ index ].SetLabelText( text )
class TextAndGauge( wx.Panel ):
def __init__( self, parent ):
@ -2727,7 +2776,7 @@ class TimeDeltaButton( wx.Button ):
class TimeDeltaCtrl( wx.Panel ):
def __init__( self, parent, min = 1, days = False, hours = False, minutes = False, seconds = False, monthly_allowed = False ):
def __init__( self, parent, min = 1, days = False, hours = False, minutes = False, seconds = False, monthly_allowed = False, monthly_label = 'monthly' ):
wx.Panel.__init__( self, parent )
@ -2782,7 +2831,7 @@ class TimeDeltaCtrl( wx.Panel ):
self._monthly.Bind( wx.EVT_CHECKBOX, self.EventChange )
hbox.AddF( self._monthly, CC.FLAGS_VCENTER )
hbox.AddF( BetterStaticText( self, 'monthly' ), CC.FLAGS_VCENTER )
hbox.AddF( BetterStaticText( self, monthly_label ), CC.FLAGS_VCENTER )
self.SetSizer( hbox )
@ -2937,47 +2986,60 @@ class TimeDeltaCtrl( wx.Panel ):
self._UpdateEnables()
class RadioBox( StaticBox ):
class VelocityCtrl( wx.Panel ):
def __init__( self, parent, title, choice_pairs, initial_index = None ):
def __init__( self, parent, min_time_delta = 60, days = False, hours = False, minutes = False, seconds = False, per_phrase = 'per', unit = None ):
StaticBox.__init__( self, parent, title )
wx.Panel.__init__( self, parent )
self._indices_to_radio_buttons = {}
self._radio_buttons_to_data = {}
self._num = wx.SpinCtrl( self, min = 0, max = 1000, size = ( 60, -1 ) )
first_button = True
self._times = TimeDeltaCtrl( self, min = min_time_delta, days = days, hours = hours, minutes = minutes, seconds = seconds )
for ( index, ( text, data ) ) in enumerate( choice_pairs ):
#
hbox = wx.BoxSizer( wx.HORIZONTAL )
hbox.AddF( self._num, CC.FLAGS_VCENTER )
mid_text = per_phrase
if unit is not None:
if first_button:
style = wx.RB_GROUP
first_button = False
else: style = 0
radio_button = wx.RadioButton( self, label = text, style = style )
self.AddF( radio_button, CC.FLAGS_EXPAND_PERPENDICULAR )
self._indices_to_radio_buttons[ index ] = radio_button
self._radio_buttons_to_data[ radio_button ] = data
mid_text = unit + ' ' + mid_text
if initial_index is not None and initial_index in self._indices_to_radio_buttons: self._indices_to_radio_buttons[ initial_index ].SetValue( True )
hbox.AddF( BetterStaticText( self, mid_text ), CC.FLAGS_VCENTER )
hbox.AddF( self._times, CC.FLAGS_VCENTER )
self.SetSizer( hbox )
def GetSelectedClientData( self ):
def GetValue( self ):
for radio_button in self._radio_buttons_to_data.keys():
num = self._num.GetValue()
time_delta = self._times.GetValue()
return ( num, time_delta )
def SetToolTipString( self, text ):
wx.Panel.SetToolTipString( self, text )
for c in self.GetChildren():
if radio_button.GetValue() == True: return self._radio_buttons_to_data[ radio_button ]
c.SetToolTipString( text )
def SetSelection( self, index ): self._indices_to_radio_buttons[ index ].SetValue( True )
def SetString( self, index, text ): self._indices_to_radio_buttons[ index ].SetLabelText( text )
def SetValue( self, velocity ):
( num, time_delta ) = velocity
self._num.SetValue( num )
self._times.SetValue( time_delta )

View File

@ -14,6 +14,225 @@ import HydrusTags
import os
import wx
class QueueListBox( wx.Panel ):
def __init__( self, parent, data_to_pretty_callable, add_callable, edit_callable ):
self._data_to_pretty_callable = data_to_pretty_callable
self._add_callable = add_callable
self._edit_callable = edit_callable
wx.Panel.__init__( self, parent )
self._listbox = wx.ListBox( self, style = wx.LB_MULTIPLE )
self._up_button = ClientGUICommon.BetterButton( self, u'\u2191', self._Up )
self._delete_button = ClientGUICommon.BetterButton( self, 'X', self._Delete )
self._down_button = ClientGUICommon.BetterButton( self, u'\u2193', self._Down )
self._add_button = ClientGUICommon.BetterButton( self, 'add', self._Add )
self._edit_button = ClientGUICommon.BetterButton( self, 'edit', self._Edit )
#
vbox = wx.BoxSizer( wx.VERTICAL )
buttons_vbox = wx.BoxSizer( wx.VERTICAL )
buttons_vbox.AddF( self._up_button, CC.FLAGS_VCENTER )
buttons_vbox.AddF( self._delete_button, CC.FLAGS_VCENTER )
buttons_vbox.AddF( self._down_button, CC.FLAGS_VCENTER )
hbox = wx.BoxSizer( wx.HORIZONTAL )
hbox.AddF( self._listbox, CC.FLAGS_EXPAND_BOTH_WAYS )
hbox.AddF( buttons_vbox, CC.FLAGS_VCENTER )
buttons_hbox = wx.BoxSizer( wx.HORIZONTAL )
buttons_hbox.AddF( self._add_button, CC.FLAGS_EXPAND_BOTH_WAYS )
buttons_hbox.AddF( self._edit_button, CC.FLAGS_EXPAND_BOTH_WAYS )
vbox.AddF( hbox, CC.FLAGS_EXPAND_BOTH_WAYS )
vbox.AddF( buttons_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
self.SetSizer( vbox )
#
self._listbox.Bind( wx.EVT_LISTBOX, self.EventSelection )
def _Add( self ):
( result, data ) = self._add_callable()
if result:
self._AddData( data )
def _AddData( self, data ):
pretty_data = self._data_to_pretty_callable( data )
self._listbox.Append( pretty_data, data )
def _Delete( self ):
indices = self._listbox.GetSelections()
indices.sort( reverse = True )
if len( indices ) == 0:
return
import ClientGUIDialogs
with ClientGUIDialogs.DialogYesNo( self, 'Remove all selected?' ) as dlg_yn:
if dlg_yn.ShowModal() == wx.ID_YES:
for i in indices:
self._listbox.Delete( i )
def _Down( self ):
indices = self._listbox.GetSelections()
indices.sort( reverse = True )
for i in indices:
if i > 0:
if not self._listbox.IsSelected( i + 1 ): # is the one below not selected?
self._SwapRows( i, i + 1 )
def _Edit( self ):
for i in range( self._listbox.GetCount() ):
if not self._listbox.IsSelected( i ):
continue
data = self._listbox.GetClientData( i )
( result, new_data ) = self._edit_callable( data )
if result:
self._listbox.Delete( i )
pretty_new_data = self._data_to_pretty_callable( new_data )
self._listbox.Set( pretty_new_data, i, new_data )
else:
break
def _SwapRows( self, index_a, index_b ):
data_a = self._listbox.GetClientData( index_a )
data_b = self._listbox.GetClientData( index_b )
pretty_data_a = self._data_to_pretty_callable( data_a )
pretty_data_b = self._data_to_pretty_callable( data_b )
self._listbox.Delete( index_a )
self._listbox.Insert( pretty_data_b, index_a, data_b )
self._listbox.Delete( index_b )
self._listbox.Insert( pretty_data_a, index_b, data_a )
def _Up( self ):
indices = self._listbox.GetSelections()
for i in indices:
if i > 0:
if not self._listbox.IsSelected( i - 1 ): # is the one above not selected?
self._SwapRows( i, i - 1 )
def AddDatas( self, datas ):
for data in datas:
self._AddData( data )
def Bind( self, event, handler ):
self._listbox.Bind( event, handler )
def EventSelection( self, event ):
if self._listbox.GetSelection() == wx.NOT_FOUND:
self._up_button.Disable()
self._delete_button.Disable()
self._down_button.Disable()
self._edit_button.Disable()
else:
self._up_button.Enable()
self._delete_button.Enable()
self._down_button.Enable()
self._edit_button.Enable()
event.Skip()
def GetData( self, only_selected = False ):
datas = []
for i in range( self._listbox.GetCount() ):
data = self._listbox.GetClientData( i )
datas.append( data )
return datas
class ListBox( wx.ScrolledWindow ):
TEXT_X_PADDING = 3
@ -914,7 +1133,7 @@ class ListBoxTags( ListBox ):
ClientGUIMenus.AppendMenuItem( self, menu, 'add parents to ' + text, 'Add a parent to this tag.', self._ProcessMenuTagEvent, 'parent' )
ClientGUIMenus.AppendMenuItem( self, menu, 'add parents to ' + text, 'Add a sibling to this tag.', self._ProcessMenuTagEvent, 'sibling' )
ClientGUIMenus.AppendMenuItem( self, menu, 'add siblings to ' + text, 'Add a sibling to this tag.', self._ProcessMenuTagEvent, 'sibling' )

View File

@ -376,7 +376,7 @@ class SaneListCtrlForSingleObject( SaneListCtrl ):
name = obj.GetName()
current_names = { obj.GetName() for obj in self.GetObjects() }
current_names = { o.GetName() for o in self.GetObjects() if o is not obj }
if name in current_names:
@ -492,7 +492,7 @@ class SaneListCtrlPanel( wx.Panel ):
self._listctrl.Bind( wx.EVT_LIST_DELETE_ITEM, self.EventContentChanged )
self._listctrl.Bind( wx.EVT_LIST_DELETE_ALL_ITEMS, self.EventContentChanged )
class BetterListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin ):
def __init__( self, parent, name, height_num_chars, sizing_column_initial_width_num_chars, columns, data_to_tuples_func, delete_key_callback = None, activation_callback = None ):
@ -881,3 +881,92 @@ class BetterListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin ):
class BetterListCtrlPanel( wx.Panel ):
def __init__( self, parent ):
wx.Panel.__init__( self, parent )
self._vbox = wx.BoxSizer( wx.VERTICAL )
self._buttonbox = wx.BoxSizer( wx.HORIZONTAL )
self._listctrl = None
self._button_infos = []
def _HasSelected( self ):
return self._listctrl.HasSelected()
def _UpdateButtons( self ):
for ( button, enabled_check_func ) in self._button_infos:
if enabled_check_func():
button.Enable()
else:
button.Disable()
def AddButton( self, label, clicked_func, enabled_only_on_selection = False, enabled_check_func = None ):
button = ClientGUICommon.BetterButton( self, label, clicked_func )
self._buttonbox.AddF( button, CC.FLAGS_VCENTER )
if enabled_only_on_selection:
enabled_check_func = self._HasSelected
if enabled_check_func is not None:
self._button_infos.append( ( button, enabled_check_func ) )
def AddWindow( self, window ):
self._buttonbox.AddF( window, CC.FLAGS_VCENTER )
def EventContentChanged( self, event ):
self._UpdateButtons()
event.Skip()
def EventSelectionChanged( self, event ):
self._UpdateButtons()
event.Skip()
def SetListCtrl( self, listctrl ):
self._listctrl = listctrl
self._vbox.AddF( self._listctrl, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._vbox.AddF( self._buttonbox, CC.FLAGS_BUTTON_SIZER )
self.SetSizer( self._vbox )
self._listctrl.Bind( wx.EVT_LIST_ITEM_SELECTED, self.EventSelectionChanged )
self._listctrl.Bind( wx.EVT_LIST_ITEM_DESELECTED, self.EventSelectionChanged )
self._listctrl.Bind( wx.EVT_LIST_INSERT_ITEM, self.EventContentChanged )
self._listctrl.Bind( wx.EVT_LIST_DELETE_ITEM, self.EventContentChanged )
self._listctrl.Bind( wx.EVT_LIST_DELETE_ALL_ITEMS, self.EventContentChanged )

View File

@ -766,6 +766,16 @@ class ManagementPanel( wx.lib.scrolledpanel.ScrolledPanel ):
pass
def PageHidden( self ):
pass
def PageShown( self ):
pass
def SetSearchFocus( self, page_key ):
pass
@ -1330,12 +1340,38 @@ class ManagementPanelImporter( ManagementPanel ):
self._import_update_timer.Start( 250, wx.TIMER_CONTINUOUS )
self._controller.sub( self, 'RefreshSort', 'refresh_query' )
def _UpdateStatus( self ):
raise NotImplementedError()
def PageHidden( self ):
ManagementPanel.PageHidden( self )
self._import_update_timer.Stop()
def PageShown( self ):
ManagementPanel.PageShown( self )
self._UpdateStatus()
self._import_update_timer.Start()
def RefreshSort( self, page_key ):
if page_key == self._page_key:
self._sort_by.BroadcastSort()
def TIMEREventImportUpdate( self, event ):
if self._controller.gui.IsCurrentPage( self._page_key ):
@ -2213,12 +2249,12 @@ class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ):
self._options_panel = wx.Panel( self._thread_watcher_panel )
self._pause_button = wx.BitmapButton( self._options_panel, bitmap = CC.GlobalBMPs.pause )
self._pause_button.Bind( wx.EVT_BUTTON, self.EventPause )
#
imports_panel = ClientGUICommon.StaticBox( self._options_panel, 'imports' )
imports_panel = ClientGUICommon.StaticBox( self._options_panel, 'file imports' )
self._files_pause_button = wx.BitmapButton( imports_panel, bitmap = CC.GlobalBMPs.pause )
self._files_pause_button.Bind( wx.EVT_BUTTON, self.EventPauseFiles )
self._current_action = ClientGUICommon.BetterStaticText( imports_panel )
self._seed_cache_control = ClientGUISeedCache.SeedCacheStatusControl( imports_panel, self._controller )
@ -2228,52 +2264,58 @@ class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ):
checker_panel = ClientGUICommon.StaticBox( self._options_panel, 'checker' )
self._file_velocity_status = ClientGUICommon.BetterStaticText( checker_panel )
self._thread_pause_button = wx.BitmapButton( checker_panel, bitmap = CC.GlobalBMPs.pause )
self._thread_pause_button.Bind( wx.EVT_BUTTON, self.EventPauseThread )
self._watcher_status = ClientGUICommon.BetterStaticText( checker_panel )
self._thread_download_control = ClientGUIControls.NetworkJobControl( checker_panel )
( times_to_check, check_period ) = HC.options[ 'thread_checker_timings' ]
self._thread_times_to_check = wx.SpinCtrl( checker_panel, size = ( 60, -1 ), min = 0, max = 65536 )
self._thread_times_to_check.SetValue( times_to_check )
self._thread_times_to_check.Bind( wx.EVT_SPINCTRL, self.EventTimesToCheck )
self._thread_check_period = ClientGUICommon.TimeDeltaButton( checker_panel, min = 30, days = True, hours = True, minutes = True, seconds = True )
self._thread_check_period.SetValue( check_period )
self._thread_check_period.Bind( ClientGUICommon.EVT_TIME_DELTA, self.EventCheckPeriod )
self._thread_check_now_button = wx.Button( checker_panel, label = 'check now' )
self._thread_check_now_button.Bind( wx.EVT_BUTTON, self.EventCheckNow )
self._watcher_options_button = ClientGUICommon.BetterButton( checker_panel, 'edit check timings', self._EditWatcherOptions )
self._thread_download_control = ClientGUIControls.NetworkJobControl( checker_panel )
#
self._thread_watcher_import = self._management_controller.GetVariable( 'thread_watcher_import' )
( thread_url, file_import_options, tag_import_options, times_to_check, check_period ) = self._thread_watcher_import.GetOptions()
( thread_url, file_import_options, tag_import_options ) = self._thread_watcher_import.GetOptions()
self._file_import_options = ClientGUIImport.FileImportOptionsButton( self._thread_watcher_panel, file_import_options, self._thread_watcher_import.SetFileImportOptions )
self._tag_import_options = ClientGUICollapsible.CollapsibleOptionsTags( self._thread_watcher_panel, namespaces = [ 'filename' ] )
#
imports_panel.AddF( self._current_action, CC.FLAGS_EXPAND_PERPENDICULAR )
hbox = wx.BoxSizer( wx.HORIZONTAL )
hbox.AddF( self._current_action, CC.FLAGS_VCENTER_EXPAND_DEPTH_ONLY )
hbox.AddF( self._files_pause_button, CC.FLAGS_LONE_BUTTON )
imports_panel.AddF( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
imports_panel.AddF( self._seed_cache_control, CC.FLAGS_EXPAND_PERPENDICULAR )
imports_panel.AddF( self._file_download_control, CC.FLAGS_EXPAND_PERPENDICULAR )
hbox_1 = wx.WrapSizer( wx.HORIZONTAL )
#
hbox_1.AddF( ClientGUICommon.BetterStaticText( checker_panel, label = 'checking ' ), CC.FLAGS_VCENTER )
hbox_1.AddF( self._thread_times_to_check, CC.FLAGS_VCENTER )
hbox_1.AddF( ClientGUICommon.BetterStaticText( checker_panel, label = ' more times, every ' ), CC.FLAGS_VCENTER )
hbox_1.AddF( self._thread_check_period, CC.FLAGS_VCENTER )
hbox_1.AddF( self._thread_check_now_button, CC.FLAGS_VCENTER )
gridbox = wx.FlexGridSizer( 0, 2 )
checker_panel.AddF( self._watcher_status, CC.FLAGS_EXPAND_PERPENDICULAR )
gridbox.AddGrowableCol( 0, 1 )
gridbox.AddF( self._file_velocity_status, CC.FLAGS_VCENTER_EXPAND_DEPTH_ONLY )
gridbox.AddF( self._thread_pause_button, CC.FLAGS_LONE_BUTTON )
gridbox.AddF( self._watcher_status, CC.FLAGS_VCENTER_EXPAND_DEPTH_ONLY )
gridbox.AddF( self._thread_check_now_button, CC.FLAGS_VCENTER )
checker_panel.AddF( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
checker_panel.AddF( self._watcher_options_button, CC.FLAGS_EXPAND_PERPENDICULAR )
checker_panel.AddF( self._thread_download_control, CC.FLAGS_EXPAND_PERPENDICULAR )
checker_panel.AddF( hbox_1, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
vbox = wx.BoxSizer( wx.VERTICAL )
vbox.AddF( self._pause_button, CC.FLAGS_LONE_BUTTON )
vbox.AddF( imports_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( checker_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
@ -2302,8 +2344,6 @@ class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ):
self.Bind( wx.EVT_MENU, self.EventMenu )
self._controller.sub( self, 'DecrementTimesToCheck', 'decrement_times_to_check' )
seed_cache = self._thread_watcher_import.GetSeedCache()
self._seed_cache_control.SetSeedCache( seed_cache )
@ -2315,12 +2355,30 @@ class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ):
self._tag_import_options.SetOptions( tag_import_options )
self._thread_times_to_check.SetValue( times_to_check )
self._thread_check_period.SetValue( check_period )
self._UpdateStatus()
def _EditWatcherOptions( self ):
watcher_options = self._thread_watcher_import.GetWatcherOptions()
with ClientGUITopLevelWindows.DialogEdit( self._watcher_options_button, 'edit check timings' ) as dlg:
panel = ClientGUIScrolledPanelsEdit.EditWatcherOptions( dlg, watcher_options )
dlg.SetPanel( panel )
if dlg.ShowModal() == wx.ID_OK:
new_watcher_options = panel.GetValue()
self._thread_watcher_import.SetWatcherOptions( new_watcher_options )
self._UpdateStatus()
def _UpdateStatus( self ):
if self._thread_watcher_import.HasThread():
@ -2344,37 +2402,63 @@ class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ):
( current_action, watcher_status, check_now, paused ) = self._thread_watcher_import.GetStatus()
( current_action, files_paused, file_velocity_status, next_check_time, watcher_status, check_now, thread_paused ) = self._thread_watcher_import.GetStatus()
self._current_action.SetLabelText( current_action )
if paused:
if files_paused:
if self._thread_times_to_check.GetValue() > 0 or check_now:
if current_action == '':
watcher_status = 'paused'
current_action = 'paused'
else:
current_action = 'pausing, ' + current_action
if self._pause_button.GetBitmap() != CC.GlobalBMPs.play:
if self._files_pause_button.GetBitmap() != CC.GlobalBMPs.play:
self._pause_button.SetBitmap( CC.GlobalBMPs.play )
self._files_pause_button.SetBitmap( CC.GlobalBMPs.play )
else:
if self._pause_button.GetBitmap() != CC.GlobalBMPs.pause:
if self._files_pause_button.GetBitmap() != CC.GlobalBMPs.pause:
self._pause_button.SetBitmap( CC.GlobalBMPs.pause )
self._files_pause_button.SetBitmap( CC.GlobalBMPs.pause )
self._current_action.SetLabelText( current_action )
self._file_velocity_status.SetLabelText( file_velocity_status )
if thread_paused:
if watcher_status == '':
watcher_status = 'paused'
if self._thread_pause_button.GetBitmap() != CC.GlobalBMPs.play:
self._thread_pause_button.SetBitmap( CC.GlobalBMPs.play )
else:
if watcher_status == '':
watcher_status = 'next check ' + HydrusData.ConvertTimestampToPrettyPending( next_check_time )
if self._thread_pause_button.GetBitmap() != CC.GlobalBMPs.pause:
self._thread_pause_button.SetBitmap( CC.GlobalBMPs.pause )
self._watcher_status.SetLabelText( watcher_status )
if current_action == '' and paused:
current_action = 'paused'
if check_now:
self._thread_check_now_button.Disable()
@ -2385,18 +2469,6 @@ class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ):
def DecrementTimesToCheck( self, page_key ):
if page_key == self._page_key:
current_value = self._thread_times_to_check.GetValue()
new_value = max( 0, current_value - 1 )
self._thread_times_to_check.SetValue( new_value )
def EventCheckNow( self, event ):
self._thread_watcher_import.CheckNow()
@ -2404,11 +2476,18 @@ class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ):
self._UpdateStatus()
def EventCheckPeriod( self, event ):
def EventPauseFiles( self, event ):
check_period = self._thread_check_period.GetValue()
self._thread_watcher_import.PausePlayFiles()
self._thread_watcher_import.SetCheckPeriod( check_period )
self._UpdateStatus()
def EventPauseThread( self, event ):
self._thread_watcher_import.PausePlayThread()
self._UpdateStatus()
def EventKeyDown( self, event ):
@ -2468,20 +2547,6 @@ class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ):
def EventPause( self, event ):
self._thread_watcher_import.PausePlay()
self._UpdateStatus()
def EventTimesToCheck( self, event ):
times_to_check = self._thread_times_to_check.GetValue()
self._thread_watcher_import.SetTimesToCheck( times_to_check )
def SetSearchFocus( self, page_key ):
if page_key == self._page_key and self._thread_input.IsEditable():

View File

@ -118,8 +118,6 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
HG.client_controller.sub( self, 'AddMediaResults', 'add_media_results' )
HG.client_controller.sub( self, 'SetFocussedMedia', 'set_focus' )
HG.client_controller.sub( self, 'PageHidden', 'page_hidden' )
HG.client_controller.sub( self, 'PageShown', 'page_shown' )
HG.client_controller.sub( self, 'Collect', 'collect_media' )
HG.client_controller.sub( self, 'Sort', 'sort_media' )
HG.client_controller.sub( self, 'FileDumped', 'file_dumped' )
@ -547,34 +545,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
num_selected = self._GetNumSelected()
( sorted_mime_classes, selected_mime_classes ) = self._GetSortedSelectedMimeClasses()
if sorted_mime_classes == set( [ 'image' ] ):
num_files_descriptor = 'image'
elif sorted_mime_classes == set( [ 'video' ] ):
num_files_descriptor = 'video'
else:
num_files_descriptor = 'file'
if selected_mime_classes == set( [ 'image' ] ):
selected_files_descriptor = 'image'
elif selected_mime_classes == set( [ 'video' ] ):
selected_files_descriptor = 'video'
else:
selected_files_descriptor = 'file'
( num_files_descriptor, selected_files_descriptor ) = self._GetSortedSelectedMimeDescriptors()
if num_files == 1:
@ -585,28 +556,21 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
num_files_string = HydrusData.ConvertIntToPrettyString( num_files ) + ' ' + num_files_descriptor + 's'
if selected_files_descriptor == num_files_descriptor:
selected_files_string = HydrusData.ConvertIntToPrettyString( num_selected )
else:
if num_selected == 1:
selected_files_string = '1 ' + selected_files_descriptor
else:
selected_files_string = HydrusData.ConvertIntToPrettyString( num_selected ) + ' ' + selected_files_descriptor + 's'
s = num_files_string # 23 files
if num_selected > 0:
s += ' - '
if num_selected == 1 or selected_files_descriptor == num_files_descriptor:
selected_files_string = HydrusData.ConvertIntToPrettyString( num_selected )
else:
selected_files_string = HydrusData.ConvertIntToPrettyString( num_selected ) + ' ' + selected_files_descriptor + 's'
if num_selected == 1: # 23 files - 1 video selected, file_info
( selected_media, ) = self._selected_media
@ -617,9 +581,18 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
num_inbox = sum( ( media.GetNumInbox() for media in self._selected_media ) )
if num_inbox == num_selected: inbox_phrase = 'all in inbox, '
elif num_inbox == 0: inbox_phrase = 'all archived, '
else: inbox_phrase = HydrusData.ConvertIntToPrettyString( num_inbox ) + ' in inbox and ' + HydrusData.ConvertIntToPrettyString( num_selected - num_inbox ) + ' archived, '
if num_inbox == num_selected:
inbox_phrase = 'all in inbox, '
elif num_inbox == 0:
inbox_phrase = 'all archived, '
else:
inbox_phrase = HydrusData.ConvertIntToPrettyString( num_inbox ) + ' in inbox and ' + HydrusData.ConvertIntToPrettyString( num_selected - num_inbox ) + ' archived, '
pretty_total_size = self._GetPrettyTotalSelectedSize()
@ -706,57 +679,63 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
def _GetSortedSelectedMimeClasses( self ):
def _GetSortedSelectedMimeDescriptors( self ):
sorted_mimes = set()
for media in self._sorted_media:
def GetDescriptor( classes ):
mime = media.GetMime()
if len( classes ) == 0:
return 'file'
if mime in HC.IMAGES:
if len( classes ) == 1:
sorted_mimes.add( 'image' )
( mime, ) = classes
elif mime in HC.VIDEO:
return HC.mime_string_lookup[ mime ]
sorted_mimes.add( 'video' )
if len( classes.difference( HC.IMAGES ) ) == 0:
elif mime in HC.AUDIO:
return 'image'
sorted_mimes.add( 'audio' )
elif len( classes.difference( HC.VIDEO ) ) == 0:
return 'video'
elif len( classes.difference( HC.AUDIO ) ) == 0:
return 'audio file'
else:
sorted_mimes.add( 'misc' )
return 'file'
selected_mimes = set()
for media in self._selected_media:
if len( self._sorted_media ) > 1000:
mime = media.GetMime()
sorted_mime_descriptor = 'file'
if mime in HC.IMAGES:
selected_mimes.add( 'image' )
elif mime in HC.VIDEO:
selected_mimes.add( 'video' )
elif mime in HC.AUDIO:
selected_mimes.add( 'audio' )
else:
selected_mimes.add( 'misc' )
else:
sorted_mimes = { media.GetMime() for media in self._sorted_media }
sorted_mime_descriptor = GetDescriptor( sorted_mimes )
return ( sorted_mimes, selected_mimes )
if len( self._selected_media ) > 1000:
selected_mime_descriptor = 'file'
else:
selected_mimes = { media.GetMime() for media in self._selected_media }
selected_mime_descriptor = GetDescriptor( selected_mimes )
return ( sorted_mime_descriptor, selected_mime_descriptor )
def _HitMedia( self, media, ctrl, shift ):
@ -1031,7 +1010,23 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
action = data
if action == 'manage_file_ratings':
if action == 'copy_bmp':
self._CopyBMPToClipboard()
elif action == 'copy_file':
self._CopyFilesToClipboard()
elif action == 'copy_path':
self._CopyPathsToClipboard()
elif action == 'copy_sha256_hash':
self._CopyHashesToClipboard( 'sha256' )
elif action == 'manage_file_ratings':
self._ManageRatings()
@ -1586,22 +1581,16 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
def PageHidden( self, page_key ):
def PageHidden( self ):
if page_key == self._page_key:
HG.client_controller.pub( 'preview_changed', self._page_key, None )
HG.client_controller.pub( 'preview_changed', self._page_key, None )
def PageShown( self, page_key ):
def PageShown( self ):
if page_key == self._page_key:
HG.client_controller.pub( 'preview_changed', self._page_key, self._focussed_media )
self._PublishSelectionChange()
HG.client_controller.pub( 'preview_changed', self._page_key, self._focussed_media )
self._PublishSelectionChange()
def ProcessContentUpdates( self, service_keys_to_content_updates ):
@ -3594,7 +3583,6 @@ class MediaPanelThumbnails( MediaPanel ):
( wx.ACCEL_SHIFT, wx.WXK_RIGHT, ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'key_shift_right' ) ),
( wx.ACCEL_SHIFT, wx.WXK_NUMPAD_RIGHT, ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'key_shift_right' ) ),
( wx.ACCEL_CTRL, ord( 'A' ), ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'select', 'all' ) ),
( wx.ACCEL_CTRL, ord( 'c' ), ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'copy_files' ) ),
( wx.ACCEL_CTRL, wx.WXK_SPACE, ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'ctrl-space' ) )
]

View File

@ -576,12 +576,14 @@ class Page( wx.SplitterWindow ):
def PageHidden( self ):
self._controller.pub( 'page_hidden', self._page_key )
self._management_panel.PageHidden()
self._media_panel.PageHidden()
def PageShown( self ):
self._controller.pub( 'page_shown', self._page_key )
self._management_panel.PageShown()
self._media_panel.PageShown()
def PrepareToHide( self ):
@ -1998,12 +2000,20 @@ class PagesNotebook( wx.Notebook ):
page.Reparent( dest_notebook )
self._controller.pub( 'refresh_page_name', source_notebook.GetPageKey() )
dest_notebook.InsertPage( insertion_tab_index, page, page.GetName() )
shift_down = wx.GetKeyState( wx.WXK_SHIFT )
self.ShowPage( page )
follow_dropped_page = not shift_down
dest_notebook.InsertPage( insertion_tab_index, page, page.GetName(), select = follow_dropped_page )
if follow_dropped_page:
self.ShowPage( page )
self._controller.pub( 'refresh_page_name', source_notebook.GetPageKey() )
self._controller.pub( 'refresh_page_name', page.GetPageKey() )

View File

@ -15,9 +15,12 @@ import ClientGUIMenus
import ClientGUIScrolledPanels
import ClientGUISeedCache
import ClientGUITopLevelWindows
import ClientNetworkingDomain
import ClientParsing
import ClientTags
import HydrusConstants as HC
import HydrusData
import HydrusExceptions
import HydrusGlobals as HG
import HydrusNetwork
import HydrusSerialisable
@ -161,6 +164,47 @@ class EditBandwidthRulesPanel( ClientGUIScrolledPanels.EditPanel ):
return self._bandwidth_rules_ctrl.GetValue()
class EditChooseMultiple( ClientGUIScrolledPanels.EditPanel ):
def __init__( self, parent, choice_tuples ):
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
self._checkboxes = wx.CheckListBox( self )
self._checkboxes.SetMinSize( ( 320, 420 ) )
for ( i, ( label, data, selected ) ) in enumerate( choice_tuples ):
self._checkboxes.Append( label, data )
if selected:
self._checkboxes.Check( i )
#
vbox = wx.BoxSizer( wx.VERTICAL )
vbox.AddF( self._checkboxes, CC.FLAGS_EXPAND_BOTH_WAYS )
self.SetSizer( vbox )
def GetValue( self ):
datas = []
for index in self._checkboxes.GetChecked():
datas.append( self._checkboxes.GetClientData( index ) )
return datas
class EditDuplicateActionOptionsPanel( ClientGUIScrolledPanels.EditPanel ):
def __init__( self, parent, duplicate_action, duplicate_action_options ):
@ -1190,45 +1234,183 @@ class EditServersideService( ClientGUIScrolledPanels.EditPanel ):
class EditChooseMultiple( ClientGUIScrolledPanels.EditPanel ):
class EditStringMatchPanel( ClientGUIScrolledPanels.EditPanel ):
def __init__( self, parent, choice_tuples ):
def __init__( self, parent, string_match ):
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
self._checkboxes = wx.CheckListBox( self )
self._match_type = ClientGUICommon.BetterChoice( self )
self._checkboxes.SetMinSize( ( 320, 420 ) )
self._match_type.Append( ClientParsing.STRING_MATCH_ANY, 'any characters' )
self._match_type.Append( ClientParsing.STRING_MATCH_FIXED, 'fixed characters' )
self._match_type.Append( ClientParsing.STRING_MATCH_FLEXIBLE, 'character set' )
self._match_type.Append( ClientParsing.STRING_MATCH_REGEX, 'regex' )
for ( i, ( label, data, selected ) ) in enumerate( choice_tuples ):
self._checkboxes.Append( label, data )
if selected:
self._checkboxes.Check( i )
self._match_value_text_input = wx.TextCtrl( self )
self._match_value_flexible_input = ClientGUICommon.BetterChoice( self )
self._match_value_flexible_input.Append( ClientParsing.ALPHA, 'alphabetic characters (a-zA-Z)' )
self._match_value_flexible_input.Append( ClientParsing.ALPHANUMERIC, 'alphanumeric characters (a-zA-Z0-9)' )
self._match_value_flexible_input.Append( ClientParsing.NUMERIC, 'numeric characters (0-9)' )
self._min_chars = ClientGUICommon.NoneableSpinCtrl( self, min = 1, max = 65535, unit = 'characters', none_phrase = 'no limit' )
self._max_chars = ClientGUICommon.NoneableSpinCtrl( self, min = 1, max = 65535, unit = 'characters', none_phrase = 'no limit' )
self._example_string = wx.TextCtrl( self )
self._example_string_matches = ClientGUICommon.BetterStaticText( self )
#
( match_type, match_value, min_chars, max_chars, example_string ) = string_match.ToTuple()
self._match_type.SetClientData( match_type )
if match_type == ClientParsing.STRING_MATCH_FLEXIBLE:
self._match_value_flexible_input.SetClientData( match_value )
else:
self._match_value_flexible_input.SetClientData( ClientParsing.ALPHA )
self._match_value_text_input.SetValue( match_value )
self._UpdateControls()
#
rows = []
rows.append( ( 'match type: ', self._match_type ) )
rows.append( ( 'match text: ', self._match_value_text_input ) )
rows.append( ( 'match value (character set): ', self._match_value_flexible_input ) )
rows.append( ( 'minumum allowed number of characters: ', self._min_chars ) )
rows.append( ( 'maximum allowed number of characters: ', self._max_chars ) )
rows.append( ( 'example string: ', self._example_string ) )
gridbox = ClientGUICommon.WrapInGrid( self, rows )
vbox = wx.BoxSizer( wx.VERTICAL )
vbox.AddF( self._checkboxes, CC.FLAGS_EXPAND_BOTH_WAYS )
vbox.AddF( gridbox, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( self._example_string_matches, CC.FLAGS_EXPAND_PERPENDICULAR )
self.SetSizer( vbox )
#
self._match_type.Bind( wx.EVT_CHOICE, self.EventUpdate )
self._match_value_text_input.Bind( wx.EVT_TEXT, self.EventUpdate )
self._match_value_flexible_input.Bind( wx.EVT_CHOICE, self.EventUpdate )
self._min_chars.Bind( wx.EVT_SPINCTRL, self.EventUpdate )
self._max_chars.Bind( wx.EVT_SPINCTRL, self.EventUpdate )
self._example_string.Bind( wx.EVT_TEXT, self.EventUpdate )
def _GetValue( self ):
match_type = self._match_type.GetChoice()
if match_type == ClientParsing.STRING_MATCH_ANY:
match_value = ''
elif match_type == ClientParsing.STRING_MATCH_FLEXIBLE:
match_value = self._match_value_flexible_input.GetChoice()
else:
match_value = self._match_value_text_input.GetValue()
min_chars = self._min_chars.GetValue()
max_chars = self._max_chars.GetValue()
example_string = self._example_string.GetValue()
string_match = ClientParsing.StringMatch( match_type = match_type, match_value = match_value, min_chars = min_chars, max_chars = max_chars, example_string = example_string )
return string_match
def _UpdateControls( self ):
match_type = self._match_type.GetChoice()
if match_type == ClientParsing.STRING_MATCH_ANY:
self._match_value_text_input.Disable()
self._match_value_flexible_input.Disable()
elif match_type == ClientParsing.STRING_MATCH_FLEXIBLE:
self._match_value_text_input.Disable()
self._match_value_flexible_input.Enable()
else:
self._match_value_text_input.Enable()
self._match_value_flexible_input.Disable()
if match_type == ClientParsing.STRING_MATCH_FIXED:
self._min_chars.SetValue( None )
self._max_chars.SetValue( None )
self._min_chars.Disable()
self._max_chars.Disable()
self._example_string.SetValue( self._match_value_text_input.GetValue() )
self._example_string_matches.SetLabelText( '' )
else:
self._min_chars.Enable()
self._max_chars.Enable()
string_match = self._GetValue()
( result, reason ) = string_match.Test( self._example_string.GetValue() )
if result:
self._example_string_matches.SetLabelText( 'Example matches ok!' )
self._example_string_matches.SetForegroundColour( ( 0, 128, 0 ) )
else:
self._example_string_matches.SetLabelText( 'Example does not match - ' + reason )
self._example_string_matches.SetForegroundColour( ( 128, 0, 0 ) )
def EventUpdate( self, event ):
self._UpdateControls()
def GetValue( self ):
datas = []
string_match = self._GetValue()
for index in self._checkboxes.GetChecked():
( result, reason ) = string_match.Test( self._example_string.GetValue() )
if not result:
datas.append( self._checkboxes.GetClientData( index ) )
wx.MessageBox( 'Please enter an example text that matches the given rules!' )
raise HydrusExceptions.VetoException()
return datas
return string_match
class EditSubscriptionPanel( ClientGUIScrolledPanels.EditPanel ):
@ -1673,7 +1855,6 @@ class EditSubscriptionPanel( ClientGUIScrolledPanels.EditPanel ):
self._UpdateSeedInfo()
class EditTagCensorPanel( ClientGUIScrolledPanels.EditPanel ):
def __init__( self, parent, tag_censor ):
@ -2123,3 +2304,310 @@ class EditTagImportOptions( ClientGUIScrolledPanels.EditPanel ):
return tag_import_options
class EditURLMatch( ClientGUIScrolledPanels.EditPanel ):
def __init__( self, parent, url_match ):
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
self._name = wx.TextCtrl( self )
self._preferred_scheme = ClientGUICommon.BetterChoice( self )
self._preferred_scheme.Append( 'http', 'http' )
self._preferred_scheme.Append( 'https', 'https' )
self._netloc = wx.TextCtrl( self )
self._subdomain_is_important = wx.CheckBox( self )
#
path_components_panel = ClientGUICommon.StaticBox( self, 'path components' )
self._path_components = ClientGUIListBoxes.QueueListBox( path_components_panel, self._ConvertPathComponentToString, self._AddPathComponent, self._EditPathComponent )
#
parameters_panel = ClientGUICommon.StaticBox( self, 'parameters' )
self._parameters = ClientGUIListCtrl.BetterListCtrl( parameters_panel, 'url_match_path_components', 5, 20, [ ( 'key', 14 ), ( 'value', -1 ) ], self._ConvertParameterToListCtrlTuples, delete_key_callback = self._DeleteParameters, activation_callback = self._EditParameters )
# parameter buttons, do it with a new wrapper panel class
#
self._example_url = wx.TextCtrl( self )
self._example_url_matches = ClientGUICommon.BetterStaticText( self )
#
name = url_match.GetName()
self._name.SetValue( name )
( preferred_scheme, netloc, subdomain_is_important, path_components, parameters, example_url ) = url_match.ToTuple()
self._preferred_scheme.SelectClientData( preferred_scheme )
self._netloc.SetValue( netloc )
self._subdomain_is_important.SetValue( subdomain_is_important )
self._path_components.AddDatas( path_components )
00
self._parameters.AddDatas( parameters.items() )
self._example_url.SetValue( example_url )
self._UpdateControls()
#
rows = []
rows.append( ( 'name: ', self._name ) )
rows.append( ( 'preferred scheme: ', self._preferred_scheme ) )
rows.append( ( 'network location: ', self._netloc ) )
rows.append( ( 'keep subdomains?: ', self._subdomain_is_important ) )
gridbox_1 = ClientGUICommon.WrapInGrid( self, rows )
rows = []
rows.append( ( 'example url: ', self._example_url ) )
gridbox_2 = ClientGUICommon.WrapInGrid( self, rows )
vbox = wx.BoxSizer( wx.VERTICAL )
vbox.AddF( gridbox_1, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( path_components_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
vbox.AddF( parameters_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
vbox.AddF( self._example_url_matches, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( gridbox_2, CC.FLAGS_EXPAND_PERPENDICULAR )
self.SetSizer( vbox )
#
self._preferred_scheme.Bind( wx.EVT_CHOICE, self.EventUpdate )
self._netloc.Bind( wx.EVT_TEXT, self.EventUpdate )
self._subdomain_is_important.Bind( wx.EVT_CHECKBOX, self.EventUpdate )
self._example_url.Bind( wx.EVT_TEXT, self.EventUpdate )
def _AddParameters( self ):
# throw up a dialog to take key text
# warn on key conflict I guess
# throw up a string match dialog to take value
# add it
pass
def _AddPathComponent( self ):
string_match = ClientParsing.StringMatch()
return self._EditPathComponent( string_match )
def _ConvertParameterToListCtrlTuples( self, data ):
( key, string_match ) = data
pretty_key = key
pretty_string_match = string_match.ToUnicode()
sort_key = pretty_key
sort_string_match = pretty_string_match
display_tuple = ( pretty_key, pretty_string_match )
sort_tuple = ( sort_key, sort_string_match )
return ( display_tuple, sort_tuple )
def _ConvertPathComponentToString( self, path_component ):
return path_component.ToUnicode()
def _DeleteParameters( self ):
# ask for certain, then do it
pass
def _EditParameters( self ):
# for each in list, throw up dialog for key, value
# delete and readd
# break on cancel, etc...
# sort at the end
pass
def _EditPathComponent( self, string_match ):
with ClientGUITopLevelWindows.DialogEdit( self, 'edit path component' ) as dlg:
panel = EditStringMatchPanel( dlg, string_match )
dlg.SetPanel( panel )
if dlg.Showmodal() == wx.ID_OK:
new_string_match = panel.GetValue()
return ( True, new_string_match )
else:
return ( False, None )
def _GetValue( self ):
name = self._name.GetValue()
preferred_scheme = self._preferred_scheme.GetChoice()
netloc = self._netloc.GetValue()
subdomain_is_important = self._subdomain_is_important.GetValue()
path_components = self._path_components.GetData()
parameters = self._parameters.GetData()
example_url = self._example_url.GetValue()
url_match = ClientNetworkingDomain.URLMatch( name, preferred_scheme = preferred_scheme, netloc = netloc, subdomain_is_important = subdomain_is_important, path_components = path_components, parameters = parameters, example_url = example_url )
return url_match
def _UpdateControls( self ):
url_match = self._GetValue()
( result, reason ) = url_match.Test( self._example_url.GetValue() )
if result:
self._example_url_matches.SetLabelText( 'Example matches ok!' )
self._example_url_matches.SetForegroundColour( ( 0, 128, 0 ) )
else:
self._example_url_matches.SetLabelText( 'Example does not match - ' + reason )
self._example_url_matches.SetForegroundColour( ( 128, 0, 0 ) )
def EventUpdate( self, event ):
self._UpdateControls()
def GetValue( self ):
url_match = self._GetValue()
( result, reason ) = url_match.Test( self._example_url.GetValue() )
if not result:
wx.MessageBox( 'Please enter an example url that matches the given rules!' )
raise HydrusExceptions.VetoException()
return url_match
class EditWatcherOptions( ClientGUIScrolledPanels.EditPanel ):
def __init__( self, parent, watcher_options ):
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
help_button = ClientGUICommon.BetterBitmapButton( self, CC.GlobalBMPs.help, self._ShowHelp )
help_button.SetToolTipString( 'Show help regarding these checker options.' )
# add statictext or whatever that will update on any updates above to say 'given velocity of blah and last check at blah, next check in 5 mins'
# or indeed this could just take the seed cache and last check of the caller, if there is one
# this would be more useful to the user, to know 'right, on ok, it'll refresh in 30 mins'
self._intended_files_per_check = wx.SpinCtrl( self, min = 1, max = 1000 )
self._never_faster_than = ClientGUICommon.TimeDeltaCtrl( self, min = 30, days = True, hours = True, minutes = True, seconds = True )
self._never_slower_than = ClientGUICommon.TimeDeltaCtrl( self, min = 600, days = True, hours = True, minutes = True )
self._death_file_velocity = ClientGUICommon.VelocityCtrl( self, min_time_delta = 60, days = True, hours = True, minutes = True, per_phrase = 'in', unit = 'files' )
#
( intended_files_per_check, never_faster_than, never_slower_than, death_file_velocity ) = watcher_options.ToTuple()
self._intended_files_per_check.SetValue( intended_files_per_check )
self._never_faster_than.SetValue( never_faster_than )
self._never_slower_than.SetValue( never_slower_than )
self._death_file_velocity.SetValue( death_file_velocity )
#
rows = []
rows.append( ( 'intended new files per check: ', self._intended_files_per_check ) )
rows.append( ( 'stop checking if new files found falls below: ', self._death_file_velocity ) )
rows.append( ( 'never check faster than once per: ', self._never_faster_than ) )
rows.append( ( 'never check slower than once per: ', self._never_slower_than ) )
gridbox = ClientGUICommon.WrapInGrid( self, rows )
vbox = wx.BoxSizer( wx.VERTICAL )
help_hbox = wx.BoxSizer( wx.HORIZONTAL )
st = ClientGUICommon.BetterStaticText( self, 'help for this panel -->' )
st.SetForegroundColour( wx.Colour( 0, 0, 255 ) )
help_hbox.AddF( st, CC.FLAGS_VCENTER )
help_hbox.AddF( help_button, CC.FLAGS_VCENTER )
vbox.AddF( help_hbox, CC.FLAGS_LONE_BUTTON )
vbox.AddF( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
self.SetSizer( vbox )
def _ShowHelp( self ):
help = 'PROTIP: Do not change anything here unless you understand what it means!'
help += os.linesep * 2
help += 'After its initialisation check, the checker times future checks so that it will probably find the same specified number of new files each time. When files are being posted frequently, it will check more often. When things are slow, it will slow down as well.'
help += os.linesep * 2
help += 'For instance, if it were set to try for 5 new files with every check, and at the last check it knew that the last 24 hours had produced 10 new files, it would check again 12 hours later. When that check was done and any new files found, it would then recalculate and repeat the process.'
help += os.linesep * 2
help += 'If the \'file velocity\' drops below a certain amount, the checker considers the source of files dead and will stop checking. If it falls into this state but you think there might have been a rush of new files, hit the \'check now\' button in an attempt to revive the checker. If there are new files, it will start checking again until they drop off once more.'
wx.MessageBox( help )
def GetValue( self ):
intended_files_per_check = self._intended_files_per_check.GetValue()
never_faster_than = self._never_faster_than.GetValue()
never_slower_than = self._never_slower_than.GetValue()
death_file_velocity = self._death_file_velocity.GetValue()
return ClientData.WatcherOptions( intended_files_per_check, never_faster_than, never_slower_than, death_file_velocity )

View File

@ -1865,11 +1865,9 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
thread_checker = ClientGUICommon.StaticBox( self, 'thread checker' )
self._thread_times_to_check = wx.SpinCtrl( thread_checker, min = 0, max = 65536 )
self._thread_times_to_check.SetToolTipString( 'how many times the thread checker will check' )
watcher_options = self._new_options.GetDefaultThreadWatcherOptions()
self._thread_check_period = ClientGUICommon.TimeDeltaButton( thread_checker, min = 30, days = True, hours = True, minutes = True, seconds = True )
self._thread_check_period.SetToolTipString( 'how long the checker will wait between checks' )
self._thread_watcher_options = ClientGUIScrolledPanelsEdit.EditWatcherOptions( thread_checker, watcher_options )
#
@ -1877,12 +1875,6 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._gallery_file_limit.SetValue( HC.options[ 'gallery_file_limit' ] )
( times_to_check, check_period ) = HC.options[ 'thread_checker_timings' ]
self._thread_times_to_check.SetValue( times_to_check )
self._thread_check_period.SetValue( check_period )
#
rows = []
@ -1899,14 +1891,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
#
rows = []
rows.append( ( 'default number of times to check: ', self._thread_times_to_check ) )
rows.append( ( 'default wait between checks: ', self._thread_check_period ) )
gridbox = ClientGUICommon.WrapInGrid( thread_checker, rows )
thread_checker.AddF( gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
thread_checker.AddF( self._thread_watcher_options, CC.FLAGS_EXPAND_PERPENDICULAR )
#
@ -1923,7 +1908,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._new_options.SetBoolean( 'verify_regular_https', self._verify_regular_https.GetValue() )
HC.options[ 'gallery_file_limit' ] = self._gallery_file_limit.GetValue()
HC.options[ 'thread_checker_timings' ] = ( self._thread_times_to_check.GetValue(), self._thread_check_period.GetValue() )
self._new_options.SetDefaultThreadWatcherOptions( self._thread_watcher_options.GetValue() )
@ -6244,6 +6230,72 @@ class ManageURLsPanel( ClientGUIScrolledPanels.ManagePanel ):
class ManageURLMatchesPanel( ClientGUIScrolledPanels.ManagePanel ):
def __init__( self, parent ):
ClientGUIScrolledPanels.ManagePanel.__init__( self, parent )
self._url_matches = ClientGUIListCtrl.BetterListCtrl( self, 'manage_url_matches', 15, 30, [ ( 'name', 20 ), ( 'example url', -1 ) ], self._ConvertURLMatchToListCtrlTuples, delete_key_callback = self._Delete, activation_callback = self._Edit )
# add, edit, delete buttons
# it would be nice to wrap this up in a panel rather than writing it out manually
#
# load them from the db, populate the listctrl
self._original_names = set()
# set self._original_names
#
vbox = wx.BoxSizer( wx.VERTICAL )
vbox.AddF( self._url_matches, CC.FLAGS_EXPAND_BOTH_WAYS )
self.SetSizer( vbox )
def _Add( self ):
pass
def _ConvertURLMatchToListCtrlTuples( self, url_match ):
name = url_match.GetName()
example_url = url_match.GetExampleURL()
display_tuple = ( name, example_url )
sort_tuple = display_tuple
return ( display_tuple, sort_tuple )
def _Delete( self ):
pass
def _Edit( self ):
pass
def CommitChanges( self ):
datas = self._url_matches.GetData()
datas_names = { data.GetName() for data in datas }
to_delete = [ name for name in self._original_names if name not in datas_names ]
# save datas to db
# delete names from db
class RepairFileSystemPanel( ClientGUIScrolledPanels.ManagePanel ):
def __init__( self, parent, missing_locations ):
@ -6409,3 +6461,4 @@ class RepairFileSystemPanel( ClientGUIScrolledPanels.ManagePanel ):
HG.client_controller.WriteSynchronous( 'repair_client_files', correct_rows )

View File

@ -24,7 +24,7 @@ class EditSeedCachePanel( ClientGUIScrolledPanels.EditPanel ):
# add index control row here, hide it if needed and hook into showing/hiding and postsizechangedevent on seed add/remove
columns = [ ( '#', 3 ), ( 'source', -1 ), ( 'status', 12 ), ( 'added', 20 ), ( 'last modified', 20 ), ( 'note', 30 ) ]
columns = [ ( '#', 3 ), ( 'source', -1 ), ( 'status', 12 ), ( 'added', 23 ), ( 'last modified', 23 ), ( 'source time', 23 ), ( 'note', 20 ) ]
self._list_ctrl = ClientGUIListCtrl.BetterListCtrl( self, 'seed_cache', 30, 30, columns, self._ConvertSeedToListCtrlTuples )
@ -59,16 +59,26 @@ class EditSeedCachePanel( ClientGUIScrolledPanels.EditPanel ):
sort_tuple = self._seed_cache.GetSeedInfo( seed )
( seed_index, seed, status, added_timestamp, last_modified_timestamp, note ) = sort_tuple
( seed_index, seed, status, added_timestamp, last_modified_timestamp, source_timestamp, note ) = sort_tuple
pretty_seed_index = HydrusData.ConvertIntToPrettyString( seed_index )
pretty_seed = HydrusData.ToUnicode( seed )
pretty_status = CC.status_string_lookup[ status ]
pretty_added = HydrusData.ConvertTimestampToPrettyAgo( added_timestamp ) + ' ago'
pretty_modified = HydrusData.ConvertTimestampToPrettyAgo( last_modified_timestamp ) + ' ago'
if source_timestamp is None:
pretty_source_time = 'unknown'
else:
pretty_source_time = HydrusData.ConvertTimestampToHumanPrettyTime( source_timestamp )
pretty_note = note.split( os.linesep )[0]
display_tuple = ( pretty_seed_index, pretty_seed, pretty_status, pretty_added, pretty_modified, pretty_note )
display_tuple = ( pretty_seed_index, pretty_seed, pretty_status, pretty_added, pretty_modified, pretty_source_time, pretty_note )
return ( display_tuple, sort_tuple )
@ -79,7 +89,7 @@ class EditSeedCachePanel( ClientGUIScrolledPanels.EditPanel ):
for seed in self._list_ctrl.GetData( only_selected = True ):
( seed_index, seed, status, added_timestamp, last_modified_timestamp, note ) = self._seed_cache.GetSeedInfo( seed )
( seed_index, seed, status, added_timestamp, last_modified_timestamp, source_timestamp, note ) = self._seed_cache.GetSeedInfo( seed )
if note != '':

View File

@ -1171,6 +1171,22 @@ class HDDImport( HydrusSerialisable.SerialisableBase ):
self._paths_cache.AddSeeds( paths )
for path in paths:
try:
s = os.stat( path )
source_time = min( s.st_mtime, s.st_ctime )
self._paths_cache.UpdateSeedSourceTime( path, source_time )
except:
pass
self._file_import_options = file_import_options
self._paths_to_tags = paths_to_tags
@ -2438,7 +2454,7 @@ HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIAL
class SeedCache( HydrusSerialisable.SerialisableBase ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_SEED_CACHE
SERIALISABLE_VERSION = 5
SERIALISABLE_VERSION = 6
def __init__( self ):
@ -2496,20 +2512,6 @@ class SeedCache( HydrusSerialisable.SerialisableBase ):
self._dirty = False
def _GetSeedTuple( self, seed ):
seed_index = self._seeds_to_indices[ seed ]
seed_info = self._seeds_to_info[ seed ]
status = seed_info[ 'status' ]
added_timestamp = seed_info[ 'added_timestamp' ]
last_modified_timestamp = seed_info[ 'last_modified_timestamp' ]
note = seed_info[ 'note' ]
return ( seed_index, seed, status, added_timestamp, last_modified_timestamp, note )
def _GetSerialisableInfo( self ):
with self._lock:
@ -2527,6 +2529,20 @@ class SeedCache( HydrusSerialisable.SerialisableBase ):
def _GetSourceTimestamp( self, seed ):
seed_info = self._seeds_to_info[ seed ]
source_timestamp = seed_info[ 'source_timestamp' ]
if source_timestamp is None:
source_timestamp = seed_info[ 'added_timestamp' ] # decent fallback compromise
return source_timestamp
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
with self._lock:
@ -2680,6 +2696,20 @@ class SeedCache( HydrusSerialisable.SerialisableBase ):
return ( 5, new_serialisable_info )
if version == 5:
new_serialisable_info = []
for ( seed, seed_info ) in old_serialisable_info:
seed_info[ 'source_timestamp' ] = None
new_serialisable_info.append( ( seed, seed_info ) )
return ( 6, new_serialisable_info )
def AddSeeds( self, seeds ):
@ -2719,6 +2749,7 @@ class SeedCache( HydrusSerialisable.SerialisableBase ):
seed_info[ 'status' ] = CC.STATUS_UNKNOWN
seed_info[ 'added_timestamp' ] = now
seed_info[ 'last_modified_timestamp' ] = now
seed_info[ 'source_timestamp' ] = None
seed_info[ 'note' ] = ''
self._seeds_to_info[ seed ] = seed_info
@ -2774,6 +2805,16 @@ class SeedCache( HydrusSerialisable.SerialisableBase ):
HG.client_controller.pub( 'seed_cache_seeds_updated', self._seed_cache_key, ( seed, ) )
def GetEarliestTimestamp( self ):
with self._lock:
earliest_timestamp = min( ( self._GetSourceTimestamp( seed ) for seed in self._seeds_ordered ) )
return earliest_timestamp
def GetNextSeed( self, status ):
with self._lock:
@ -2792,6 +2833,26 @@ class SeedCache( HydrusSerialisable.SerialisableBase ):
return None
def GetNumNewFilesSince( self, since ):
num_files = 0
with self._lock:
for seed in self._seeds_ordered:
source_timestamp = self._GetSourceTimestamp( seed )
if source_timestamp > since:
num_files += 1
return num_files
def GetSeedCacheKey( self ):
return self._seed_cache_key
@ -2855,7 +2916,17 @@ class SeedCache( HydrusSerialisable.SerialisableBase ):
with self._lock:
return self._GetSeedTuple( seed )
seed_index = self._seeds_to_indices[ seed ]
seed_info = self._seeds_to_info[ seed ]
status = seed_info[ 'status' ]
added_timestamp = seed_info[ 'added_timestamp' ]
last_modified_timestamp = seed_info[ 'last_modified_timestamp' ]
source_timestamp = seed_info[ 'source_timestamp' ]
note = seed_info[ 'note' ]
return ( seed_index, seed, status, added_timestamp, last_modified_timestamp, source_timestamp, note )
@ -2948,6 +3019,18 @@ class SeedCache( HydrusSerialisable.SerialisableBase ):
HG.client_controller.pub( 'seed_cache_seeds_updated', self._seed_cache_key, seeds_to_delete )
def UpdateSeedSourceTime( self, seed, source_timestamp ):
# this is ugly--this should all be moved to the seed when it becomes a cleverer object, rather than jimmying it through the cache
with self._lock:
seed_info = self._seeds_to_info[ seed ]
seed_info[ 'source_timestamp' ] = source_timestamp
def UpdateSeedStatus( self, seed, status, note = '', exception = None ):
with self._lock:
@ -3892,7 +3975,7 @@ HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIAL
class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_THREAD_WATCHER_IMPORT
SERIALISABLE_VERSION = 1
SERIALISABLE_VERSION = 2
MIN_CHECK_PERIOD = 30
@ -3906,17 +3989,16 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
tag_import_options = new_options.GetDefaultTagImportOptions( ClientDownloading.GalleryIdentifier( HC.SITE_TYPE_THREAD_WATCHER ) )
( times_to_check, check_period ) = HC.options[ 'thread_checker_timings' ]
self._thread_url = ''
self._urls_cache = SeedCache()
self._urls_to_filenames = {}
self._urls_to_md5_base64 = {}
self._watcher_options = new_options.GetDefaultThreadWatcherOptions()
self._file_import_options = file_import_options
self._tag_import_options = tag_import_options
self._times_to_check = times_to_check
self._check_period = check_period
self._last_time_checked = 0
self._last_check_time = 0
self._next_check_time = None
self._download_control_file_set = None
self._download_control_file_clear = None
@ -3924,10 +4006,12 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
self._download_control_thread_clear = None
self._check_now = False
self._paused = False
self._files_paused = False
self._thread_paused = False
self._watcher_status = ''
self._file_velocity_status = ''
self._current_action = ''
self._watcher_status = ''
self._thread_key = HydrusData.GenerateKey()
@ -3937,24 +4021,210 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
self._new_thread_event = threading.Event()
def _CheckThread( self, page_key ):
error_occurred = False
watcher_status_should_stick = True
with self._lock:
self._watcher_status = 'checking thread'
try:
json_url = ClientDownloading.GetImageboardThreadJSONURL( self._thread_url )
network_job = ClientNetworking.NetworkJobThreadWatcher( self._thread_key, 'GET', json_url )
network_job.OverrideBandwidth()
HG.client_controller.network_engine.AddJob( network_job )
with self._lock:
if self._download_control_thread_set is not None:
wx.CallAfter( self._download_control_thread_set, network_job )
try:
network_job.WaitUntilDone()
finally:
if self._download_control_thread_clear is not None:
wx.CallAfter( self._download_control_thread_clear )
raw_json = network_job.GetContent()
file_infos = ClientDownloading.ParseImageboardFileURLsFromJSON( self._thread_url, raw_json )
new_urls = []
new_urls_set = set()
file_urls_to_source_timestamps = {}
for ( file_url, file_md5_base64, file_original_filename, source_timestamp ) in file_infos:
if not self._urls_cache.HasSeed( file_url ) and not file_url in new_urls_set:
new_urls.append( file_url )
new_urls_set.add( file_url )
self._urls_to_filenames[ file_url ] = file_original_filename
if file_md5_base64 is not None:
self._urls_to_md5_base64[ file_url ] = file_md5_base64
file_urls_to_source_timestamps[ file_url ] = source_timestamp
self._urls_cache.AddSeeds( new_urls )
for ( file_url, source_timestamp ) in file_urls_to_source_timestamps.items():
self._urls_cache.UpdateSeedSourceTime( file_url, source_timestamp )
num_new = len( new_urls )
watcher_status = 'thread checked OK - ' + HydrusData.ConvertIntToPrettyString( num_new ) + ' new urls'
watcher_status_should_stick = False
if num_new > 0:
self._new_files_event.set()
except HydrusExceptions.NotFoundException:
error_occurred = True
watcher_status = 'thread 404'
except Exception as e:
error_occurred = True
watcher_status = HydrusData.ToUnicode( e )
HydrusData.PrintException( e )
with self._lock:
if self._check_now:
self._check_now = False
self._watcher_status = watcher_status
self._last_check_time = HydrusData.GetNow()
self._UpdateFileVelocityStatus()
self._UpdateNextCheckTime()
if error_occurred:
self._thread_paused = True
if error_occurred:
time.sleep( 5 )
if not watcher_status_should_stick:
time.sleep( 5 )
with self._lock:
self._watcher_status = ''
def _GetSerialisableInfo( self ):
serialisable_url_cache = self._urls_cache.GetSerialisableTuple()
serialisable_watcher_options = self._watcher_options.GetSerialisableTuple()
serialisable_file_options = self._file_import_options.GetSerialisableTuple()
serialisable_tag_options = self._tag_import_options.GetSerialisableTuple()
return ( self._thread_url, serialisable_url_cache, self._urls_to_filenames, self._urls_to_md5_base64, serialisable_file_options, serialisable_tag_options, self._times_to_check, self._check_period, self._last_time_checked, self._paused )
return ( self._thread_url, serialisable_url_cache, self._urls_to_filenames, self._urls_to_md5_base64, serialisable_watcher_options, serialisable_file_options, serialisable_tag_options, self._last_check_time, self._files_paused, self._thread_paused )
def _HasThread( self ):
return self._thread_url != ''
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
( self._thread_url, serialisable_url_cache, self._urls_to_filenames, self._urls_to_md5_base64, serialisable_file_options, serialisable_tag_options, self._times_to_check, self._check_period, self._last_time_checked, self._paused ) = serialisable_info
( self._thread_url, serialisable_url_cache, self._urls_to_filenames, self._urls_to_md5_base64, serialisable_watcher_options, serialisable_file_options, serialisable_tag_options, self._last_check_time, self._files_paused, self._thread_paused ) = serialisable_info
self._urls_cache = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_url_cache )
self._watcher_options = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_watcher_options )
self._file_import_options = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_file_options )
self._tag_import_options = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_tag_options )
def _UpdateFileVelocityStatus( self ):
self._file_velocity_status = self._watcher_options.GetPrettyCurrentVelocity( self._urls_cache, self._last_check_time )
def _UpdateNextCheckTime( self ):
if self._check_now:
self._next_check_time = self._last_check_time + self.MIN_CHECK_PERIOD
else:
if self._watcher_options.IsDead( self._urls_cache, self._last_check_time ):
self._watcher_status = 'thread is dead'
self._thread_paused = True
self._next_check_time = self._watcher_options.GetNextCheckTime( self._urls_cache, self._last_check_time )
def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
if version == 1:
( thread_url, serialisable_url_cache, urls_to_filenames, urls_to_md5_base64, serialisable_file_options, serialisable_tag_options, times_to_check, check_period, last_check_time, paused ) = old_serialisable_info
watcher_options = ClientData.WatcherOptions( intended_files_per_check = 8, never_faster_than = 300, never_slower_than = 86400, death_file_velocity = ( 1, 86400 ) )
serialisable_watcher_options = watcher_options.GetSerialisableTuple()
files_paused = paused
thread_paused = paused
new_serialisable_info = ( thread_url, serialisable_url_cache, urls_to_filenames, urls_to_md5_base64, serialisable_watcher_options, serialisable_file_options, serialisable_tag_options, last_check_time, files_paused, thread_paused )
return ( 2, new_serialisable_info )
def _WorkOnFiles( self, page_key ):
file_url = self._urls_cache.GetNextSeed( CC.STATUS_UNKNOWN )
@ -4139,168 +4409,11 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
return True
def _WorkOnThread( self, page_key ):
error_occurred = False
with self._lock:
p1 = self._check_now and HydrusData.TimeHasPassed( self._last_time_checked + self.MIN_CHECK_PERIOD )
p2 = self._times_to_check > 0 and HydrusData.TimeHasPassed( self._last_time_checked + self._check_period )
if p1 or p2:
with self._lock:
self._watcher_status = 'checking thread'
try:
json_url = ClientDownloading.GetImageboardThreadJSONURL( self._thread_url )
network_job = ClientNetworking.NetworkJobThreadWatcher( self._thread_key, 'GET', json_url )
network_job.OverrideBandwidth()
HG.client_controller.network_engine.AddJob( network_job )
with self._lock:
if self._download_control_thread_set is not None:
wx.CallAfter( self._download_control_thread_set, network_job )
try:
network_job.WaitUntilDone()
finally:
if self._download_control_thread_clear is not None:
wx.CallAfter( self._download_control_thread_clear )
raw_json = network_job.GetContent()
file_infos = ClientDownloading.ParseImageboardFileURLsFromJSON( self._thread_url, raw_json )
new_urls = []
new_urls_set = set()
for ( file_url, file_md5_base64, file_original_filename ) in file_infos:
if not self._urls_cache.HasSeed( file_url ) and not file_url in new_urls_set:
new_urls.append( file_url )
new_urls_set.add( file_url )
self._urls_to_filenames[ file_url ] = file_original_filename
if file_md5_base64 is not None:
self._urls_to_md5_base64[ file_url ] = file_md5_base64
self._urls_cache.AddSeeds( new_urls )
num_new = len( new_urls )
watcher_status = 'thread checked OK - ' + HydrusData.ConvertIntToPrettyString( num_new ) + ' new urls'
if num_new > 0:
self._new_files_event.set()
except HydrusExceptions.NotFoundException:
error_occurred = True
watcher_status = 'thread 404'
with self._lock:
for i in range( self._times_to_check ):
HG.client_controller.pub( 'decrement_times_to_check', page_key )
self._times_to_check = 0
except Exception as e:
error_occurred = True
watcher_status = HydrusData.ToUnicode( e )
HydrusData.PrintException( e )
with self._lock:
if self._check_now:
self._check_now = False
else:
self._times_to_check -= 1
HG.client_controller.pub( 'decrement_times_to_check', page_key )
self._last_time_checked = HydrusData.GetNow()
else:
with self._lock:
if self._check_now or self._times_to_check > 0:
if self._check_now:
delay = self.MIN_CHECK_PERIOD
else:
delay = self._check_period
watcher_status = 'checking again ' + HydrusData.ConvertTimestampToPrettyPending( self._last_time_checked + delay )
else:
watcher_status = 'checking finished'
with self._lock:
self._watcher_status = watcher_status
if error_occurred:
time.sleep( 5 )
def _THREADWorkOnFiles( self, page_key ):
while not ( HG.view_shutdown or HG.client_controller.PageCompletelyDestroyed( page_key ) ):
if self._paused or HG.client_controller.PageClosedButNotDestroyed( page_key ):
if self._files_paused or HG.client_controller.PageClosedButNotDestroyed( page_key ):
self._new_files_event.wait( 5 )
@ -4344,7 +4457,15 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
while not ( HG.view_shutdown or HG.client_controller.PageCompletelyDestroyed( page_key ) ):
if self._paused or HG.client_controller.PageClosedButNotDestroyed( page_key ):
with self._lock:
able_to_check = self._HasThread() and not self._thread_paused
check_due = HydrusData.TimeHasPassed( self._next_check_time )
time_to_check = able_to_check and check_due
if not time_to_check or HG.client_controller.PageClosedButNotDestroyed( page_key ):
self._new_thread_event.wait( 5 )
@ -4352,14 +4473,9 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
try:
if self._thread_url != '':
self._WorkOnThread( page_key )
self._CheckThread( page_key )
time.sleep( 1 )
# 1s wait here regardless so the text countdown status updates every sec
time.sleep( 5 )
HG.client_controller.WaitUntilPubSubsEmpty()
@ -4381,6 +4497,10 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
self._check_now = True
self._thread_paused = False
self._UpdateNextCheckTime()
self._new_thread_event.set()
@ -4391,7 +4511,7 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
finished = not self._urls_cache.WorkToDo()
return not finished and not self._paused
return not finished and not self._files_paused
@ -4404,7 +4524,7 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
with self._lock:
return ( self._thread_url, self._file_import_options, self._tag_import_options, self._times_to_check, self._check_period )
return ( self._thread_url, self._file_import_options, self._tag_import_options )
@ -4412,9 +4532,15 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
with self._lock:
finished = not self._urls_cache.WorkToDo()
return ( self._current_action, self._files_paused, self._file_velocity_status, self._next_check_time, self._watcher_status, self._check_now, self._thread_paused )
return ( self._current_action, self._watcher_status, self._check_now, self._paused )
def GetWatcherOptions( self ):
with self._lock:
return self._watcher_options
@ -4422,26 +4548,34 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
with self._lock:
return self._thread_url != ''
return self._HasThread()
def PausePlay( self ):
def PausePlayFiles( self ):
with self._lock:
self._paused = not self._paused
self._files_paused = not self._files_paused
self._new_files_event.set()
self._new_thread_event.set()
def SetCheckPeriod( self, check_period ):
def PausePlayThread( self ):
with self._lock:
self._check_period = max( self.MIN_CHECK_PERIOD, check_period )
if self._thread_paused and self._watcher_options.IsDead( self._urls_cache, self._last_check_time ):
self._watcher_status = 'thread is dead--hit check now to try to revive'
else:
self._thread_paused = not self._thread_paused
self._new_thread_event.set()
@ -4489,11 +4623,17 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
def SetTimesToCheck( self, times_to_check ):
def SetWatcherOptions( self, watcher_options ):
with self._lock:
self._times_to_check = times_to_check
self._watcher_options = watcher_options
self._thread_paused = False
self._UpdateNextCheckTime()
self._UpdateFileVelocityStatus()
self._new_thread_event.set()
@ -4501,6 +4641,10 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
def Start( self, page_key ):
self._UpdateNextCheckTime()
self._UpdateFileVelocityStatus()
HG.client_controller.CallToThreadLongRunning( self._THREADWorkOnThread, page_key )
HG.client_controller.CallToThreadLongRunning( self._THREADWorkOnFiles, page_key )

View File

@ -2472,7 +2472,15 @@ class NetworkJob( object ):
def WaitUntilDone( self ):
self._is_done_event.wait()
while True:
self._is_done_event.wait( 5 )
if self.IsDone():
break
with self._lock:

View File

@ -1,27 +1,63 @@
import ClientConstants as CC
import ClientParsing
import HydrusConstants as HC
import HydrusGlobals as HG
import HydrusData
import HydrusExceptions
import HydrusSerialisable
import threading
import urlparse
# this should do network_contexts->user-agent as well, with some kind of approval system in place
# approval needs a new queue in the network engine. this will eventually test downloader validity and so on. failable at that stage
# user-agent info should be exportable/importable on the ui as well
# eventually extend this to do urlmatch->downloader, I think.
# eventually extend this to do urlmatch->downloader_key, I think.
# hence we'll be able to do some kind of dnd_url->new thread watcher page
# hide urls on media viewer based on domain
# decide whether we want to add this to the dirtyobjects loop, and it which case, if anything is appropriate to store in the db separately
# hence making this a serialisableobject itself.
class DomainManager( object ):
def __init__( self, controller ):
self._controller = controller
self._domains_to_url_matches = {}
self._network_contexts_to_custom_headers = {} # user-agent here
# ( header_key, header_value, approved, approval_reason )
# approved is True for user created, None for imported and defaults
self._lock = threading.Lock()
self._Initialise()
def _GetURLMatch( self, url ):
domain = 'blah' # get top urldomain
if domain in self._domains_to_url_matches:
url_matches = self._domains_to_url_matches[ domain ]
# it would be nice to somehow sort these based on descending complexity
# maybe by length of example url
# in this way, url matches can have overlapping desmaign
# e.g. 'post url' vs 'post url, manga subpage'
for url_match in url_matches:
( result_bool, result_reason ) = url_match.Test( url )
if result_bool:
return url_match
return None
def _Initialise( self ):
self._domains_to_url_matches = {}
@ -32,28 +68,70 @@ class DomainManager( object ):
pass
def GetURLMatch( self, url ):
def CanApprove( self, network_contexts ):
# if user selected false for any approval, return false
# network job presumably throws a ValidationError at this point, which will cause any larger queue to pause.
pass
def DoApproval( self, network_contexts ):
# if false on validity check, it presents the user with a yes/no popup with the approval_reason and waits
pass
def GetCustomHeaders( self, network_contexts ):
with self._lock:
domain = 'blah' # get top urldomain
pass
if domain in self._domains_to_url_matches:
# good order is global = least powerful, which I _think_ is how these come.
def GetDownloader( self, url ):
with self._lock:
# this might be better as getdownloaderkey, but we'll see how it shakes out
# might also be worth being a getifhasdownloader
# match the url to a url_match, then lookup that in a 'this downloader can handle this url_match type' dict that we'll manage
pass
def NeedsApproval( self, network_contexts ):
# this is called by the network engine in the new approval queue
# if a job needs approval, it goes to a single step like the login one and waits, possibly failing.
# checks for 'approved is None' on all ncs
pass
def NormaliseURL( self, url ):
# call this before an entry into a seed cache or the db
# use it in the dialog to review mass db-level changes
with self._lock:
url_match = self._GetURLMatch( url )
if url_match is None:
url_matches = self._domains_to_url_matches[ domain ]
for url_match in url_matches:
( result_bool, result_reason ) = url_match.Test( url )
if result_bool:
return url_match
return url
return None
normalised_url = url_match.Normalise( url )
return normalised_url
@ -69,21 +147,41 @@ class DomainManager( object ):
# __hash__ for name? not sure
# maybe all serialisable should return __hash__ of ( type, name ) if they don't already
# that might lead to problems elsewhere, so careful
class URLMatch( object ):
class URLMatch( HydrusSerialisable.SerialisableBaseNamed ):
def __init__( self ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_URL_MATCH
SERIALISABLE_VERSION = 1
def __init__( self, name, preferred_scheme = 'https', netloc = 'hostname.com', subdomain_is_important = False, path_components = None, parameters = None, example_url = 'https://hostname.com' ):
if path_components is None:
path_components = HydrusSerialisable.SerialisableList()
path_components.append( ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_FIXED, match_value = 'post', example_string = 'post' ) )
path_components.append( ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_FIXED, match_value = 'page.php', example_string = 'page.php' ) )
if parameters is None:
parameters = HydrusSerialisable.SerialisableDictionary()
parameters[ 's' ] = ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_FIXED, match_value = 'view', example_string = 'view' )
parameters[ 'id' ] = ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_FLEXIBLE, match_value = ClientParsing.NUMERIC, example_string = '123456' )
# an edit dialog panel for this that has example url and testing of current values
# a parent panel or something that lists all current urls in the db that match and how they will be clipped, is this ok? kind of thing.
self._preferred_scheme = None
self._netloc = None
self._subdomain_is_important = False
self._path_components = None
self._parameters = {}
HydrusSerialisable.SerialisableBaseNamed.__init__( self, name )
self._name = None
self._example_url = None
self._preferred_scheme = 'https'
self._netloc = 'hostname.com'
self._subdomain_is_important = False
self._path_components = HydrusSerialisable.SerialisableList()
self._parameters = HydrusSerialisable.SerialisableDictionary()
self._example_url = 'https://hostname.com/post/page.php?id=123456&s=view'
def _ClipNetLoc( self, netloc ):
@ -105,6 +203,22 @@ class URLMatch( object ):
return netloc
def _GetSerialisableInfo( self ):
serialisable_path_components = self._path_components.GetSerialisableTuple()
serialisable_parameters = self._parameters.GetSerialisableTuple()
return ( self._preferred_scheme, self._netloc, self._subdomain_is_important, serialisable_path_components, serialisable_parameters, self._example_url )
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
( self._preferred_scheme, self._netloc, self._subdomain_is_important, serialisable_path_components, serialisable_parameters, self._example_url ) = serialisable_info
self._path_components = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_path_components )
self._parameters = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_parameters )
def _ClipPath( self, path ):
# /post/show/1326143/akunim-anthro-armband-armwear-clothed-clothing-fem
@ -151,7 +265,7 @@ class URLMatch( object ):
return query
def Clip( self, url ):
def Normalise( self, url ):
p = urlparse.urlparse( url )
@ -225,3 +339,5 @@ class URLMatch( object ):
return ( True, 'good' )
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_URLS_IMPORT ] = URLMatch

View File

@ -1026,19 +1026,35 @@ STRING_MATCH_ANY = 3
ALPHA = 0
ALPHANUMERIC = 1
NUMERIC = 2
# make it serialisable as well
class StringMatch( object ):
class StringMatch( HydrusSerialisable.SerialisableBase ):
def __init__( self, match_type = STRING_MATCH_FIXED, match_value = 'post' ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_STRING_MATCH
SERIALISABLE_VERSION = 1
def __init__( self, match_type = STRING_MATCH_FIXED, match_value = 'post', min_chars = None, max_chars = None, example_string = 'post' ):
HydrusSerialisable.SerialisableBase.__init__( self )
# make a gui control that accepts one of these. displays expected input on the right and colours red/green (and does isvalid) based on current input
# think about replacing the veto stuff above with this.
self._match_type = match_type
self._match_value = match_value
self._min_chars = None
self._max_chars = None
self._min_chars = min_chars
self._max_chars = max_chars
self._example_string = example_string
def _GetSerialisableInfo( self ):
return ( self._match_type, self._match_value, self._min_chars, self._max_chars, self._example_string )
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
( self._match_type, self._match_value, self._min_chars, self._max_chars, self._example_string ) = serialisable_info
def SetMaxChars( self, max_chars ):
@ -1059,12 +1075,12 @@ class StringMatch( object ):
if self._min_chars is not None and text_len < self._min_chars:
return ( False, presentation_text + ' had too few characters' )
return ( False, presentation_text + ' had fewer than ' + HydrusData.ConvertIntToPrettyString( self._min_chars ) + ' characters' )
if self._max_chars is not None and text_len > self._max_chars:
return ( False, presentation_text + ' had too many characters' )
return ( False, presentation_text + ' had more than ' + HydrusData.ConvertIntToPrettyString( self._min_chars ) + ' characters' )
if self._match_type == STRING_MATCH_FIXED:
@ -1105,7 +1121,7 @@ class StringMatch( object ):
fail_reason = ' did not match "' + r + '"'
if re.search( r, text ) is None:
if re.search( r, text, flags = re.UNICODE ) is None:
return ( False, presentation_text + fail_reason )
@ -1120,3 +1136,64 @@ class StringMatch( object ):
def ToUnicode( self ):
result = ''
if self._min_chars is not None:
if self._max_chars is not None:
result += 'between ' + HydrusData.ToUnicode( self._min_chars ) + ' and ' + HydrusData.ToUnicode( self._max_chars ) + ' '
else:
result += 'at least ' + HydrusData.ToUnicode( self._min_chars ) + ' '
else:
result += 'at most ' + HydrusData.ToUnicode( self._max_chars ) + ' '
show_example = True
if self._match_type == STRING_MATCH_ANY:
result += 'characters'
elif self._match_type == STRING_MATCH_FIXED:
result += self._match_value
show_example = False
elif self._match_type == STRING_MATCH_FLEXIBLE:
if self._match_value == ALPHA:
result += 'alphabetical characters'
elif self._match_value == ALPHANUMERIC:
result += 'alphanumeric characters'
elif self._match_value == NUMERIC:
result += 'numeric characters'
elif self._match_type == STRING_MATCH_REGEX:
result += 'characters, matching regex "' + self._match_value + '"'
if show_example:
result += ', such as ' + self._example_string
return result
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_STRING_MATCH ] = StringMatch

View File

@ -49,7 +49,7 @@ options = {}
# Misc
NETWORK_VERSION = 18
SOFTWARE_VERSION = 274
SOFTWARE_VERSION = 275
UNSCALED_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -230,7 +230,7 @@ def ConvertTimeDeltaToPrettyString( seconds ):
return 'per month'
if seconds > 60:
if seconds >= 60:
seconds = int( seconds )
@ -277,7 +277,14 @@ def ConvertTimeDeltaToPrettyString( seconds ):
minutes = seconds / 60
seconds = seconds % 60
result = '%d' % minutes + ' minutes'
if minutes == 1:
result = '1 minute'
else:
result = '%d' % minutes + ' minutes'
if seconds > 0:

View File

@ -52,6 +52,9 @@ SERIALISABLE_TYPE_NETWORK_SESSION_MANAGER = 46
SERIALISABLE_TYPE_NETWORK_CONTEXT = 47
SERIALISABLE_TYPE_NETWORK_LOGIN_MANAGER = 48
SERIALISABLE_TYPE_MEDIA_SORT = 49
SERIALISABLE_TYPE_URL_MATCH = 50
SERIALISABLE_TYPE_STRING_MATCH = 51
SERIALISABLE_TYPE_WATCHER_OPTIONS = 52
SERIALISABLE_TYPES_TO_OBJECT_TYPES = {}

151
include/TestClientData.py Normal file
View File

@ -0,0 +1,151 @@
import ClientData
import ClientImporting
import os
import unittest
class TestData( unittest.TestCase ):
def test_watcher_options( self ):
regular_watcher_options = ClientData.WatcherOptions( intended_files_per_check = 5, never_faster_than = 30, never_slower_than = 86400, death_file_velocity = ( 1, 86400 ) )
fast_watcher_options = ClientData.WatcherOptions( intended_files_per_check = 2, never_faster_than = 30, never_slower_than = 86400, death_file_velocity = ( 1, 86400 ) )
slow_watcher_options = ClientData.WatcherOptions( intended_files_per_check = 10, never_faster_than = 30, never_slower_than = 86400, death_file_velocity = ( 1, 86400 ) )
callous_watcher_options = ClientData.WatcherOptions( intended_files_per_check = 5, never_faster_than = 30, never_slower_than = 86400, death_file_velocity = ( 1, 60 ) )
empty_seed_cache = ClientImporting.SeedCache()
seed_cache = ClientImporting.SeedCache()
last_check_time = 10000000
one_day_before = last_check_time - 86400
for i in range( 50 ):
seed = os.urandom( 16 ).encode( 'hex' )
seed_cache.AddSeeds( ( seed, ) )
seed_cache.UpdateSeedSourceTime( seed, one_day_before - 10 )
for i in range( 50 ):
seed = os.urandom( 16 ).encode( 'hex' )
seed_cache.AddSeeds( ( seed, ) )
seed_cache.UpdateSeedSourceTime( seed, one_day_before + 10 )
bare_seed_cache = ClientImporting.SeedCache()
bare_seed_cache.AddSeeds( ( 'early', ) )
bare_seed_cache.AddSeeds( ( 'in_time_delta', ) )
bare_seed_cache.UpdateSeedSourceTime( 'early', one_day_before - 10 )
bare_seed_cache.UpdateSeedSourceTime( 'in_time_delta', one_day_before + 10 )
busy_seed_cache = ClientImporting.SeedCache()
busy_seed_cache.AddSeeds( ( 'early', ) )
busy_seed_cache.UpdateSeedSourceTime( 'early', one_day_before - 10 )
for i in range( 8640 ):
seed = os.urandom( 16 ).encode( 'hex' )
busy_seed_cache.AddSeeds( ( seed, ) )
busy_seed_cache.UpdateSeedSourceTime( seed, one_day_before + ( ( i + 1 ) * 10 ) - 1 )
new_thread_seed_cache = ClientImporting.SeedCache()
for i in range( 10 ):
seed = os.urandom( 16 ).encode( 'hex' )
new_thread_seed_cache.AddSeeds( ( seed, ) )
new_thread_seed_cache.UpdateSeedSourceTime( seed, last_check_time - 600 )
# empty
# should say ok if last_check_time is 0, so it can initialise
# otherwise sperg out safely
self.assertFalse( regular_watcher_options.IsDead( empty_seed_cache, 0 ) )
self.assertEqual( regular_watcher_options.GetPrettyCurrentVelocity( empty_seed_cache, 0 ), 'no files yet' )
self.assertEqual( regular_watcher_options.GetNextCheckTime( empty_seed_cache, 0 ), 0 )
self.assertTrue( regular_watcher_options.IsDead( empty_seed_cache, last_check_time ) )
self.assertEqual( regular_watcher_options.GetPrettyCurrentVelocity( empty_seed_cache, last_check_time ), 'no files, unable to determine velocity' )
# regular
# current velocity should be 50 files per day for the day ones and 0 files per min for the callous minute one
self.assertFalse( regular_watcher_options.IsDead( seed_cache, last_check_time ) )
self.assertFalse( fast_watcher_options.IsDead( seed_cache, last_check_time ) )
self.assertFalse( slow_watcher_options.IsDead( seed_cache, last_check_time ) )
self.assertTrue( callous_watcher_options.IsDead( seed_cache, last_check_time ) )
self.assertEqual( regular_watcher_options.GetPrettyCurrentVelocity( seed_cache, last_check_time ), u'at last check, found 50 files in previous 1 day' )
self.assertEqual( fast_watcher_options.GetPrettyCurrentVelocity( seed_cache, last_check_time ), u'at last check, found 50 files in previous 1 day' )
self.assertEqual( slow_watcher_options.GetPrettyCurrentVelocity( seed_cache, last_check_time ), u'at last check, found 50 files in previous 1 day' )
self.assertEqual( callous_watcher_options.GetPrettyCurrentVelocity( seed_cache, last_check_time ), u'at last check, found 0 files in previous 1 minute' )
self.assertEqual( regular_watcher_options.GetNextCheckTime( seed_cache, last_check_time ), last_check_time + 8640 )
self.assertEqual( fast_watcher_options.GetNextCheckTime( seed_cache, last_check_time ), last_check_time + 3456 )
self.assertEqual( slow_watcher_options.GetNextCheckTime( seed_cache, last_check_time ), last_check_time + 17280 )
# bare
# 1 files per day
self.assertFalse( regular_watcher_options.IsDead( bare_seed_cache, last_check_time ) )
self.assertTrue( callous_watcher_options.IsDead( bare_seed_cache, last_check_time ) )
self.assertEqual( regular_watcher_options.GetPrettyCurrentVelocity( bare_seed_cache, last_check_time ), u'at last check, found 1 files in previous 1 day' )
self.assertEqual( regular_watcher_options.GetNextCheckTime( bare_seed_cache, last_check_time ), last_check_time + 86400 )
self.assertEqual( fast_watcher_options.GetNextCheckTime( bare_seed_cache, last_check_time ), last_check_time + 86400 )
self.assertEqual( slow_watcher_options.GetNextCheckTime( bare_seed_cache, last_check_time ), last_check_time + 86400 )
# busy
# 8640 files per day, 6 files per minute
self.assertFalse( regular_watcher_options.IsDead( busy_seed_cache, last_check_time ) )
self.assertFalse( fast_watcher_options.IsDead( busy_seed_cache, last_check_time ) )
self.assertFalse( slow_watcher_options.IsDead( busy_seed_cache, last_check_time ) )
self.assertFalse( callous_watcher_options.IsDead( busy_seed_cache, last_check_time ) )
self.assertEqual( regular_watcher_options.GetPrettyCurrentVelocity( busy_seed_cache, last_check_time ), u'at last check, found 8,640 files in previous 1 day' )
self.assertEqual( callous_watcher_options.GetPrettyCurrentVelocity( busy_seed_cache, last_check_time ), u'at last check, found 6 files in previous 1 minute' )
self.assertEqual( regular_watcher_options.GetNextCheckTime( busy_seed_cache, last_check_time ), last_check_time + 50 )
self.assertEqual( fast_watcher_options.GetNextCheckTime( busy_seed_cache, last_check_time ), last_check_time + 30 )
self.assertEqual( slow_watcher_options.GetNextCheckTime( busy_seed_cache, last_check_time ), last_check_time + 100 )
self.assertEqual( callous_watcher_options.GetNextCheckTime( busy_seed_cache, last_check_time ), last_check_time + 50 )
# new thread
# only had files from ten mins ago, so timings are different
self.assertFalse( regular_watcher_options.IsDead( new_thread_seed_cache, last_check_time ) )
self.assertFalse( fast_watcher_options.IsDead( new_thread_seed_cache, last_check_time ) )
self.assertFalse( slow_watcher_options.IsDead( new_thread_seed_cache, last_check_time ) )
self.assertTrue( callous_watcher_options.IsDead( new_thread_seed_cache, last_check_time ) )
self.assertEqual( regular_watcher_options.GetPrettyCurrentVelocity( new_thread_seed_cache, last_check_time ), u'at last check, found 10 files in previous 10 minutes' )
self.assertEqual( fast_watcher_options.GetPrettyCurrentVelocity( new_thread_seed_cache, last_check_time ), u'at last check, found 10 files in previous 10 minutes' )
self.assertEqual( slow_watcher_options.GetPrettyCurrentVelocity( new_thread_seed_cache, last_check_time ), u'at last check, found 10 files in previous 10 minutes' )
self.assertEqual( callous_watcher_options.GetPrettyCurrentVelocity( new_thread_seed_cache, last_check_time ), u'at last check, found 0 files in previous 1 minute' )
self.assertEqual( regular_watcher_options.GetNextCheckTime( new_thread_seed_cache, last_check_time ), last_check_time + 300 )
self.assertEqual( fast_watcher_options.GetNextCheckTime( new_thread_seed_cache, last_check_time ), last_check_time + 120 )
self.assertEqual( slow_watcher_options.GetNextCheckTime( new_thread_seed_cache, last_check_time ), last_check_time + 600 )

View File

@ -17,6 +17,7 @@ from include import HydrusTags
from include import HydrusThreading
from include import TestClientConstants
from include import TestClientDaemons
from include import TestClientData
from include import TestClientListBoxes
from include import TestClientNetworking
from include import TestConstants
@ -312,6 +313,7 @@ class Controller( object ):
if run_all or only_run == 'data':
suites.append( unittest.TestLoader().loadTestsFromModule( TestClientConstants ) )
suites.append( unittest.TestLoader().loadTestsFromModule( TestClientData ) )
suites.append( unittest.TestLoader().loadTestsFromModule( TestFunctions ) )
suites.append( unittest.TestLoader().loadTestsFromModule( TestHydrusSerialisable ) )
suites.append( unittest.TestLoader().loadTestsFromModule( TestHydrusSessions ) )