2798 lines
107 KiB
Python
2798 lines
107 KiB
Python
import collections
|
|
import os
|
|
import threading
|
|
import time
|
|
import typing
|
|
|
|
from qtpy import QtCore as QC
|
|
from qtpy import QtWidgets as QW
|
|
from qtpy import QtGui as QG
|
|
|
|
from hydrus.core import HydrusConstants as HC
|
|
from hydrus.core import HydrusData
|
|
from hydrus.core import HydrusExceptions
|
|
from hydrus.core import HydrusGlobals as HG
|
|
from hydrus.core import HydrusSerialisable
|
|
from hydrus.core import HydrusText
|
|
|
|
from hydrus.client import ClientConstants as CC
|
|
from hydrus.client import ClientData
|
|
from hydrus.client import ClientPaths
|
|
from hydrus.client.gui import ClientGUIAsync
|
|
from hydrus.client.gui import ClientGUIDialogs
|
|
from hydrus.client.gui import ClientGUIDialogsQuick
|
|
from hydrus.client.gui import ClientGUIFunctions
|
|
from hydrus.client.gui import ClientGUIImport
|
|
from hydrus.client.gui import ClientGUIScrolledPanels
|
|
from hydrus.client.gui import ClientGUIFileSeedCache
|
|
from hydrus.client.gui import ClientGUIGallerySeedLog
|
|
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
|
|
from hydrus.client.gui import ClientGUITime
|
|
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
|
|
from hydrus.client.gui import QtPorting as QP
|
|
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
|
|
from hydrus.client.gui.lists import ClientGUIListCtrl
|
|
from hydrus.client.gui.widgets import ClientGUICommon
|
|
from hydrus.client.gui.widgets import ClientGUIMenuButton
|
|
from hydrus.client.importing import ClientImporting
|
|
from hydrus.client.importing import ClientImportSubscriptions
|
|
from hydrus.client.importing import ClientImportSubscriptionQuery
|
|
from hydrus.client.importing import ClientImportSubscriptionLegacy # keep this here so the serialisable stuff is registered, it has to be imported somewhere
|
|
|
|
def GetQueryHeadersQualityInfo( query_headers: typing.Iterable[ ClientImportSubscriptionQuery.SubscriptionQueryHeader ] ):
|
|
|
|
data = []
|
|
|
|
for query_header in query_headers:
|
|
|
|
try:
|
|
|
|
query_log_container = HG.client_controller.Read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SUBSCRIPTION_QUERY_LOG_CONTAINER, query_header.GetQueryLogContainerName() )
|
|
|
|
except HydrusExceptions.DataMissing:
|
|
|
|
continue
|
|
|
|
|
|
fsc = query_log_container.GetFileSeedCache()
|
|
|
|
hashes = fsc.GetHashes()
|
|
|
|
media_results = HG.client_controller.Read( 'media_results', hashes )
|
|
|
|
num_inbox = 0
|
|
num_archived = 0
|
|
num_deleted = 0
|
|
|
|
for media_result in media_results:
|
|
|
|
lm = media_result.GetLocationsManager()
|
|
|
|
if lm.IsLocal() and not lm.IsTrashed():
|
|
|
|
if media_result.GetInbox():
|
|
|
|
num_inbox += 1
|
|
|
|
else:
|
|
|
|
num_archived += 1
|
|
|
|
|
|
else:
|
|
|
|
num_deleted += 1
|
|
|
|
|
|
|
|
data.append( ( query_header.GetHumanName(), num_inbox, num_archived, num_deleted ) )
|
|
|
|
|
|
return data
|
|
|
|
def GetQueryLogContainers( query_headers: typing.Iterable[ ClientImportSubscriptionQuery.SubscriptionQueryHeader ] ):
|
|
|
|
query_log_containers = []
|
|
|
|
for query_header in query_headers:
|
|
|
|
try:
|
|
|
|
query_log_container = HG.client_controller.Read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SUBSCRIPTION_QUERY_LOG_CONTAINER, query_header.GetQueryLogContainerName() )
|
|
|
|
except HydrusExceptions.DBException as e:
|
|
|
|
if isinstance( e.db_e, HydrusExceptions.DataMissing ):
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
raise
|
|
|
|
|
|
|
|
query_log_containers.append( query_log_container )
|
|
|
|
|
|
return query_log_containers
|
|
|
|
class EditSubscriptionPanel( ClientGUIScrolledPanels.EditPanel ):
|
|
|
|
def __init__( self, parent: QW.QWidget, subscription: ClientImportSubscriptions.Subscription, names_to_edited_query_log_containers: typing.Mapping[ str, ClientImportSubscriptionQuery.SubscriptionQueryLogContainer ] ):
|
|
|
|
subscription = subscription.Duplicate()
|
|
|
|
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
|
|
|
self._original_subscription = subscription
|
|
self._names_to_edited_query_log_containers = dict( names_to_edited_query_log_containers )
|
|
|
|
#
|
|
|
|
menu_items = []
|
|
|
|
page_func = HydrusData.Call( ClientPaths.LaunchPathInWebBrowser, os.path.join( HC.HELP_DIR, 'getting_started_subscriptions.html' ) )
|
|
|
|
menu_items.append( ( 'normal', 'open the html subscriptions help', 'Open the help page for subscriptions in your web browser.', page_func ) )
|
|
|
|
help_button = ClientGUIMenuButton.MenuBitmapButton( self, CC.global_pixmaps().help, menu_items )
|
|
|
|
help_hbox = ClientGUICommon.WrapInText( help_button, self, 'help for this panel -->', QG.QColor( 0, 0, 255 ) )
|
|
|
|
#
|
|
|
|
self._name = QW.QLineEdit( self )
|
|
self._delay_st = ClientGUICommon.BetterStaticText( self )
|
|
|
|
#
|
|
|
|
( name, gug_key_and_name, query_headers, checker_options, initial_file_limit, periodic_file_limit, paused, file_import_options, tag_import_options, self._no_work_until, self._no_work_until_reason ) = subscription.ToTuple()
|
|
this_is_a_random_sample_sub = subscription.ThisIsARandomSampleSubscription()
|
|
|
|
self._query_panel = ClientGUICommon.StaticBox( self, 'site and queries' )
|
|
|
|
self._gug_key_and_name = ClientGUIImport.GUGKeyAndNameSelector( self._query_panel, gug_key_and_name )
|
|
|
|
queries_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._query_panel )
|
|
|
|
self._query_headers = ClientGUIListCtrl.BetterListCtrl( queries_panel, CGLC.COLUMN_LIST_SUBSCRIPTION_QUERIES.ID, 10, self._ConvertQueryHeaderToListCtrlTuples, use_simple_delete = True, activation_callback = self._EditQuery )
|
|
|
|
queries_panel.SetListCtrl( self._query_headers )
|
|
|
|
queries_panel.AddButton( 'add', self._AddQuery )
|
|
queries_panel.AddButton( 'copy queries', self._CopyQueries, enabled_only_on_selection = True )
|
|
queries_panel.AddButton( 'paste queries', self._PasteQueries )
|
|
queries_panel.AddButton( 'edit', self._EditQuery, enabled_only_on_selection = True )
|
|
queries_panel.AddDeleteButton()
|
|
queries_panel.AddSeparator()
|
|
queries_panel.AddButton( 'pause/play', self._PausePlay, enabled_only_on_selection = True )
|
|
queries_panel.AddButton( 'retry failed', self._STARTRetryFailed, enabled_check_func = self._ListCtrlCanRetryFailed )
|
|
queries_panel.AddButton( 'retry ignored', self._STARTRetryIgnored, enabled_check_func = self._ListCtrlCanRetryIgnored )
|
|
queries_panel.AddButton( 'check now', self._CheckNow, enabled_check_func = self._ListCtrlCanCheckNow )
|
|
queries_panel.AddButton( 'reset', self._STARTReset, enabled_check_func = self._ListCtrlCanResetCache )
|
|
|
|
if HG.client_controller.new_options.GetBoolean( 'advanced_mode' ):
|
|
|
|
queries_panel.AddSeparator()
|
|
|
|
menu_items = []
|
|
|
|
menu_items.append( ( 'normal', 'show', 'Show quality info.', self._STARTShowQualityInfo ) )
|
|
menu_items.append( ( 'normal', 'copy csv data to clipboard', 'Copy quality info to clipboard.', self._STARTCopyQualityInfo ) )
|
|
|
|
queries_panel.AddMenuButton( 'quality info', menu_items, enabled_only_on_selection = True )
|
|
|
|
|
|
#
|
|
|
|
self._file_limits_panel = ClientGUICommon.StaticBox( self, 'synchronisation' )
|
|
|
|
if HG.client_controller.new_options.GetBoolean( 'advanced_mode' ):
|
|
|
|
limits_max = 10000
|
|
|
|
else:
|
|
|
|
limits_max = 1000
|
|
|
|
|
|
self._initial_file_limit = QP.MakeQSpinBox( self._file_limits_panel, min=1, max=limits_max )
|
|
self._initial_file_limit.setToolTip( 'The first sync will add no more than this many URLs.' )
|
|
|
|
self._periodic_file_limit = QP.MakeQSpinBox( self._file_limits_panel, min=1, max=limits_max )
|
|
self._periodic_file_limit.setToolTip( 'Normal syncs will add no more than this many URLs, stopping early if they find several URLs the query has seen before.' )
|
|
|
|
self._this_is_a_random_sample_sub = QW.QCheckBox( self._file_limits_panel )
|
|
self._this_is_a_random_sample_sub.setToolTip( 'If you check this, you will not get warnings if the normal file limit is hit. Useful if you have a randomly sorted gallery, or you just want a recurring small sample of files.' )
|
|
|
|
self._checker_options = ClientGUIImport.CheckerOptionsButton( self._file_limits_panel, checker_options, update_callable = self._CheckerOptionsUpdated )
|
|
|
|
self._file_presentation_panel = ClientGUICommon.StaticBox( self, 'presentation' )
|
|
|
|
self._show_a_popup_while_working = QW.QCheckBox( self._file_presentation_panel )
|
|
self._show_a_popup_while_working.setToolTip( 'Careful with this! Leave it on to begin with, just in case it goes wrong!' )
|
|
|
|
self._publish_files_to_popup_button = QW.QCheckBox( self._file_presentation_panel )
|
|
self._publish_files_to_page = QW.QCheckBox( self._file_presentation_panel )
|
|
self._publish_label_override = ClientGUICommon.NoneableTextCtrl( self._file_presentation_panel, none_phrase = 'no, use subscription name' )
|
|
self._merge_query_publish_events = QW.QCheckBox( self._file_presentation_panel )
|
|
|
|
tt = 'This is great to merge multiple subs to a combined location!'
|
|
|
|
self._publish_label_override.setToolTip( tt )
|
|
|
|
tt = 'If unchecked, each query will produce its own \'subscription_name: query\' button or page.'
|
|
|
|
self._merge_query_publish_events.setToolTip( tt )
|
|
|
|
#
|
|
|
|
self._control_panel = ClientGUICommon.StaticBox( self, 'control' )
|
|
|
|
self._paused = QW.QCheckBox( self._control_panel )
|
|
|
|
#
|
|
|
|
show_downloader_options = True
|
|
|
|
self._file_import_options = ClientGUIImport.FileImportOptionsButton( self, file_import_options, show_downloader_options )
|
|
self._tag_import_options = ClientGUIImport.TagImportOptionsButton( self, tag_import_options, show_downloader_options, allow_default_selection = True )
|
|
|
|
#
|
|
|
|
self._name.setText( name )
|
|
|
|
self._query_headers.AddDatas( query_headers )
|
|
|
|
self._query_headers.Sort()
|
|
|
|
self._initial_file_limit.setValue( initial_file_limit )
|
|
self._periodic_file_limit.setValue( periodic_file_limit )
|
|
self._this_is_a_random_sample_sub.setChecked( this_is_a_random_sample_sub )
|
|
|
|
( show_a_popup_while_working, publish_files_to_popup_button, publish_files_to_page, publish_label_override, merge_query_publish_events ) = subscription.GetPresentationOptions()
|
|
|
|
self._show_a_popup_while_working.setChecked( show_a_popup_while_working )
|
|
self._publish_files_to_popup_button.setChecked( publish_files_to_popup_button )
|
|
self._publish_files_to_page.setChecked( publish_files_to_page )
|
|
self._publish_label_override.SetValue( publish_label_override )
|
|
self._merge_query_publish_events.setChecked( merge_query_publish_events )
|
|
|
|
self._paused.setChecked( paused )
|
|
|
|
#
|
|
|
|
self._query_panel.Add( self._gug_key_and_name, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._query_panel.Add( queries_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
#
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'on first check, get at most this many files: ', self._initial_file_limit ) )
|
|
rows.append( ( 'on normal checks, get at most this many newer files: ', self._periodic_file_limit ) )
|
|
rows.append( ( 'do not worry about subscription gaps: ', self._this_is_a_random_sample_sub ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( self._file_limits_panel, rows )
|
|
|
|
self._file_limits_panel.Add( ClientGUICommon.BetterStaticText( self._file_limits_panel, label = 'Don\'t change these values unless you know what you are doing!' ), CC.FLAGS_CENTER )
|
|
self._file_limits_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._file_limits_panel.Add( self._checker_options, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
#
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'show a popup while working: ', self._show_a_popup_while_working ) )
|
|
rows.append( ( 'publish new files to a popup button: ', self._publish_files_to_popup_button ) )
|
|
rows.append( ( 'publish new files to a page: ', self._publish_files_to_page ) )
|
|
rows.append( ( 'publish to a specific label: ', self._publish_label_override ) )
|
|
rows.append( ( 'publish all queries to the same page/popup button: ', self._merge_query_publish_events ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( self._file_presentation_panel, rows )
|
|
|
|
self._file_presentation_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
|
|
#
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'currently paused: ', self._paused ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( self._control_panel, rows )
|
|
|
|
self._control_panel.Add( gridbox, CC.FLAGS_ON_RIGHT )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, help_hbox, CC.FLAGS_ON_RIGHT )
|
|
QP.AddToLayout( vbox, ClientGUICommon.WrapInText(self._name,self,'name: '), CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._delay_st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._query_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( vbox, self._control_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._file_limits_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._file_presentation_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._file_import_options, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._tag_import_options, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
self._UpdateDelayText()
|
|
|
|
|
|
def _AddQuery( self ):
|
|
|
|
gug_key_and_name = self._gug_key_and_name.GetValue()
|
|
|
|
initial_search_text = HG.client_controller.network_engine.domain_manager.GetInitialSearchText( gug_key_and_name )
|
|
|
|
query_header = ClientImportSubscriptionQuery.SubscriptionQueryHeader()
|
|
|
|
query_header.SetQueryText( initial_search_text )
|
|
|
|
query_log_container = ClientImportSubscriptionQuery.SubscriptionQueryLogContainer( query_header.GetQueryLogContainerName() )
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit subscription query' ) as dlg:
|
|
|
|
panel = EditSubscriptionQueryPanel( dlg, query_header, query_log_container )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
( query_header, query_log_container ) = panel.GetValue()
|
|
|
|
query_text = query_header.GetQueryText()
|
|
|
|
if query_text in self._GetCurrentQueryTexts():
|
|
|
|
QW.QMessageBox.warning( self, 'Warning', 'You already have a query for "{}", so nothing new has been added.'.format( query_text ) )
|
|
|
|
return
|
|
|
|
|
|
self._query_headers.AddDatas( ( query_header, ) )
|
|
|
|
self._names_to_edited_query_log_containers[ query_log_container.GetName() ] = query_log_container
|
|
|
|
|
|
|
|
|
|
def _CheckerOptionsUpdated( self, checker_options ):
|
|
|
|
checker_options = self._checker_options.GetValue()
|
|
|
|
for query_header in self._query_headers.GetData():
|
|
|
|
query_log_container_name = query_header.GetQueryLogContainerName()
|
|
|
|
if query_log_container_name in self._names_to_edited_query_log_containers:
|
|
|
|
query_log_container = self._names_to_edited_query_log_containers[ query_log_container_name ]
|
|
|
|
query_header.SyncToQueryLogContainer( checker_options, query_log_container )
|
|
|
|
else:
|
|
|
|
query_header.SetQueryLogContainerStatus( ClientImportSubscriptionQuery.LOG_CONTAINER_UNSYNCED )
|
|
|
|
|
|
|
|
self._query_headers.UpdateDatas()
|
|
|
|
|
|
def _CheckNow( self ):
|
|
|
|
selected_queries = self._query_headers.GetData( only_selected = True )
|
|
|
|
for query_header in selected_queries:
|
|
|
|
query_header.CheckNow()
|
|
|
|
|
|
self._query_headers.UpdateDatas( selected_queries )
|
|
|
|
self._query_headers.Sort()
|
|
|
|
self._no_work_until = 0
|
|
|
|
self._UpdateDelayText()
|
|
|
|
|
|
def _ConvertQueryHeaderToListCtrlTuples( self, query_header: ClientImportSubscriptionQuery.SubscriptionQueryHeader ):
|
|
|
|
last_check_time = query_header.GetLastCheckTime()
|
|
next_check_time = query_header.GetNextCheckTime()
|
|
paused = query_header.IsPaused()
|
|
checker_status = query_header.GetCheckerStatus()
|
|
|
|
name = query_header.GetHumanName()
|
|
pretty_name = name
|
|
|
|
if paused:
|
|
|
|
pretty_paused = 'yes'
|
|
|
|
else:
|
|
|
|
pretty_paused = ''
|
|
|
|
|
|
if checker_status == ClientImporting.CHECKER_STATUS_OK:
|
|
|
|
pretty_status = 'ok'
|
|
|
|
else:
|
|
|
|
pretty_status = 'dead'
|
|
|
|
|
|
file_seed_cache_status = query_header.GetFileSeedCacheStatus()
|
|
|
|
latest_new_file_time = file_seed_cache_status.GetLatestAddedTime()
|
|
|
|
if latest_new_file_time is None or latest_new_file_time == 0:
|
|
|
|
pretty_latest_new_file_time = 'n/a'
|
|
|
|
else:
|
|
|
|
pretty_latest_new_file_time = ClientData.TimestampToPrettyTimeDelta( latest_new_file_time )
|
|
|
|
|
|
if last_check_time is None or last_check_time == 0:
|
|
|
|
pretty_last_check_time = '(initial check has not yet occurred)'
|
|
|
|
else:
|
|
|
|
pretty_last_check_time = ClientData.TimestampToPrettyTimeDelta( last_check_time )
|
|
|
|
|
|
pretty_next_check_time = query_header.GetNextCheckStatusString()
|
|
|
|
( file_velocity, pretty_file_velocity ) = query_header.GetFileVelocityInfo()
|
|
|
|
file_velocity = tuple( file_velocity ) # for sorting, list/tuple -> tuple
|
|
|
|
try:
|
|
|
|
estimate = query_header.GetBandwidthWaitingEstimate( HG.client_controller.network_engine.bandwidth_manager, self._original_subscription.GetName() )
|
|
|
|
if estimate == 0:
|
|
|
|
pretty_delay = ''
|
|
delay = 0
|
|
|
|
else:
|
|
|
|
pretty_delay = 'bandwidth: ' + HydrusData.TimeDeltaToPrettyTimeDelta( estimate )
|
|
delay = estimate
|
|
|
|
|
|
except:
|
|
|
|
pretty_delay = 'could not determine bandwidth--there may be a problem with some of the urls in this query'
|
|
delay = 0
|
|
|
|
|
|
( num_done, num_total ) = file_seed_cache_status.GetValueRange()
|
|
|
|
items = ( num_total, num_done )
|
|
|
|
pretty_items = file_seed_cache_status.GetStatusText( simple = True )
|
|
|
|
sort_latest_new_file_time = ClientGUIListCtrl.SafeNoneInt( latest_new_file_time )
|
|
sort_last_check_time = ClientGUIListCtrl.SafeNoneInt( last_check_time )
|
|
sort_next_check_time = ClientGUIListCtrl.SafeNoneInt( next_check_time )
|
|
|
|
display_tuple = ( pretty_name, pretty_paused, pretty_status, pretty_latest_new_file_time, pretty_last_check_time, pretty_next_check_time, pretty_file_velocity, pretty_delay, pretty_items )
|
|
sort_tuple = ( name, paused, checker_status, sort_latest_new_file_time, sort_last_check_time, sort_next_check_time, file_velocity, delay, items )
|
|
|
|
return ( display_tuple, sort_tuple )
|
|
|
|
|
|
def _CopyQueries( self ):
|
|
|
|
query_texts = []
|
|
|
|
for query_header in self._query_headers.GetData( only_selected = True ):
|
|
|
|
query_texts.append( query_header.GetQueryText() )
|
|
|
|
|
|
clipboard_text = os.linesep.join( query_texts )
|
|
|
|
if len( clipboard_text ) > 0:
|
|
|
|
HG.client_controller.pub( 'clipboard', 'text', clipboard_text )
|
|
|
|
|
|
|
|
def _DoAsyncGetQueryLogContainers( self, query_headers: typing.Collection[ ClientImportSubscriptionQuery.SubscriptionQueryHeader ], call: HydrusData.Call ):
|
|
|
|
missing_query_headers = [ query_header for query_header in query_headers if query_header.GetQueryLogContainerName() not in self._names_to_edited_query_log_containers ]
|
|
|
|
if len( missing_query_headers ) > 0:
|
|
|
|
self.setEnabled( False )
|
|
|
|
def work_callable():
|
|
|
|
result = GetQueryLogContainers( missing_query_headers )
|
|
|
|
return result
|
|
|
|
|
|
def publish_callable( result ):
|
|
|
|
query_log_containers = result
|
|
|
|
for query_log_container in query_log_containers:
|
|
|
|
self._names_to_edited_query_log_containers[ query_log_container.GetName() ] = query_log_container
|
|
|
|
|
|
self.setEnabled( True )
|
|
|
|
call()
|
|
|
|
|
|
async_call = ClientGUIAsync.AsyncQtJob( self, work_callable, publish_callable )
|
|
|
|
async_call.start()
|
|
|
|
else:
|
|
|
|
call()
|
|
|
|
|
|
|
|
def _EditQuery( self ):
|
|
|
|
selected_query_headers = self._query_headers.GetData( only_selected = True )
|
|
|
|
for old_query_header in selected_query_headers:
|
|
|
|
query_log_container_name = old_query_header.GetQueryLogContainerName()
|
|
|
|
if query_log_container_name not in self._names_to_edited_query_log_containers:
|
|
|
|
try:
|
|
|
|
old_query_log_container = HG.client_controller.Read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SUBSCRIPTION_QUERY_LOG_CONTAINER, query_log_container_name )
|
|
|
|
except HydrusExceptions.DataMissing:
|
|
|
|
QW.QMessageBox.critical( self, 'Error', 'Some data for this query, "{}" was missing! This should have been dealt with when the dialog launched, so something is very wrong! Please exit the manage subscriptions dialog immediately, pause your subs, and contact hydrus dev!' )
|
|
|
|
return
|
|
|
|
|
|
self._names_to_edited_query_log_containers[ query_log_container_name ] = old_query_log_container
|
|
|
|
|
|
old_query_log_container = self._names_to_edited_query_log_containers[ query_log_container_name ]
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit subscription query' ) as dlg:
|
|
|
|
panel = EditSubscriptionQueryPanel( dlg, old_query_header, old_query_log_container )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
( edited_query_header, edited_query_log_container ) = panel.GetValue()
|
|
|
|
edited_query_header.SyncToQueryLogContainer( self._checker_options.GetValue(), edited_query_log_container )
|
|
|
|
edited_query_text = edited_query_header.GetQueryText()
|
|
|
|
if edited_query_text != old_query_header.GetQueryText() and edited_query_text in self._GetCurrentQueryTexts():
|
|
|
|
QW.QMessageBox.warning( self, 'Warning', 'You already have a query for "'+edited_query_text+'"! The edit you just made will not be saved.' )
|
|
|
|
break
|
|
|
|
|
|
self._query_headers.DeleteDatas( ( old_query_header, ) )
|
|
|
|
self._query_headers.AddDatas( ( edited_query_header, ) )
|
|
|
|
self._names_to_edited_query_log_containers[ query_log_container_name ] = edited_query_log_container
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
self._query_headers.Sort()
|
|
|
|
|
|
def _GetCurrentQueryTexts( self ):
|
|
|
|
query_strings = set()
|
|
|
|
for query_header in self._query_headers.GetData():
|
|
|
|
query_strings.add( query_header.GetQueryText() )
|
|
|
|
|
|
return query_strings
|
|
|
|
|
|
def _STARTCopyQualityInfo( self ):
|
|
|
|
self.setEnabled( False )
|
|
|
|
query_headers = self._query_headers.GetData( only_selected = True )
|
|
|
|
def work_callable():
|
|
|
|
result = GetQueryHeadersQualityInfo( query_headers )
|
|
|
|
return result
|
|
|
|
|
|
def publish_callable( result ):
|
|
|
|
self.setEnabled( True )
|
|
|
|
self._CopyQualityInfo( result )
|
|
|
|
|
|
async_call = ClientGUIAsync.AsyncQtJob( self, work_callable, publish_callable )
|
|
|
|
async_call.start()
|
|
|
|
|
|
def _CopyQualityInfo( self, data ):
|
|
|
|
data_strings = []
|
|
|
|
for ( name, num_inbox, num_archived, num_deleted ) in data:
|
|
|
|
if num_archived + num_deleted > 0:
|
|
|
|
percent = HydrusData.ConvertFloatToPercentage( num_archived / ( num_archived + num_deleted ) )
|
|
|
|
else:
|
|
|
|
percent = '0.0%'
|
|
|
|
|
|
data_string = '{},{},{},{},{}'.format( name, num_inbox, num_archived, num_deleted, percent )
|
|
|
|
data_strings.append( data_string )
|
|
|
|
|
|
text = os.linesep.join( data_strings )
|
|
|
|
HG.client_controller.pub( 'clipboard', 'text', text )
|
|
|
|
|
|
def _STARTShowQualityInfo( self ):
|
|
|
|
self.setEnabled( False )
|
|
|
|
query_headers = self._query_headers.GetData( only_selected = True )
|
|
|
|
def work_callable():
|
|
|
|
result = GetQueryHeadersQualityInfo( query_headers )
|
|
|
|
return result
|
|
|
|
|
|
def publish_callable( result ):
|
|
|
|
self.setEnabled( True )
|
|
|
|
self._ShowQualityInfo( result )
|
|
|
|
|
|
async_call = ClientGUIAsync.AsyncQtJob( self, work_callable, publish_callable )
|
|
|
|
async_call.start()
|
|
|
|
|
|
def _ShowQualityInfo( self, data ):
|
|
|
|
data_strings = []
|
|
|
|
for ( name, num_inbox, num_archived, num_deleted ) in data:
|
|
|
|
data_string = '{}: inbox {} | archive {} | deleted {}'.format( name, HydrusData.ToHumanInt( num_inbox ), HydrusData.ToHumanInt( num_archived ), HydrusData.ToHumanInt( num_deleted ) )
|
|
|
|
if num_archived + num_deleted > 0:
|
|
|
|
data_string += ' | good {}'.format( HydrusData.ConvertFloatToPercentage( num_archived / ( num_archived + num_deleted ) ) )
|
|
|
|
|
|
data_strings.append( data_string )
|
|
|
|
|
|
message = os.linesep.join( data_strings )
|
|
|
|
QW.QMessageBox.information( self, 'Information', message )
|
|
|
|
|
|
def _ListCtrlCanCheckNow( self ):
|
|
|
|
for query_header in self._query_headers.GetData( only_selected = True ):
|
|
|
|
if query_header.CanCheckNow():
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
def _ListCtrlCanResetCache( self ):
|
|
|
|
for query_header in self._query_headers.GetData( only_selected = True ):
|
|
|
|
if not query_header.IsInitialSync():
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
def _ListCtrlCanRetryFailed( self ):
|
|
|
|
for query_header in self._query_headers.GetData( only_selected = True ):
|
|
|
|
if query_header.CanRetryFailed():
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
def _ListCtrlCanRetryIgnored( self ):
|
|
|
|
for query_header in self._query_headers.GetData( only_selected = True ):
|
|
|
|
if query_header.CanRetryIgnored():
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
def _PasteQueries( self ):
|
|
|
|
message = 'This will add new queries by pulling them from your clipboard. It assumes they are currently in your clipboard and newline separated. Queries that are already in the subscription (with any combination of upper/lower case) will not be re-added. Is that ok?'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
pasted_text = HG.client_controller.GetClipboardText()
|
|
|
|
except HydrusExceptions.DataMissing as e:
|
|
|
|
QW.QMessageBox.critical( self, 'Error', str(e) )
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
pasted_query_texts = HydrusText.DeserialiseNewlinedTexts( pasted_text )
|
|
|
|
except:
|
|
|
|
QW.QMessageBox.critical( self, 'Error', 'I could not understand what was in the clipboard!' )
|
|
|
|
return
|
|
|
|
|
|
if len( pasted_query_texts ) == 0:
|
|
|
|
QW.QMessageBox.warning( self, 'Empty Paste?', 'The clipboard did not seem to have anything in it!' )
|
|
|
|
return
|
|
|
|
|
|
current_query_texts_lower = { query_text.lower() for query_text in self._GetCurrentQueryTexts() }
|
|
|
|
already_existing_query_texts = set()
|
|
new_query_texts = set()
|
|
|
|
for query_text in pasted_query_texts:
|
|
|
|
query_text_lower = query_text.lower()
|
|
|
|
if query_text_lower in current_query_texts_lower:
|
|
|
|
already_existing_query_texts.add( query_text )
|
|
|
|
else:
|
|
|
|
new_query_texts.add( query_text )
|
|
|
|
|
|
|
|
already_existing_query_texts = sorted( already_existing_query_texts )
|
|
new_query_texts = sorted( new_query_texts )
|
|
|
|
message = ''
|
|
|
|
if len( already_existing_query_texts ) > 0:
|
|
|
|
if len( already_existing_query_texts ) > 50:
|
|
|
|
message += '{} queries were already in the subscription.'.format( HydrusData.ToHumanInt( len( already_existing_query_texts ) ) )
|
|
|
|
else:
|
|
|
|
if len( already_existing_query_texts ) > 5:
|
|
|
|
aeqt_separator = ', '
|
|
|
|
else:
|
|
|
|
aeqt_separator = os.linesep
|
|
|
|
|
|
message += 'The queries:'
|
|
message += os.linesep * 2
|
|
message += aeqt_separator.join( already_existing_query_texts )
|
|
message += os.linesep * 2
|
|
message += 'Were already in the subscription.'
|
|
|
|
|
|
|
|
if len( new_query_texts ) > 0:
|
|
|
|
if len( already_existing_query_texts ) > 0:
|
|
|
|
message += os.linesep * 2
|
|
|
|
|
|
if len( new_query_texts ) > 50:
|
|
|
|
message += '{} queries were added.'.format( HydrusData.ToHumanInt( len( new_query_texts ) ) )
|
|
|
|
else:
|
|
|
|
if len( new_query_texts ) > 5:
|
|
|
|
nqt_separator = ', '
|
|
|
|
else:
|
|
|
|
nqt_separator = os.linesep
|
|
|
|
|
|
message += 'The queries:'
|
|
message += os.linesep * 2
|
|
message += nqt_separator.join( new_query_texts )
|
|
message += os.linesep * 2
|
|
message += 'Were added.'
|
|
|
|
|
|
|
|
if len( message ) > 0:
|
|
|
|
QW.QMessageBox.information( self, 'Information', message )
|
|
|
|
|
|
query_headers = []
|
|
|
|
for query_text in new_query_texts:
|
|
|
|
query_header = ClientImportSubscriptionQuery.SubscriptionQueryHeader()
|
|
|
|
query_header.SetQueryText( query_text )
|
|
|
|
query_headers.append( query_header )
|
|
|
|
query_log_container_name = query_header.GetQueryLogContainerName()
|
|
|
|
query_log_container = ClientImportSubscriptionQuery.SubscriptionQueryLogContainer( query_log_container_name )
|
|
|
|
self._names_to_edited_query_log_containers[ query_log_container_name ] = query_log_container
|
|
|
|
|
|
self._query_headers.AddDatas( query_headers )
|
|
|
|
|
|
|
|
def _PausePlay( self ):
|
|
|
|
selected_query_headers = self._query_headers.GetData( only_selected = True )
|
|
|
|
for query_header in selected_query_headers:
|
|
|
|
query_header.PausePlay()
|
|
|
|
|
|
self._query_headers.UpdateDatas( selected_query_headers )
|
|
|
|
|
|
def _STARTReset( self ):
|
|
|
|
message = 'Resetting these queries will delete all their cached urls, meaning when the subscription next runs, they will have to download all those links over again. This may be expensive in time and data. Only do this if you know what it means. Do you want to do it?'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
selected_query_headers = self._query_headers.GetData( only_selected = True )
|
|
|
|
call = HydrusData.Call( self._Reset, selected_query_headers )
|
|
|
|
self._DoAsyncGetQueryLogContainers( selected_query_headers, call )
|
|
|
|
|
|
|
|
def _Reset( self, query_headers ):
|
|
|
|
for query_header in query_headers:
|
|
|
|
query_log_container_name = query_header.GetQueryLogContainerName()
|
|
|
|
if query_log_container_name not in self._names_to_edited_query_log_containers:
|
|
|
|
continue
|
|
|
|
|
|
query_log_container = self._names_to_edited_query_log_containers[ query_log_container_name ]
|
|
|
|
query_header.Reset( query_log_container )
|
|
|
|
|
|
self._query_headers.UpdateDatas( query_headers )
|
|
|
|
|
|
def _STARTRetryFailed( self ):
|
|
|
|
selected_query_headers = self._query_headers.GetData( only_selected = True )
|
|
|
|
query_headers = [ query_header for query_header in selected_query_headers if query_header.CanRetryFailed() ]
|
|
|
|
call = HydrusData.Call( self._RetryFailed, query_headers )
|
|
|
|
self._DoAsyncGetQueryLogContainers( query_headers, call )
|
|
|
|
|
|
def _RetryFailed( self, query_headers: typing.Collection[ ClientImportSubscriptionQuery.SubscriptionQueryHeader ] ):
|
|
|
|
for query_header in query_headers:
|
|
|
|
query_log_container_name = query_header.GetQueryLogContainerName()
|
|
|
|
if query_log_container_name not in self._names_to_edited_query_log_containers:
|
|
|
|
continue
|
|
|
|
|
|
query_log_container = self._names_to_edited_query_log_containers[ query_log_container_name ]
|
|
|
|
query_log_container.GetFileSeedCache().RetryFailed()
|
|
|
|
query_header.UpdateFileStatus( query_log_container )
|
|
|
|
|
|
self._query_headers.UpdateDatas( query_headers )
|
|
|
|
self._no_work_until = 0
|
|
|
|
self._UpdateDelayText()
|
|
|
|
|
|
def _STARTRetryIgnored( self ):
|
|
|
|
try:
|
|
|
|
ignored_regex = ClientGUIFileSeedCache.GetRetryIgnoredParam( self )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
selected_query_headers = self._query_headers.GetData( only_selected = True )
|
|
|
|
query_headers = [ query_header for query_header in selected_query_headers if query_header.CanRetryIgnored() ]
|
|
|
|
call = HydrusData.Call( self._RetryIgnored, query_headers, ignored_regex )
|
|
|
|
self._DoAsyncGetQueryLogContainers( query_headers, call )
|
|
|
|
|
|
def _RetryIgnored( self, query_headers: typing.Collection[ ClientImportSubscriptionQuery.SubscriptionQueryHeader ], ignored_regex = typing.Optional[ str ] ):
|
|
|
|
for query_header in query_headers:
|
|
|
|
query_log_container_name = query_header.GetQueryLogContainerName()
|
|
|
|
if query_log_container_name not in self._names_to_edited_query_log_containers:
|
|
|
|
continue
|
|
|
|
|
|
query_log_container = self._names_to_edited_query_log_containers[ query_log_container_name ]
|
|
|
|
query_log_container.GetFileSeedCache().RetryIgnored( ignored_regex = ignored_regex )
|
|
|
|
query_header.UpdateFileStatus( query_log_container )
|
|
|
|
|
|
self._query_headers.UpdateDatas( query_headers )
|
|
|
|
self._no_work_until = 0
|
|
|
|
self._UpdateDelayText()
|
|
|
|
|
|
def _UpdateDelayText( self ):
|
|
|
|
if HydrusData.TimeHasPassed( self._no_work_until ):
|
|
|
|
status = 'no recent errors'
|
|
|
|
else:
|
|
|
|
status = 'delayed--retrying ' + ClientData.TimestampToPrettyTimeDelta( self._no_work_until, just_now_threshold = 0 ) + ' because: ' + self._no_work_until_reason
|
|
|
|
|
|
self._delay_st.setText( status )
|
|
|
|
|
|
def GetValue( self ) -> ClientImportSubscriptions.Subscription:
|
|
|
|
if not self.isEnabled():
|
|
|
|
raise HydrusExceptions.VetoException( 'It looks like a long-running job is still working. Please wait for it to finish before applying this dialog.' )
|
|
|
|
|
|
name = self._name.text()
|
|
|
|
subscription = ClientImportSubscriptions.Subscription( name )
|
|
|
|
gug_key_and_name = self._gug_key_and_name.GetValue()
|
|
|
|
initial_file_limit = self._initial_file_limit.value()
|
|
periodic_file_limit = self._periodic_file_limit.value()
|
|
|
|
paused = self._paused.isChecked()
|
|
|
|
checker_options = self._checker_options.GetValue()
|
|
file_import_options = self._file_import_options.GetValue()
|
|
tag_import_options = self._tag_import_options.GetValue()
|
|
|
|
subscription.SetTuple( gug_key_and_name, checker_options, initial_file_limit, periodic_file_limit, paused, file_import_options, tag_import_options, self._no_work_until )
|
|
|
|
subscription.SetThisIsARandomSampleSubscription( self._this_is_a_random_sample_sub.isChecked() )
|
|
|
|
subscription.SetQueryHeaders( self._query_headers.GetData() )
|
|
|
|
show_a_popup_while_working = self._show_a_popup_while_working.isChecked()
|
|
publish_files_to_popup_button = self._publish_files_to_popup_button.isChecked()
|
|
publish_files_to_page = self._publish_files_to_page.isChecked()
|
|
publish_label_override = self._publish_label_override.GetValue()
|
|
merge_query_publish_events = self._merge_query_publish_events.isChecked()
|
|
|
|
subscription.SetPresentationOptions( show_a_popup_while_working, publish_files_to_popup_button, publish_files_to_page, publish_label_override, merge_query_publish_events )
|
|
|
|
return ( subscription, self._names_to_edited_query_log_containers )
|
|
|
|
|
|
class EditSubscriptionQueryPanel( ClientGUIScrolledPanels.EditPanel ):
|
|
|
|
def __init__( self, parent: QW.QWidget, query_header: ClientImportSubscriptionQuery.SubscriptionQueryHeader, query_log_container: ClientImportSubscriptionQuery.SubscriptionQueryLogContainer ):
|
|
|
|
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
|
|
|
self._original_query_header = query_header
|
|
self._original_query_log_container = query_log_container
|
|
|
|
query_header = query_header.Duplicate()
|
|
query_log_container = query_log_container.Duplicate()
|
|
|
|
self._status_st = ClientGUICommon.BetterStaticText( self )
|
|
|
|
st_width = ClientGUIFunctions.ConvertTextToPixelWidth( self._status_st, 50 )
|
|
|
|
self._status_st.setMinimumWidth( st_width )
|
|
|
|
self._display_name = ClientGUICommon.NoneableTextCtrl( self, none_phrase = 'show query text' )
|
|
self._query_text = QW.QLineEdit( self )
|
|
self._check_now = QW.QCheckBox( self )
|
|
self._paused = QW.QCheckBox( self )
|
|
|
|
self._file_seed_cache_control = ClientGUIFileSeedCache.FileSeedCacheStatusControl( self, HG.client_controller )
|
|
|
|
self._gallery_seed_log_control = ClientGUIGallerySeedLog.GallerySeedLogStatusControl( self, HG.client_controller, True, True, 'search' )
|
|
|
|
tag_import_options = query_header.GetTagImportOptions()
|
|
show_downloader_options = False # just for additional tags, no parsing gubbins needed
|
|
|
|
self._tag_import_options = ClientGUIImport.TagImportOptionsButton( self, tag_import_options, show_downloader_options )
|
|
|
|
#
|
|
|
|
display_name = query_header.GetDisplayName()
|
|
query_text = query_header.GetQueryText()
|
|
check_now = query_header.IsCheckingNow()
|
|
paused = query_header.IsPaused()
|
|
|
|
self._display_name.SetValue( display_name )
|
|
|
|
self._query_text.setText( query_text )
|
|
|
|
self._check_now.setChecked( check_now )
|
|
|
|
self._paused.setChecked( paused )
|
|
|
|
self._file_seed_cache = query_log_container.GetFileSeedCache()
|
|
|
|
self._file_seed_cache_control.SetFileSeedCache( self._file_seed_cache )
|
|
|
|
self._gallery_seed_log = query_log_container.GetGallerySeedLog()
|
|
|
|
self._gallery_seed_log_control.SetGallerySeedLog( self._gallery_seed_log )
|
|
|
|
#
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'optional display name: ', self._display_name ) )
|
|
rows.append( ( 'query text: ', self._query_text ) )
|
|
rows.append( ( 'check now: ', self._check_now ) )
|
|
rows.append( ( 'paused: ', self._paused ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( self, rows )
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._status_st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._file_seed_cache_control, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._gallery_seed_log_control, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._tag_import_options, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
#
|
|
|
|
self._check_now.clicked.connect( self._UpdateStatus )
|
|
self._paused.clicked.connect( self._UpdateStatus )
|
|
|
|
self._UpdateStatus()
|
|
|
|
self._query_text.selectAll()
|
|
|
|
ClientGUIFunctions.SetFocusLater( self._query_text )
|
|
|
|
|
|
def _GetValue( self ) -> typing.Tuple[ ClientImportSubscriptionQuery.SubscriptionQueryHeader, ClientImportSubscriptionQuery.SubscriptionQueryLogContainer ]:
|
|
|
|
query_header = self._original_query_header.Duplicate()
|
|
|
|
query_header.SetQueryText( self._query_text.text() )
|
|
|
|
query_header.SetPaused( self._paused.isChecked() )
|
|
|
|
query_header.SetCheckNow( self._check_now.isChecked() )
|
|
|
|
query_header.SetDisplayName( self._display_name.GetValue() )
|
|
|
|
query_header.SetTagImportOptions( self._tag_import_options.GetValue() )
|
|
|
|
query_log_container = self._original_query_log_container.Duplicate()
|
|
|
|
query_log_container.SetFileSeedCache( self._file_seed_cache )
|
|
query_log_container.SetGallerySeedLog( self._gallery_seed_log )
|
|
|
|
return ( query_header, query_log_container )
|
|
|
|
|
|
def _UpdateStatus( self ):
|
|
|
|
( query_header, query_log_container ) = self._GetValue()
|
|
|
|
self._status_st.setText( 'next check: {}'.format( query_header.GetNextCheckStatusString() ) )
|
|
|
|
|
|
def GetValue( self ) -> typing.Tuple[ ClientImportSubscriptionQuery.SubscriptionQueryHeader, ClientImportSubscriptionQuery.SubscriptionQueryLogContainer ]:
|
|
|
|
return self._GetValue()
|
|
|
|
|
|
class EditSubscriptionsPanel( ClientGUIScrolledPanels.EditPanel ):
|
|
|
|
def __init__( self, parent: QW.QWidget, subscriptions: typing.Collection[ ClientImportSubscriptions.Subscription ], subs_are_globally_paused: bool = False ):
|
|
|
|
subscriptions = [ subscription.Duplicate() for subscription in subscriptions ]
|
|
|
|
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
|
|
|
self._existing_query_log_container_names = set()
|
|
|
|
for subscription in subscriptions:
|
|
|
|
self._existing_query_log_container_names.update( subscription.GetAllQueryLogContainerNames() )
|
|
|
|
|
|
self._names_to_edited_query_log_containers = {}
|
|
|
|
self._gug_names_to_dupe_cased_query_texts = {}
|
|
self._subscriptions_to_dupe_cased_query_texts = {}
|
|
|
|
self._gug_names_to_dupe_caseless_query_texts = {}
|
|
self._subscriptions_to_dupe_caseless_query_texts = {}
|
|
|
|
#
|
|
|
|
menu_items = []
|
|
|
|
page_func = HydrusData.Call( ClientPaths.LaunchPathInWebBrowser, os.path.join( HC.HELP_DIR, 'getting_started_subscriptions.html' ) )
|
|
|
|
menu_items.append( ( 'normal', 'open the html subscriptions help', 'Open the help page for subscriptions in your web browser.', page_func ) )
|
|
|
|
help_button = ClientGUIMenuButton.MenuBitmapButton( self, CC.global_pixmaps().help, menu_items )
|
|
|
|
help_hbox = ClientGUICommon.WrapInText( help_button, self, 'help for this panel -->', QG.QColor( 0, 0, 255 ) )
|
|
|
|
self._subscriptions_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
|
|
|
|
self._subscriptions = ClientGUIListCtrl.BetterListCtrl( self._subscriptions_panel, CGLC.COLUMN_LIST_SUBSCRIPTIONS.ID, 12, self._ConvertSubscriptionToListCtrlTuples, use_simple_delete = True, activation_callback = self.Edit )
|
|
|
|
self._subscriptions_panel.SetListCtrl( self._subscriptions )
|
|
|
|
self._subscriptions_panel.AddButton( 'add', self.Add )
|
|
self._subscriptions_panel.AddButton( 'edit', self.Edit, enabled_only_on_selection = True )
|
|
self._subscriptions_panel.AddDeleteButton()
|
|
|
|
self._subscriptions_panel.AddSeparator()
|
|
|
|
self._subscriptions_panel.AddImportExportButtons( ( ClientImportSubscriptionLegacy.SubscriptionLegacy, ClientImportSubscriptions.SubscriptionContainer ), self._AddSubscription, custom_get_callable = self._GetSelectedSubsAsExportableContainers )
|
|
|
|
self._subscriptions_panel.NewButtonRow()
|
|
|
|
self._subscriptions_panel.AddButton( 'merge', self.Merge, enabled_check_func = self._CanMerge )
|
|
self._subscriptions_panel.AddButton( 'separate', self.Separate, enabled_check_func = self._CanSeparate )
|
|
self._subscriptions_panel.AddButton( 'deduplicate', self.DedupeAll, enabled_check_func = self._CanDedupeAll )
|
|
self._subscriptions_panel.AddButton( 'lowercase', self.LowerCaseQueries, enabled_check_func = self._CanLowerCaseQueries )
|
|
|
|
self._subscriptions_panel.AddSeparator()
|
|
|
|
self._subscriptions_panel.AddButton( 'pause/resume', self.PauseResume, enabled_only_on_selection = True )
|
|
self._subscriptions_panel.AddButton( 'retry failed', self._STARTRetryFailed, enabled_check_func = self._CanRetryFailed )
|
|
self._subscriptions_panel.AddButton( 'retry ignored', self._STARTRetryIgnored, enabled_check_func = self._CanRetryIgnored )
|
|
self._subscriptions_panel.AddButton( 'scrub delays', self.ScrubDelays, enabled_check_func = self._CanScrubDelays )
|
|
self._subscriptions_panel.AddButton( 'check queries now', self.CheckNow, enabled_check_func = self._CanCheckNow )
|
|
|
|
self._subscriptions_panel.AddButton( 'reset', self._STARTReset, enabled_check_func = self._CanReset )
|
|
|
|
self._subscriptions_panel.NewButtonRow()
|
|
|
|
self._subscriptions_panel.AddButton( 'select subscriptions', self.SelectSubscriptions )
|
|
self._subscriptions_panel.AddButton( 'overwrite checker timings', self.SetCheckerOptions, enabled_only_on_selection = True )
|
|
self._subscriptions_panel.AddButton( 'overwrite tag import options', self.SetTagImportOptions, enabled_only_on_selection = True )
|
|
|
|
#
|
|
|
|
self._subscriptions.AddDatas( subscriptions )
|
|
|
|
self._subscriptions.Sort()
|
|
|
|
self._RegenDupeData()
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, help_hbox, CC.FLAGS_ON_RIGHT )
|
|
|
|
if subs_are_globally_paused:
|
|
|
|
message = 'SUBSCRIPTIONS ARE CURRENTLY GLOBALLY PAUSED! CHECK THE NETWORK MENU TO UNPAUSE THEM.'
|
|
|
|
st = ClientGUICommon.BetterStaticText( self, message )
|
|
st.setObjectName( 'HydrusWarning' )
|
|
|
|
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
|
|
QP.AddToLayout( vbox, self._subscriptions_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
|
|
def _AddSubscription( self, unknown_subscription ):
|
|
|
|
if isinstance( unknown_subscription, ( ClientImportSubscriptionLegacy.SubscriptionLegacy, ClientImportSubscriptions.SubscriptionContainer ) ):
|
|
|
|
if isinstance( unknown_subscription, ClientImportSubscriptionLegacy.SubscriptionLegacy ):
|
|
|
|
( subscription, query_log_containers ) = ClientImportSubscriptionLegacy.ConvertLegacySubscriptionToNew( unknown_subscription )
|
|
|
|
elif isinstance( unknown_subscription, ClientImportSubscriptions.SubscriptionContainer ):
|
|
|
|
subscription = unknown_subscription.subscription
|
|
query_log_containers = unknown_subscription.query_log_containers
|
|
|
|
|
|
old_names_to_query_log_containers = { query_log_container.GetName() : query_log_container for query_log_container in query_log_containers }
|
|
|
|
there_were_missing_query_log_containers = False
|
|
|
|
for query_header in subscription.GetQueryHeaders():
|
|
|
|
old_query_log_container_name = query_header.GetQueryLogContainerName()
|
|
|
|
new_query_log_container_name = ClientImportSubscriptionQuery.GenerateQueryLogContainerName()
|
|
|
|
query_header.SetQueryLogContainerName( new_query_log_container_name )
|
|
|
|
if old_query_log_container_name in old_names_to_query_log_containers:
|
|
|
|
old_names_to_query_log_containers[ old_query_log_container_name ].SetName( new_query_log_container_name )
|
|
|
|
else:
|
|
|
|
there_were_missing_query_log_containers = True
|
|
|
|
|
|
|
|
if there_were_missing_query_log_containers:
|
|
|
|
message = 'When importing this subscription, "{}", there was missing log data! I will still let you add it, but some of its queries are incomplete. If you are ok with this, ok and then immediately re-open the manage subscriptions dialog to reinitialise the missing data back to zero (and clear any orphaned data that came with this). If you are not ok with this, cancel out now or cancel out of the whole manage subs dialog.'.format( subscription.GetName() )
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'missing query log data!', yes_label = 'import it anyway', no_label = 'back out now' )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
|
|
new_names_to_query_log_containers = { query_log_container.GetName() : query_log_container for query_log_container in query_log_containers }
|
|
|
|
self._names_to_edited_query_log_containers.update( new_names_to_query_log_containers )
|
|
|
|
elif isinstance( unknown_subscription, ClientImportSubscriptions.Subscription ):
|
|
|
|
subscription = unknown_subscription
|
|
|
|
|
|
subscription.SetNonDupeName( self._GetExistingNames() )
|
|
|
|
self._subscriptions.AddDatas( ( subscription, ) )
|
|
|
|
self._RegenDupeData()
|
|
|
|
|
|
def _CanCheckNow( self ):
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
return True in ( subscription.CanCheckNow() for subscription in subscriptions )
|
|
|
|
|
|
def _CanDedupeAll( self ):
|
|
|
|
return len( self._subscriptions_to_dupe_cased_query_texts ) > 0 or len( self._subscriptions_to_dupe_caseless_query_texts ) > 0
|
|
|
|
|
|
def _CanLowerCaseQueries( self ):
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
return True in ( subscription.CanLowerCaseQueries() for subscription in subscriptions )
|
|
|
|
|
|
def _CanMerge( self ):
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
# only subs with queries can be merged
|
|
|
|
mergeable_subscriptions = [ subscription for subscription in subscriptions if len( subscription.GetQueryHeaders() ) > 0 ]
|
|
|
|
unique_gug_names = { subscription.GetGUGKeyAndName()[1] for subscription in mergeable_subscriptions }
|
|
|
|
# if there are fewer, there must be dupes, so we must be able to merge
|
|
|
|
return len( unique_gug_names ) < len( subscriptions )
|
|
|
|
|
|
def _CanReset( self ):
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
return True in ( subscription.CanReset() for subscription in subscriptions )
|
|
|
|
|
|
def _CanRetryFailed( self ):
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
return True in ( subscription.CanRetryFailed() for subscription in subscriptions )
|
|
|
|
|
|
def _CanRetryIgnored( self ):
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
return True in ( subscription.CanRetryIgnored() for subscription in subscriptions )
|
|
|
|
|
|
def _CanScrubDelays( self ):
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
return True in ( subscription.CanScrubDelay() for subscription in subscriptions )
|
|
|
|
|
|
def _CanSeparate( self ):
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
if len( subscriptions ) != 1:
|
|
|
|
return False
|
|
|
|
|
|
subscription = subscriptions[0]
|
|
|
|
if len( subscription.GetQueryHeaders() ) > 1:
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
def _ConvertSubscriptionToListCtrlTuples( self, subscription ):
|
|
|
|
( name, gug_key_and_name, query_headers, checker_options, initial_file_limit, periodic_file_limit, paused, file_import_options, tag_import_options, no_work_until, no_work_until_reason ) = subscription.ToTuple()
|
|
|
|
pretty_site = gug_key_and_name[1]
|
|
|
|
if len( query_headers ) > 0:
|
|
|
|
latest_new_file_time = max( ( query_header.GetLatestAddedTime() for query_header in query_headers ) )
|
|
|
|
last_checked = max( ( query_header.GetLastCheckTime() for query_header in query_headers ) )
|
|
|
|
else:
|
|
|
|
latest_new_file_time = 0
|
|
|
|
last_checked = 0
|
|
|
|
|
|
if latest_new_file_time is None or latest_new_file_time == 0:
|
|
|
|
pretty_latest_new_file_time = 'n/a'
|
|
|
|
else:
|
|
|
|
pretty_latest_new_file_time = ClientData.TimestampToPrettyTimeDelta( latest_new_file_time )
|
|
|
|
|
|
if last_checked is None or last_checked == 0:
|
|
|
|
pretty_last_checked = 'n/a'
|
|
|
|
else:
|
|
|
|
pretty_last_checked = ClientData.TimestampToPrettyTimeDelta( last_checked )
|
|
|
|
|
|
#
|
|
|
|
num_queries = len( query_headers )
|
|
num_dead = 0
|
|
num_paused = 0
|
|
|
|
for query_header in query_headers:
|
|
|
|
if query_header.IsDead():
|
|
|
|
num_dead += 1
|
|
|
|
elif query_header.IsPaused():
|
|
|
|
num_paused += 1
|
|
|
|
|
|
|
|
num_ok = num_queries - ( num_dead + num_paused )
|
|
|
|
status = ( num_queries, num_paused, num_dead )
|
|
|
|
if num_queries == 0:
|
|
|
|
pretty_status = 'no queries'
|
|
|
|
else:
|
|
|
|
status_components = [ HydrusData.ToHumanInt( num_ok ) + ' working' ]
|
|
|
|
if num_paused > 0:
|
|
|
|
status_components.append( HydrusData.ToHumanInt( num_paused ) + ' paused' )
|
|
|
|
|
|
if num_dead > 0:
|
|
|
|
status_components.append( HydrusData.ToHumanInt( num_dead ) + ' dead' )
|
|
|
|
|
|
pretty_status = ', '.join( status_components )
|
|
|
|
|
|
#
|
|
|
|
if HydrusData.TimeHasPassed( no_work_until ):
|
|
|
|
try:
|
|
|
|
( min_estimate, max_estimate ) = subscription.GetBandwidthWaitingEstimateMinMax( HG.client_controller.network_engine.bandwidth_manager )
|
|
|
|
if max_estimate == 0: # don't seem to be any delays of any kind
|
|
|
|
pretty_delay = ''
|
|
delay = 0
|
|
|
|
elif min_estimate == 0: # some are good to go, but there are delays
|
|
|
|
pretty_delay = 'bandwidth: some ok, some up to ' + HydrusData.TimeDeltaToPrettyTimeDelta( max_estimate )
|
|
delay = max_estimate
|
|
|
|
else:
|
|
|
|
if min_estimate == max_estimate: # probably just one query, and it is delayed
|
|
|
|
pretty_delay = 'bandwidth: up to ' + HydrusData.TimeDeltaToPrettyTimeDelta( max_estimate )
|
|
delay = max_estimate
|
|
|
|
else:
|
|
|
|
pretty_delay = 'bandwidth: from ' + HydrusData.TimeDeltaToPrettyTimeDelta( min_estimate ) + ' to ' + HydrusData.TimeDeltaToPrettyTimeDelta( max_estimate )
|
|
delay = max_estimate
|
|
|
|
|
|
|
|
except:
|
|
|
|
pretty_delay = 'could not determine bandwidth, there may be an error with the sub or its urls'
|
|
delay = 0
|
|
|
|
|
|
else:
|
|
|
|
pretty_delay = 'delayed--retrying ' + ClientData.TimestampToPrettyTimeDelta( no_work_until, just_now_threshold = 0 ) + ' - because: ' + no_work_until_reason
|
|
delay = HydrusData.GetTimeDeltaUntilTime( no_work_until )
|
|
|
|
|
|
file_seed_cache_status = ClientImportSubscriptionQuery.GenerateQueryHeadersStatus( query_headers )
|
|
|
|
( num_done, num_total ) = file_seed_cache_status.GetValueRange()
|
|
|
|
items = ( num_total, num_done )
|
|
|
|
pretty_items = file_seed_cache_status.GetStatusText( simple = True )
|
|
|
|
if paused:
|
|
|
|
pretty_paused = 'yes'
|
|
|
|
else:
|
|
|
|
pretty_paused = ''
|
|
|
|
|
|
sort_latest_new_file_time = ClientGUIListCtrl.SafeNoneInt( latest_new_file_time )
|
|
sort_last_checked = ClientGUIListCtrl.SafeNoneInt( last_checked )
|
|
|
|
display_tuple = ( name, pretty_site, pretty_status, pretty_latest_new_file_time, pretty_last_checked, pretty_delay, pretty_items, pretty_paused )
|
|
sort_tuple = ( name, pretty_site, status, sort_latest_new_file_time, sort_last_checked, delay, items, paused )
|
|
|
|
return ( display_tuple, sort_tuple )
|
|
|
|
|
|
def _DoAsyncGetQueryLogContainers( self, query_headers: typing.Collection[ ClientImportSubscriptionQuery.SubscriptionQueryHeader ], call: HydrusData.Call ):
|
|
|
|
missing_query_headers = [ query_header for query_header in query_headers if query_header.GetQueryLogContainerName() not in self._names_to_edited_query_log_containers ]
|
|
|
|
if len( missing_query_headers ) > 0:
|
|
|
|
self.setEnabled( False )
|
|
|
|
def work_callable():
|
|
|
|
result = GetQueryLogContainers( missing_query_headers )
|
|
|
|
return result
|
|
|
|
|
|
def publish_callable( result ):
|
|
|
|
query_log_containers = result
|
|
|
|
for query_log_container in query_log_containers:
|
|
|
|
self._names_to_edited_query_log_containers[ query_log_container.GetName() ] = query_log_container
|
|
|
|
|
|
self.setEnabled( True )
|
|
|
|
call()
|
|
|
|
|
|
async_call = ClientGUIAsync.AsyncQtJob( self, work_callable, publish_callable )
|
|
|
|
async_call.start()
|
|
|
|
else:
|
|
|
|
call()
|
|
|
|
|
|
|
|
def _GetExistingNames( self ):
|
|
|
|
subscriptions = self._subscriptions.GetData()
|
|
|
|
names = { subscription.GetName() for subscription in subscriptions }
|
|
|
|
return names
|
|
|
|
|
|
def _GetSelectedSubsAsExportableContainers( self ):
|
|
|
|
subs_to_export = self._subscriptions.GetData( only_selected = True )
|
|
|
|
required_query_log_headers = []
|
|
|
|
for sub in subs_to_export:
|
|
|
|
required_query_log_headers.extend( sub.GetQueryHeaders() )
|
|
|
|
|
|
missing_query_headers = [ query_header for query_header in required_query_log_headers if query_header.GetQueryLogContainerName() not in self._names_to_edited_query_log_containers ]
|
|
|
|
if len( missing_query_headers ) > 0:
|
|
|
|
if len( missing_query_headers ) > 25:
|
|
|
|
message = 'Exporting or duplicating the current selection means reading query data for {} queries from the database. This may take just a couple of seconds, or, for hundreds of thousands of cached URLs, it could be a couple of minutes (and a whack of memory). Do not panic, it will get there in the end. Do you want to do the export?'.format( HydrusData.ToHumanInt( len( missing_query_headers ) ) )
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return None
|
|
|
|
|
|
|
|
self.setEnabled( False )
|
|
|
|
done = threading.Event()
|
|
|
|
done_call = lambda: done.set()
|
|
|
|
self._DoAsyncGetQueryLogContainers( missing_query_headers, done_call )
|
|
|
|
while True:
|
|
|
|
if not QP.isValid( self ):
|
|
|
|
return None
|
|
|
|
|
|
if done.is_set():
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
time.sleep( 0.25 )
|
|
|
|
|
|
QW.QApplication.instance().processEvents()
|
|
|
|
|
|
self.setEnabled( True )
|
|
|
|
|
|
to_export = HydrusSerialisable.SerialisableList()
|
|
|
|
for sub in subs_to_export:
|
|
|
|
query_log_container_names = [ query_header.GetQueryLogContainerName() for query_header in sub.GetQueryHeaders() ]
|
|
|
|
query_log_containers = [ self._names_to_edited_query_log_containers[ query_log_container_name ] for query_log_container_name in query_log_container_names ]
|
|
|
|
subscription_container = ClientImportSubscriptions.SubscriptionContainer()
|
|
|
|
subscription_container.subscription = sub
|
|
subscription_container.query_log_containers = HydrusSerialisable.SerialisableList( query_log_containers )
|
|
|
|
# duplicate important here to make sure we aren't linked with existing objects on a dupe call
|
|
to_export.append( subscription_container.Duplicate() )
|
|
|
|
|
|
if len( to_export ) == 0:
|
|
|
|
return None
|
|
|
|
elif len( to_export ) == 1:
|
|
|
|
return to_export[0]
|
|
|
|
else:
|
|
|
|
return to_export
|
|
|
|
|
|
|
|
def _RegenDupeData( self ):
|
|
|
|
self._gug_names_to_dupe_cased_query_texts = collections.defaultdict( set )
|
|
self._subscriptions_to_dupe_cased_query_texts = collections.defaultdict( set )
|
|
|
|
gug_name_and_cased_query_text_count = collections.Counter()
|
|
gug_name_and_cased_query_text_to_subscriptions = collections.defaultdict( list )
|
|
|
|
self._gug_names_to_dupe_caseless_query_texts = collections.defaultdict( set )
|
|
self._subscriptions_to_dupe_caseless_query_texts = collections.defaultdict( set )
|
|
|
|
gug_name_and_caseless_query_text_count = collections.Counter()
|
|
gug_name_and_caseless_query_text_to_subscriptions = collections.defaultdict( list )
|
|
|
|
for subscription in self._subscriptions.GetData():
|
|
|
|
gug_name = subscription.GetGUGKeyAndName()[1]
|
|
|
|
query_headers = subscription.GetQueryHeaders()
|
|
|
|
for query_header in query_headers:
|
|
|
|
query_text = query_header.GetQueryText()
|
|
|
|
gug_name_and_cased_query_text_count[ ( gug_name, query_text ) ] += 1
|
|
gug_name_and_cased_query_text_to_subscriptions[ ( gug_name, query_text ) ].append( subscription )
|
|
|
|
query_text_lower = query_text.lower()
|
|
|
|
gug_name_and_caseless_query_text_count[ ( gug_name, query_text_lower ) ] += 1
|
|
gug_name_and_caseless_query_text_to_subscriptions[ ( gug_name, query_text_lower ) ].append( subscription )
|
|
|
|
|
|
|
|
for ( ( gug_name, query_text ), count ) in gug_name_and_cased_query_text_count.items():
|
|
|
|
if count <= 1:
|
|
|
|
continue
|
|
|
|
|
|
self._gug_names_to_dupe_cased_query_texts[ gug_name ].add( query_text )
|
|
|
|
for subscription in gug_name_and_cased_query_text_to_subscriptions[ ( gug_name, query_text ) ]:
|
|
|
|
self._subscriptions_to_dupe_cased_query_texts[ subscription ].add( query_text )
|
|
|
|
|
|
|
|
for ( ( gug_name, query_text_lower ), count ) in gug_name_and_caseless_query_text_count.items():
|
|
|
|
if count <= 1:
|
|
|
|
continue
|
|
|
|
|
|
self._gug_names_to_dupe_caseless_query_texts[ gug_name ].add( query_text_lower )
|
|
|
|
for subscription in gug_name_and_caseless_query_text_to_subscriptions[ ( gug_name, query_text_lower ) ]:
|
|
|
|
self._subscriptions_to_dupe_caseless_query_texts[ subscription ].add( query_text_lower )
|
|
|
|
|
|
|
|
self._subscriptions_panel.UpdateButtons()
|
|
|
|
|
|
def _STARTReset( self ):
|
|
|
|
message = 'Resetting these subscriptions will delete all their remembered urls, meaning when they next run, they will try to download them all over again. This may be expensive in time and data. Only do it if you are willing to wait. Do you want to do it?'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
query_headers = []
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
for subscription in subscriptions:
|
|
|
|
query_headers.extend( subscription.GetQueryHeaders() )
|
|
|
|
|
|
call = HydrusData.Call( self._Reset, query_headers )
|
|
|
|
self._DoAsyncGetQueryLogContainers( query_headers, call )
|
|
|
|
|
|
|
|
def _Reset( self, query_headers: typing.Iterable[ ClientImportSubscriptionQuery.SubscriptionQueryHeader ] ):
|
|
|
|
for query_header in query_headers:
|
|
|
|
query_log_container_name = query_header.GetQueryLogContainerName()
|
|
|
|
if query_log_container_name not in self._names_to_edited_query_log_containers:
|
|
|
|
continue
|
|
|
|
|
|
query_log_container = self._names_to_edited_query_log_containers[ query_log_container_name ]
|
|
|
|
query_header.Reset( query_log_container )
|
|
|
|
|
|
self._subscriptions.UpdateDatas()
|
|
|
|
|
|
def _STARTRetryFailed( self ):
|
|
|
|
query_headers = []
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
for subscription in subscriptions:
|
|
|
|
query_headers.extend( subscription.GetQueryHeaders() )
|
|
|
|
|
|
query_headers = [ query_header for query_header in query_headers if query_header.CanRetryFailed() ]
|
|
|
|
call = HydrusData.Call( self._RetryFailed, query_headers )
|
|
|
|
self._DoAsyncGetQueryLogContainers( query_headers, call )
|
|
|
|
|
|
def _RetryFailed( self, query_headers: typing.Iterable[ ClientImportSubscriptionQuery.SubscriptionQueryHeader ] ):
|
|
|
|
for query_header in query_headers:
|
|
|
|
query_log_container_name = query_header.GetQueryLogContainerName()
|
|
|
|
if query_log_container_name not in self._names_to_edited_query_log_containers:
|
|
|
|
continue
|
|
|
|
|
|
query_log_container = self._names_to_edited_query_log_containers[ query_log_container_name ]
|
|
|
|
query_log_container.GetFileSeedCache().RetryFailed()
|
|
|
|
query_header.UpdateFileStatus( query_log_container )
|
|
|
|
|
|
self._subscriptions.UpdateDatas()
|
|
|
|
|
|
def _STARTRetryIgnored( self ):
|
|
|
|
try:
|
|
|
|
ignored_regex = ClientGUIFileSeedCache.GetRetryIgnoredParam( self )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
query_headers = []
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
for subscription in subscriptions:
|
|
|
|
query_headers.extend( subscription.GetQueryHeaders() )
|
|
|
|
|
|
query_headers = [ query_header for query_header in query_headers if query_header.CanRetryIgnored() ]
|
|
|
|
call = HydrusData.Call( self._RetryIgnored, query_headers, ignored_regex )
|
|
|
|
self._DoAsyncGetQueryLogContainers( query_headers, call )
|
|
|
|
|
|
def _RetryIgnored( self, query_headers: typing.Iterable[ ClientImportSubscriptionQuery.SubscriptionQueryHeader ], ignored_regex: typing.Optional[ str ] ):
|
|
|
|
for query_header in query_headers:
|
|
|
|
query_log_container_name = query_header.GetQueryLogContainerName()
|
|
|
|
if query_log_container_name not in self._names_to_edited_query_log_containers:
|
|
|
|
continue
|
|
|
|
|
|
query_log_container = self._names_to_edited_query_log_containers[ query_log_container_name ]
|
|
|
|
query_log_container.GetFileSeedCache().RetryIgnored( ignored_regex = ignored_regex )
|
|
|
|
query_header.UpdateFileStatus( query_log_container )
|
|
|
|
|
|
self._subscriptions.UpdateDatas()
|
|
|
|
|
|
def Add( self ):
|
|
|
|
gug_key_and_name = HG.client_controller.network_engine.domain_manager.GetDefaultGUGKeyAndName()
|
|
|
|
empty_subscription = ClientImportSubscriptions.Subscription( 'new subscription', gug_key_and_name = gug_key_and_name )
|
|
|
|
frame_key = 'edit_subscription_dialog'
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit subscription', frame_key ) as dlg_edit:
|
|
|
|
panel = EditSubscriptionPanel( dlg_edit, empty_subscription, self._names_to_edited_query_log_containers )
|
|
|
|
dlg_edit.SetPanel( panel )
|
|
|
|
if dlg_edit.exec() == QW.QDialog.Accepted:
|
|
|
|
( new_subscription, self._names_to_edited_query_log_containers ) = panel.GetValue()
|
|
|
|
self._AddSubscription( new_subscription )
|
|
|
|
self._subscriptions.Sort()
|
|
|
|
|
|
|
|
|
|
def CheckNow( self ):
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
for subscription in subscriptions:
|
|
|
|
subscription.CheckNow()
|
|
|
|
|
|
self._subscriptions.UpdateDatas( subscriptions )
|
|
|
|
|
|
def DedupeAll( self ):
|
|
|
|
# first, select if we are cased or caseless
|
|
|
|
# can't do a nice 'yes, there are definitely more to do caseless here', as you can have aaA dupes that add to number deduped but don't increase the query text count
|
|
# for now, we won't bother to dive deeper into the data here, but it would be nice
|
|
|
|
num_cased = sum( ( len( query_texts ) for query_texts in self._gug_names_to_dupe_cased_query_texts.items() ) )
|
|
num_caseless = sum( ( len( query_texts ) for query_texts in self._gug_names_to_dupe_caseless_query_texts.items() ) )
|
|
|
|
can_do_cased = num_cased > 0
|
|
can_do_caseless = num_caseless > 0
|
|
|
|
if can_do_cased and can_do_caseless:
|
|
|
|
choice_tuples = [
|
|
( 'do a normal upper/lower case deduplication', False, 'Dedupe "samus_aran" with "samus_aran" or "Samus_Aran". This is usually the best option to go with--most sites ignore case.' ),
|
|
( 'only do exact text deduplication', True, 'Dedupe "samus_aran" with "samus_aran", but not "Samus_Aran". Usually not important, but some specific galleries may care about this.' )
|
|
]
|
|
|
|
message = 'Which kind of duplication are we going to do?'
|
|
|
|
try:
|
|
|
|
enforce_case = ClientGUIDialogsQuick.SelectFromListButtons( self, 'Caseless or cased?', choice_tuples, message = message )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
elif can_do_caseless:
|
|
|
|
message = 'There are no exact text duplicates. Only upper/lower case deduplication is available, merging queries like "samus_aran" and "Samus_Aran". Is this ok?'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
enforce_case = False
|
|
|
|
else:
|
|
|
|
# this should never happen, and we should not have the situation where only cased can be done. if cased can, caseless can, riiiiiight?
|
|
|
|
QW.QMessageBox.warning( self, 'Warning', 'There are no apparent duplicates, the dupe data will now be recalculated.' )
|
|
|
|
self._RegenDupeData()
|
|
|
|
return
|
|
|
|
|
|
# then select the downloader to dedupe for
|
|
|
|
if enforce_case:
|
|
|
|
gug_names_to_dupe_query_texts = self._gug_names_to_dupe_cased_query_texts
|
|
subscriptions_to_dupe_query_texts = self._subscriptions_to_dupe_cased_query_texts
|
|
|
|
else:
|
|
|
|
gug_names_to_dupe_query_texts = self._gug_names_to_dupe_caseless_query_texts
|
|
subscriptions_to_dupe_query_texts = self._subscriptions_to_dupe_caseless_query_texts
|
|
|
|
|
|
if len( gug_names_to_dupe_query_texts ) == 0:
|
|
|
|
QW.QMessageBox.warning( self, 'Warning', 'There are no apparent duplicates, the dupe data will now be recalculated.' )
|
|
|
|
self._RegenDupeData()
|
|
|
|
return
|
|
|
|
elif len( gug_names_to_dupe_query_texts ) == 1:
|
|
|
|
( gug_name, ) = gug_names_to_dupe_query_texts.keys()
|
|
|
|
else:
|
|
|
|
choice_tuples = []
|
|
|
|
for ( gug_name, query_texts ) in gug_names_to_dupe_query_texts.items():
|
|
|
|
label = '{} ({} duplicates)'.format( gug_name, HydrusData.ToHumanInt( len( query_texts ) ) )
|
|
|
|
tooltip = ', '.join( sorted( query_texts ) )
|
|
|
|
choice_tuples.append( ( label, gug_name, tooltip ) )
|
|
|
|
|
|
message = 'Multiple downloaders have duplicate queries. Which would you like to dedupe now?'
|
|
|
|
try:
|
|
|
|
gug_name = ClientGUIDialogsQuick.SelectFromListButtons( self, 'Which downloader to work on?', choice_tuples, message = message )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
|
|
subscriptions_we_will_be_deduping = [ subscription for subscription in subscriptions_to_dupe_query_texts if subscription.GetGUGKeyAndName()[1] == gug_name ]
|
|
|
|
# now select which queries to dedupe
|
|
|
|
potential_dupe_query_texts = gug_names_to_dupe_query_texts[ gug_name ]
|
|
|
|
if len( potential_dupe_query_texts ) == 0:
|
|
|
|
QW.QMessageBox.warning( self, 'Warning', 'Strangely, there are actually no apparent duplicates for this downloader, the dupe data will now be recalculated. Let hydev know about this, please.' )
|
|
|
|
self._RegenDupeData()
|
|
|
|
return
|
|
|
|
elif len( potential_dupe_query_texts ) == 1:
|
|
|
|
( query_text, ) = potential_dupe_query_texts
|
|
|
|
message = 'The downloader "{}" has a single duplicate query "{}". Is this ok to dedupe?'.format( gug_name, query_text )
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
query_texts_we_want_to_dedupe_now = potential_dupe_query_texts
|
|
|
|
else:
|
|
|
|
message = 'There are {} duplicate query texts for the downloader "{}". Would you like to dedupe them all, or select which to do?'.format( HydrusData.ToHumanInt( len( potential_dupe_query_texts ) ), gug_name )
|
|
|
|
yes_tuples = []
|
|
|
|
yes_tuples.append( ( 'do them all', 'all' ) )
|
|
yes_tuples.append( ( 'select which to do', 'select' ) )
|
|
|
|
with ClientGUIDialogs.DialogYesYesNo( self, message, yes_tuples = yes_tuples, no_label = 'forget it' ) as dlg:
|
|
|
|
if dlg.exec() != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
value = dlg.GetValue()
|
|
|
|
if value == 'all':
|
|
|
|
query_texts_we_want_to_dedupe_now = potential_dupe_query_texts
|
|
|
|
else:
|
|
|
|
selected = True
|
|
|
|
choice_tuples = [ ( query_text, query_text, selected ) for query_text in potential_dupe_query_texts ]
|
|
|
|
try:
|
|
|
|
query_texts_we_want_to_dedupe_now = ClientGUIDialogsQuick.SelectMultipleFromList( self, 'Select which query texts to dedupe', choice_tuples )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
if len( query_texts_we_want_to_dedupe_now ) == 0:
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# we are now set on what we want to dedupe.
|
|
|
|
keep_going = True
|
|
query_texts_we_want_to_dedupe_now = set( query_texts_we_want_to_dedupe_now )
|
|
|
|
try:
|
|
|
|
while keep_going:
|
|
|
|
if enforce_case:
|
|
|
|
subscriptions_to_dupe_query_texts = self._subscriptions_to_dupe_cased_query_texts
|
|
|
|
else:
|
|
|
|
subscriptions_to_dupe_query_texts = self._subscriptions_to_dupe_caseless_query_texts
|
|
|
|
|
|
# now select the master sub that will not dedupe
|
|
|
|
subscriptions_to_dupe_query_texts_it_can_do = {}
|
|
|
|
choice_tuples = []
|
|
|
|
for subscription in subscriptions_we_will_be_deduping:
|
|
|
|
sub_dupe_query_texts = subscriptions_to_dupe_query_texts[ subscription ]
|
|
|
|
dupe_query_texts_it_can_do = set( sub_dupe_query_texts ).intersection( query_texts_we_want_to_dedupe_now )
|
|
|
|
if len( dupe_query_texts_it_can_do ) == 0:
|
|
|
|
continue # this doesn't have the queries we selected previously, or was cleared out in a previous loop
|
|
|
|
|
|
subscriptions_to_dupe_query_texts_it_can_do[ subscription ] = dupe_query_texts_it_can_do
|
|
|
|
if len( dupe_query_texts_it_can_do ) == len( query_texts_we_want_to_dedupe_now ):
|
|
|
|
label = '{} (has all duplicate queries)'.format( subscription.GetName() )
|
|
|
|
else:
|
|
|
|
label = '{} (has {} duplicate queries)'.format( subscription.GetName(), HydrusData.ConvertValueRangeToPrettyString( len( dupe_query_texts_it_can_do ), len( query_texts_we_want_to_dedupe_now ) ) )
|
|
|
|
|
|
choice_tuples.append( ( label, subscription, ', '.join( sorted( dupe_query_texts_it_can_do ) ) ) )
|
|
|
|
|
|
if len( choice_tuples ) == 0:
|
|
|
|
QW.QMessageBox.warning( self, 'Warning', 'Strangely, there are actually no subscriptions that can do dedupe work for the selected duplicates, the dupe data will now be recalculated. Let hydev know about this, please.' )
|
|
|
|
return
|
|
|
|
elif len( choice_tuples ) == 1:
|
|
|
|
master_subscription = choice_tuples[0][1]
|
|
|
|
message = 'Subscription "{}" will now dedupe the queries within itself. Is this ok?'.format( master_subscription.GetName() )
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
else:
|
|
|
|
choice_tuples.sort( key = lambda c_t: c_t[1].GetName() )
|
|
|
|
message = 'Which subscription is going to keep the queries? If the subscription cannot dedupe them all, you will be able to select another, to do the rest, in a moment.'
|
|
|
|
try:
|
|
|
|
master_subscription = ClientGUIDialogsQuick.SelectFromListButtons( self, 'Which sub to retain the queries on?', choice_tuples, message = message )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
|
|
# we are now good to go
|
|
|
|
query_texts_our_master_can_do = subscriptions_to_dupe_query_texts_it_can_do[ master_subscription ]
|
|
|
|
for subscription in subscriptions_we_will_be_deduping:
|
|
|
|
if subscription == master_subscription:
|
|
|
|
subscription.DedupeQueryTexts( query_texts_our_master_can_do, enforce_case = enforce_case )
|
|
|
|
else:
|
|
|
|
subscription.RemoveQueryTexts( query_texts_our_master_can_do, enforce_case = enforce_case )
|
|
|
|
|
|
|
|
# can more work be done? if so, ask user if they want to keep going
|
|
|
|
query_texts_we_want_to_dedupe_now.difference_update( query_texts_our_master_can_do )
|
|
|
|
keep_going = False
|
|
|
|
if len( query_texts_we_want_to_dedupe_now ) > 0:
|
|
|
|
message = 'Dedupe was done. There are still {} queries that can be deduped between other subscriptions. Want to do them now?'.format( HydrusData.ToHumanInt( len( query_texts_we_want_to_dedupe_now ) ) )
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
keep_going = True
|
|
|
|
self._RegenDupeData() # not actually needed, but let's work on current data mate
|
|
|
|
|
|
|
|
|
|
finally:
|
|
|
|
self._subscriptions.UpdateDatas()
|
|
self._subscriptions.Sort()
|
|
|
|
self._RegenDupeData()
|
|
|
|
|
|
|
|
def Edit( self ):
|
|
|
|
subs_to_edit = self._subscriptions.GetData( only_selected = True )
|
|
|
|
for subscription in subs_to_edit:
|
|
|
|
frame_key = 'edit_subscription_dialog'
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit subscription', frame_key ) as dlg:
|
|
|
|
panel = EditSubscriptionPanel( dlg, subscription, self._names_to_edited_query_log_containers )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
result = dlg.exec()
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
self._subscriptions.DeleteDatas( ( subscription, ) )
|
|
|
|
( edited_subscription, self._names_to_edited_query_log_containers ) = panel.GetValue()
|
|
|
|
edited_subscription.SetNonDupeName( self._GetExistingNames() )
|
|
|
|
self._subscriptions.AddDatas( ( edited_subscription, ) )
|
|
|
|
self._RegenDupeData()
|
|
|
|
elif dlg.WasCancelled():
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
self._subscriptions.Sort()
|
|
|
|
|
|
def GetValue( self ) -> typing.List[ ClientImportSubscriptions.Subscription ]:
|
|
|
|
if not self.isEnabled():
|
|
|
|
raise HydrusExceptions.VetoException( 'It looks like a long-running job is still working. Please wait for it to finish before applying this dialog.' )
|
|
|
|
|
|
subscriptions = self._subscriptions.GetData()
|
|
|
|
required_query_log_container_names = set()
|
|
|
|
for subscription in subscriptions:
|
|
|
|
required_query_log_container_names.update( subscription.GetAllQueryLogContainerNames() )
|
|
|
|
|
|
edited_query_log_containers = list( self._names_to_edited_query_log_containers.values() )
|
|
|
|
edited_query_log_containers = [ query_log_container for query_log_container in edited_query_log_containers if query_log_container.GetName() in required_query_log_container_names ]
|
|
|
|
deletee_query_log_container_names = self._existing_query_log_container_names.difference( required_query_log_container_names )
|
|
|
|
return ( subscriptions, edited_query_log_containers, deletee_query_log_container_names )
|
|
|
|
|
|
def LowerCaseQueries( self ):
|
|
|
|
message = 'This will convert the selected subscriptions\' queries to lowercase text. "Samus_Aran" will become "samus_aran". Most sites do not care about case, but it can help things stay neat.'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
for subscription in subscriptions:
|
|
|
|
subscription.LowerCaseQueries()
|
|
|
|
|
|
self._RegenDupeData()
|
|
|
|
|
|
|
|
def Merge( self ):
|
|
|
|
message = 'Are you sure you want to merge the selected subscriptions? This will combine all selected subscriptions that share the same downloader, wrapping all their different queries into one subscription.'
|
|
message += os.linesep * 2
|
|
message += 'This is a big operation, so if it does not do what you expect, hit cancel afterwards!'
|
|
message += os.linesep * 2
|
|
message += 'Please note that all other subscription settings settings (like paused status and file limits and tag options) will be merged as well, so double-check your merged subs\' settings afterwards.'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
original_subs = self._subscriptions.GetData( only_selected = True )
|
|
|
|
potential_mergees = [ sub.Duplicate() for sub in original_subs ]
|
|
|
|
mergeable_groups = []
|
|
merged_subs = []
|
|
unmergeable_subs = []
|
|
|
|
while len( potential_mergees ) > 0:
|
|
|
|
potential_primary = potential_mergees.pop()
|
|
|
|
( mergeables_with_our_primary, not_mergeable_with_our_primary ) = potential_primary.GetMergeable( potential_mergees )
|
|
|
|
if len( mergeables_with_our_primary ) > 0:
|
|
|
|
mergeable_group = []
|
|
|
|
mergeable_group.append( potential_primary )
|
|
mergeable_group.extend( mergeables_with_our_primary )
|
|
|
|
mergeable_groups.append( mergeable_group )
|
|
|
|
else:
|
|
|
|
unmergeable_subs.append( potential_primary )
|
|
|
|
|
|
potential_mergees = not_mergeable_with_our_primary
|
|
|
|
|
|
if len( mergeable_groups ) == 0:
|
|
|
|
QW.QMessageBox.information( self, 'Information', 'Unfortunately, none of those subscriptions appear to be mergeable!' )
|
|
|
|
return
|
|
|
|
|
|
for mergeable_group in mergeable_groups:
|
|
|
|
mergeable_group.sort( key = lambda sub: sub.GetName() )
|
|
|
|
choice_tuples = [ ( sub.GetName(), sub ) for sub in mergeable_group ]
|
|
|
|
try:
|
|
|
|
primary_sub = ClientGUIDialogsQuick.SelectFromList( self, 'select the primary subscription--into which to merge the others', choice_tuples )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
mergeable_group.remove( primary_sub )
|
|
|
|
unmerged = primary_sub.Merge( mergeable_group )
|
|
|
|
unmergeable_subs.extend( unmerged )
|
|
|
|
primary_sub_name = primary_sub.GetName()
|
|
|
|
message = primary_sub_name + ' was able to merge ' + HydrusData.ToHumanInt( len( mergeable_group ) ) + ' other subscriptions. If you wish to change its name, do so here.'
|
|
|
|
with ClientGUIDialogs.DialogTextEntry( self, message, default = primary_sub_name ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
name = dlg.GetValue()
|
|
|
|
primary_sub.SetName( name )
|
|
|
|
|
|
# don't care about a cancel here--we'll take that as 'I didn't want to change its name', not 'abort'
|
|
|
|
|
|
merged_subs.append( primary_sub )
|
|
|
|
|
|
# we are ready to do it
|
|
|
|
self._subscriptions.DeleteDatas( original_subs )
|
|
|
|
self._subscriptions.AddDatas( unmergeable_subs )
|
|
|
|
for merged_sub in merged_subs:
|
|
|
|
merged_sub.SetNonDupeName( self._GetExistingNames() )
|
|
|
|
self._subscriptions.AddDatas( ( merged_sub, ) )
|
|
|
|
|
|
self._RegenDupeData()
|
|
|
|
self._subscriptions.Sort()
|
|
|
|
|
|
|
|
def PauseResume( self ):
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
for subscription in subscriptions:
|
|
|
|
subscription.PauseResume()
|
|
|
|
|
|
self._subscriptions.UpdateDatas( subscriptions )
|
|
|
|
|
|
def ScrubDelays( self ):
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
for subscription in subscriptions:
|
|
|
|
subscription.ScrubDelay()
|
|
|
|
|
|
self._subscriptions.UpdateDatas( subscriptions )
|
|
|
|
|
|
def SelectSubscriptions( self ):
|
|
|
|
message = 'This selects subscriptions based on query text. Please enter some search text, and any subscription that has a query that includes that text will be selected.'
|
|
|
|
with ClientGUIDialogs.DialogTextEntry( self, message ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
search_text = dlg.GetValue()
|
|
|
|
self._subscriptions.clearSelection()
|
|
|
|
selectee_subscriptions = []
|
|
|
|
for subscription in self._subscriptions.GetData():
|
|
|
|
if subscription.HasQuerySearchTextFragment( search_text ):
|
|
|
|
selectee_subscriptions.append( subscription )
|
|
|
|
|
|
|
|
self._subscriptions.SelectDatas( selectee_subscriptions )
|
|
|
|
|
|
|
|
|
|
def Separate( self ):
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
if len( subscriptions ) != 1:
|
|
|
|
QW.QMessageBox.critical( self, 'Error', 'Separate only works if one subscription is selected!' )
|
|
|
|
return
|
|
|
|
|
|
subscription = subscriptions[0]
|
|
|
|
num_queries = len( subscription.GetQueryHeaders() )
|
|
|
|
if num_queries <= 1:
|
|
|
|
QW.QMessageBox.critical( self, 'Error', 'Separate only works if the selected subscription has more than one query!' )
|
|
|
|
return
|
|
|
|
|
|
if num_queries > 100:
|
|
|
|
message = 'This is a large subscription. It is difficult to separate it on a per-query basis, so instead the system will automatically cut it into two halves. Is this ok?'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
action = 'half'
|
|
|
|
elif num_queries > 2:
|
|
|
|
message = 'Are you sure you want to separate the selected subscriptions? Separating breaks merged subscriptions apart into smaller pieces.'
|
|
yes_tuples = [ ( 'break it in half', 'half' ), ( 'break it all into single-query subscriptions', 'whole' ), ( 'only extract some of the subscription', 'part' ) ]
|
|
|
|
with ClientGUIDialogs.DialogYesYesNo( self, message, yes_tuples = yes_tuples, no_label = 'forget it' ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
action = dlg.GetValue()
|
|
|
|
else:
|
|
|
|
return
|
|
|
|
|
|
|
|
else:
|
|
|
|
action = 'whole'
|
|
|
|
|
|
want_post_merge = False
|
|
|
|
if action == 'part':
|
|
|
|
query_headers = subscription.GetQueryHeaders()
|
|
|
|
choice_tuples = [ ( query_header.GetHumanName(), query_header, False ) for query_header in query_headers ]
|
|
|
|
try:
|
|
|
|
query_headers_to_extract = ClientGUIDialogsQuick.SelectMultipleFromList( self, 'select the queries to extract', choice_tuples )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
if len( query_headers_to_extract ) == num_queries: # the madman selected them all
|
|
|
|
action = 'whole'
|
|
|
|
elif len( query_headers_to_extract ) > 1:
|
|
|
|
yes_tuples = [ ( 'one new merged subscription', True ), ( 'many subscriptions with only one query', False ) ]
|
|
|
|
message = 'Do you want the extracted queries to be a new merged subscription, or many subscriptions with only one query?'
|
|
|
|
with ClientGUIDialogs.DialogYesYesNo( self, message, yes_tuples = yes_tuples, no_label = 'forget it' ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
want_post_merge = dlg.GetValue()
|
|
|
|
else:
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if action != 'half':
|
|
|
|
if want_post_merge:
|
|
|
|
message = 'Please enter the name for the new subscription.'
|
|
|
|
else:
|
|
|
|
message = 'Please enter the base name for the new subscriptions. They will be named \'[NAME]: query\'.'
|
|
|
|
|
|
with ClientGUIDialogs.DialogTextEntry( self, message, default = subscription.GetName() ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
name = dlg.GetValue()
|
|
|
|
else:
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# ok, let's do it
|
|
|
|
final_subscriptions = []
|
|
|
|
self._subscriptions.DeleteDatas( ( subscription, ) )
|
|
|
|
if action == 'whole':
|
|
|
|
final_subscriptions.extend( subscription.Separate( name ) )
|
|
|
|
elif action == 'part':
|
|
|
|
extracted_subscriptions = list( subscription.Separate( name, query_headers_to_extract ) )
|
|
|
|
if want_post_merge:
|
|
|
|
# it is ok to do a blind merge here since they all share the same settings and will get a new name
|
|
|
|
primary_sub = extracted_subscriptions.pop()
|
|
|
|
unmerged = primary_sub.Merge( extracted_subscriptions )
|
|
|
|
final_subscriptions.extend( unmerged )
|
|
|
|
primary_sub.SetName( name )
|
|
|
|
final_subscriptions.append( primary_sub )
|
|
|
|
else:
|
|
|
|
final_subscriptions.extend( extracted_subscriptions )
|
|
|
|
|
|
final_subscriptions.append( subscription )
|
|
|
|
elif action == 'half':
|
|
|
|
query_headers = subscription.GetQueryHeaders()
|
|
|
|
query_headers_to_extract = query_headers[ : len( query_headers ) // 2 ]
|
|
|
|
name = subscription.GetName()
|
|
|
|
extracted_subscriptions = list( subscription.Separate( name, query_headers_to_extract ) )
|
|
|
|
primary_sub = extracted_subscriptions.pop()
|
|
|
|
unmerged = primary_sub.Merge( extracted_subscriptions )
|
|
|
|
final_subscriptions.extend( unmerged )
|
|
|
|
primary_sub.SetName( '{} (A)'.format( name ) )
|
|
subscription.SetName( '{} (B)'.format( name ) )
|
|
|
|
final_subscriptions.append( primary_sub )
|
|
final_subscriptions.append( subscription )
|
|
|
|
|
|
for final_subscription in final_subscriptions:
|
|
|
|
final_subscription.SetNonDupeName( self._GetExistingNames() )
|
|
|
|
self._subscriptions.AddDatas( ( final_subscription, ) )
|
|
|
|
|
|
self._RegenDupeData()
|
|
|
|
self._subscriptions.Sort()
|
|
|
|
|
|
def SetCheckerOptions( self ):
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
if len( subscriptions ) == 0:
|
|
|
|
return
|
|
|
|
|
|
checker_options = subscriptions[0].GetCheckerOptions()
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit check timings' ) as dlg:
|
|
|
|
panel = ClientGUITime.EditCheckerOptions( dlg, checker_options )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
checker_options = panel.GetValue()
|
|
|
|
for subscription in subscriptions:
|
|
|
|
subscription.SetCheckerOptions( checker_options )
|
|
|
|
|
|
self._subscriptions.UpdateDatas( subscriptions )
|
|
|
|
|
|
|
|
|
|
def SetTagImportOptions( self ):
|
|
|
|
subscriptions = self._subscriptions.GetData( only_selected = True )
|
|
|
|
if len( subscriptions ) == 0:
|
|
|
|
return
|
|
|
|
|
|
tag_import_options = subscriptions[0].GetTagImportOptions()
|
|
show_downloader_options = True
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit tag import options' ) as dlg:
|
|
|
|
panel = ClientGUIScrolledPanelsEdit.EditTagImportOptionsPanel( dlg, tag_import_options, show_downloader_options, allow_default_selection = True )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
tag_import_options = panel.GetValue()
|
|
|
|
for subscription in subscriptions:
|
|
|
|
subscription.SetTagImportOptions( tag_import_options )
|
|
|
|
|
|
self._subscriptions.UpdateDatas( subscriptions )
|
|
|
|
|
|
|
|
|