1731 lines
60 KiB
Python
1731 lines
60 KiB
Python
import itertools
|
|
import os
|
|
import threading
|
|
import traceback
|
|
import typing
|
|
|
|
from qtpy import QtCore as QC
|
|
from qtpy import QtWidgets as QW
|
|
|
|
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.client import ClientConstants as CC
|
|
from hydrus.client import ClientDefaults
|
|
from hydrus.client import ClientParsing
|
|
from hydrus.client import ClientPaths
|
|
from hydrus.client import ClientStrings
|
|
from hydrus.client.gui import ClientGUIDialogs
|
|
from hydrus.client.gui import ClientGUIDialogsQuick
|
|
from hydrus.client.gui import ClientGUIScrolledPanels
|
|
from hydrus.client.gui import ClientGUISerialisable
|
|
from hydrus.client.gui import ClientGUIStringControls
|
|
from hydrus.client.gui import ClientGUIStringPanels
|
|
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
|
|
from hydrus.client.gui import QtPorting as QP
|
|
from hydrus.client.gui.lists import ClientGUIListBoxes
|
|
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
|
|
from hydrus.client.gui.lists import ClientGUIListCtrl
|
|
from hydrus.client.gui.networking import ClientGUINetworkJobControl
|
|
from hydrus.client.gui.parsing import ClientGUIParsingFormulae
|
|
from hydrus.client.gui.parsing import ClientGUIParsingTest
|
|
from hydrus.client.gui.widgets import ClientGUICommon
|
|
from hydrus.client.gui.widgets import ClientGUIMenuButton
|
|
from hydrus.client.networking import ClientNetworkingContexts
|
|
from hydrus.client.networking import ClientNetworkingDomain
|
|
from hydrus.client.networking import ClientNetworkingFunctions
|
|
from hydrus.client.networking import ClientNetworkingGUG
|
|
from hydrus.client.networking import ClientNetworkingJobs
|
|
from hydrus.client.networking import ClientNetworkingURLClass
|
|
|
|
class DownloaderExportPanel( ClientGUIScrolledPanels.ReviewPanel ):
|
|
|
|
def __init__( self, parent, network_engine ):
|
|
|
|
ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent )
|
|
|
|
self._network_engine = network_engine
|
|
|
|
menu_items = []
|
|
|
|
page_func = HydrusData.Call( ClientPaths.LaunchPathInWebBrowser, os.path.join( HC.HELP_DIR, 'downloader_sharing.html' ) )
|
|
|
|
menu_items.append( ( 'normal', 'open the downloader sharing help', 'Open the help page for sharing downloaders 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 -->', object_name = 'HydrusIndeterminate' )
|
|
|
|
listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
|
|
|
|
self._listctrl = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, CGLC.COLUMN_LIST_DOWNLOADER_EXPORT.ID, 14, self._ConvertContentToListCtrlTuples, use_simple_delete = True )
|
|
|
|
self._listctrl.Sort()
|
|
|
|
listctrl_panel.SetListCtrl( self._listctrl )
|
|
|
|
listctrl_panel.AddButton( 'add gug', self._AddGUG )
|
|
listctrl_panel.AddButton( 'add url class', self._AddURLClass )
|
|
listctrl_panel.AddButton( 'add parser', self._AddParser )
|
|
listctrl_panel.AddButton( 'add login script', self._AddLoginScript )
|
|
listctrl_panel.AddButton( 'add headers/bandwidth rules', self._AddDomainMetadata )
|
|
listctrl_panel.AddDeleteButton()
|
|
listctrl_panel.AddSeparator()
|
|
listctrl_panel.AddButton( 'export to png', self._Export, enabled_check_func = self._CanExport )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, help_hbox, CC.FLAGS_ON_RIGHT )
|
|
QP.AddToLayout( vbox, listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
|
|
def _AddDomainMetadata( self ):
|
|
|
|
message = 'Enter domain:'
|
|
|
|
with ClientGUIDialogs.DialogTextEntry( self, message ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
domain = dlg.GetValue()
|
|
|
|
else:
|
|
|
|
return
|
|
|
|
|
|
|
|
domain_metadatas = self._GetDomainMetadatasToInclude( { domain } )
|
|
|
|
if len( domain_metadatas ) > 0:
|
|
|
|
self._listctrl.AddDatas( domain_metadatas )
|
|
|
|
else:
|
|
|
|
QW.QMessageBox.information( self, 'Information', 'No headers/bandwidth rules found!' )
|
|
|
|
|
|
|
|
def _AddGUG( self ):
|
|
|
|
existing_data = self._listctrl.GetData()
|
|
|
|
choosable_gugs = [ gug for gug in self._network_engine.domain_manager.GetGUGs() if gug.IsFunctional() and gug not in existing_data ]
|
|
|
|
choice_tuples = [ ( gug.GetName(), gug, False ) for gug in choosable_gugs ]
|
|
|
|
try:
|
|
|
|
gugs_to_include = ClientGUIDialogsQuick.SelectMultipleFromList( self, 'select gugs', choice_tuples )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
gugs_to_include = self._FleshOutNGUGsWithGUGs( gugs_to_include )
|
|
|
|
domains = { ClientNetworkingFunctions.ConvertURLIntoDomain( example_url ) for example_url in itertools.chain.from_iterable( ( gug.GetExampleURLs() for gug in gugs_to_include ) ) }
|
|
|
|
domain_metadatas_to_include = self._GetDomainMetadatasToInclude( domains )
|
|
|
|
url_classes_to_include = self._GetURLClassesToInclude( gugs_to_include )
|
|
|
|
url_classes_to_include = self._FleshOutURLClassesWithAPILinks( url_classes_to_include )
|
|
|
|
parsers_to_include = self._GetParsersToInclude( url_classes_to_include )
|
|
|
|
self._listctrl.AddDatas( domain_metadatas_to_include )
|
|
self._listctrl.AddDatas( gugs_to_include )
|
|
self._listctrl.AddDatas( url_classes_to_include )
|
|
self._listctrl.AddDatas( parsers_to_include )
|
|
|
|
|
|
def _AddLoginScript( self ):
|
|
|
|
existing_data = self._listctrl.GetData()
|
|
|
|
choosable_login_scripts = [ ls for ls in self._network_engine.login_manager.GetLoginScripts() if ls not in existing_data ]
|
|
|
|
choice_tuples = [ ( login_script.GetName(), login_script, False ) for login_script in choosable_login_scripts ]
|
|
|
|
try:
|
|
|
|
login_scripts_to_include = ClientGUIDialogsQuick.SelectMultipleFromList( self, 'select login scripts', choice_tuples )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
self._listctrl.AddDatas( login_scripts_to_include )
|
|
|
|
|
|
def _AddParser( self ):
|
|
|
|
existing_data = self._listctrl.GetData()
|
|
|
|
choosable_parsers = [ p for p in self._network_engine.domain_manager.GetParsers() if p not in existing_data ]
|
|
|
|
choice_tuples = [ ( parser.GetName(), parser, False ) for parser in choosable_parsers ]
|
|
|
|
try:
|
|
|
|
parsers_to_include = ClientGUIDialogsQuick.SelectMultipleFromList( self, 'select parsers to include', choice_tuples )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
self._listctrl.AddDatas( parsers_to_include )
|
|
|
|
|
|
def _AddURLClass( self ):
|
|
|
|
existing_data = self._listctrl.GetData()
|
|
|
|
choosable_url_classes = [ u for u in self._network_engine.domain_manager.GetURLClasses() if u not in existing_data ]
|
|
|
|
choice_tuples = [ ( url_class.GetName(), url_class, False ) for url_class in choosable_url_classes ]
|
|
|
|
try:
|
|
|
|
url_classes_to_include = ClientGUIDialogsQuick.SelectMultipleFromList( self, 'select url classes to include', choice_tuples )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
url_classes_to_include = self._FleshOutURLClassesWithAPILinks( url_classes_to_include )
|
|
|
|
parsers_to_include = self._GetParsersToInclude( url_classes_to_include )
|
|
|
|
self._listctrl.AddDatas( url_classes_to_include )
|
|
self._listctrl.AddDatas( parsers_to_include )
|
|
|
|
|
|
def _CanExport( self ):
|
|
|
|
return len( self._listctrl.GetData() ) > 0
|
|
|
|
|
|
def _ConvertContentToListCtrlTuples( self, content ):
|
|
|
|
if isinstance( content, ClientNetworkingDomain.DomainMetadataPackage ):
|
|
|
|
name = content.GetDomain()
|
|
|
|
else:
|
|
|
|
name = content.GetName()
|
|
|
|
|
|
t = content.SERIALISABLE_NAME
|
|
|
|
pretty_name = name
|
|
pretty_t = t
|
|
|
|
display_tuple = ( pretty_name, pretty_t )
|
|
sort_tuple = ( name, t )
|
|
|
|
return ( display_tuple, sort_tuple )
|
|
|
|
|
|
def _Export( self ):
|
|
|
|
export_object = HydrusSerialisable.SerialisableList( self._listctrl.GetData() )
|
|
|
|
message = 'The end-user will see this sort of summary:'
|
|
message += os.linesep * 2
|
|
message += os.linesep.join( ( obj.GetSafeSummary() for obj in export_object[:20] ) )
|
|
|
|
if len( export_object ) > 20:
|
|
|
|
message += os.linesep
|
|
message += '(and ' + HydrusData.ToHumanInt( len( export_object ) - 20 ) + ' others)'
|
|
|
|
|
|
message += os.linesep * 2
|
|
message += 'Does that look good? (Ideally, every object should have correct and sane domains listed here)'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
gug_names = set()
|
|
|
|
for obj in export_object:
|
|
|
|
if isinstance( obj, ( ClientNetworkingGUG.GalleryURLGenerator, ClientNetworkingGUG.NestedGalleryURLGenerator ) ):
|
|
|
|
gug_names.add( obj.GetName() )
|
|
|
|
|
|
|
|
gug_names = sorted( gug_names )
|
|
|
|
num_gugs = len( gug_names )
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'export to png' ) as dlg:
|
|
|
|
title = 'easy-import downloader png'
|
|
|
|
if num_gugs == 0:
|
|
|
|
description = 'some download components'
|
|
|
|
else:
|
|
|
|
title += ' - ' + HydrusData.ToHumanInt( num_gugs ) + ' downloaders'
|
|
|
|
description = ', '.join( gug_names )
|
|
|
|
|
|
panel = ClientGUISerialisable.PNGExportPanel( dlg, export_object, title = title, description = description )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
dlg.exec()
|
|
|
|
|
|
|
|
def _FleshOutNGUGsWithGUGs( self, gugs ):
|
|
|
|
gugs_to_include = set( gugs )
|
|
|
|
existing_data = self._listctrl.GetData()
|
|
|
|
possible_new_gugs = [ gug for gug in self._network_engine.domain_manager.GetGUGs() if gug.IsFunctional() and gug not in existing_data and gug not in gugs_to_include ]
|
|
|
|
interesting_gug_keys_and_names = list( itertools.chain.from_iterable( [ gug.GetGUGKeysAndNames() for gug in gugs_to_include if isinstance( gug, ClientNetworkingGUG.NestedGalleryURLGenerator ) ] ) )
|
|
|
|
interesting_gugs = [ gug for gug in possible_new_gugs if gug.GetGUGKeyAndName() in interesting_gug_keys_and_names ]
|
|
|
|
gugs_to_include.update( interesting_gugs )
|
|
|
|
if True in ( isinstance( gug, ClientNetworkingGUG.NestedGalleryURLGenerator ) for gug in interesting_gugs ):
|
|
|
|
return self._FleshOutNGUGsWithGUGs( gugs_to_include )
|
|
|
|
else:
|
|
|
|
return gugs_to_include
|
|
|
|
|
|
|
|
def _FleshOutURLClassesWithAPILinks( self, url_classes ):
|
|
|
|
url_classes_to_include = set( url_classes )
|
|
|
|
api_links_dict = dict( ClientNetworkingURLClass.ConvertURLClassesIntoAPIPairs( self._network_engine.domain_manager.GetURLClasses() ) )
|
|
|
|
for url_class in url_classes:
|
|
|
|
added_this_cycle = set()
|
|
|
|
while url_class in api_links_dict and url_class not in added_this_cycle:
|
|
|
|
added_this_cycle.add( url_class )
|
|
|
|
url_class = api_links_dict[ url_class ]
|
|
|
|
url_classes_to_include.add( url_class )
|
|
|
|
|
|
|
|
existing_data = self._listctrl.GetData()
|
|
|
|
url_classes_to_include = [ u for u in url_classes_to_include if u not in existing_data ]
|
|
|
|
return url_classes_to_include
|
|
|
|
|
|
def _GetDomainMetadatasToInclude( self, domains ):
|
|
|
|
domains = { d for d in itertools.chain.from_iterable( ClientNetworkingFunctions.ConvertDomainIntoAllApplicableDomains( domain ) for domain in domains ) }
|
|
|
|
existing_domains = { obj.GetDomain() for obj in self._listctrl.GetData() if isinstance( obj, ClientNetworkingDomain.DomainMetadataPackage ) }
|
|
|
|
domains = domains.difference( existing_domains )
|
|
|
|
domains = sorted( domains )
|
|
|
|
domain_metadatas = []
|
|
|
|
for domain in domains:
|
|
|
|
network_context = ClientNetworkingContexts.NetworkContext( CC.NETWORK_CONTEXT_DOMAIN, domain )
|
|
|
|
if self._network_engine.domain_manager.HasCustomHeaders( network_context ):
|
|
|
|
headers_list = self._network_engine.domain_manager.GetShareableCustomHeaders( network_context )
|
|
|
|
else:
|
|
|
|
headers_list = None
|
|
|
|
|
|
if self._network_engine.bandwidth_manager.HasRules( network_context ):
|
|
|
|
bandwidth_rules = self._network_engine.bandwidth_manager.GetRules( network_context )
|
|
|
|
else:
|
|
|
|
bandwidth_rules = None
|
|
|
|
|
|
if headers_list is not None or bandwidth_rules is not None:
|
|
|
|
domain_metadata = ClientNetworkingDomain.DomainMetadataPackage( domain = domain, headers_list = headers_list, bandwidth_rules = bandwidth_rules )
|
|
|
|
domain_metadatas.append( domain_metadata )
|
|
|
|
|
|
|
|
for domain_metadata in domain_metadatas:
|
|
|
|
QW.QMessageBox.information( self, 'Information', domain_metadata.GetDetailedSafeSummary() )
|
|
|
|
|
|
return domain_metadatas
|
|
|
|
|
|
def _GetParsersToInclude( self, url_classes ):
|
|
|
|
parsers_to_include = set()
|
|
|
|
for url_class in url_classes:
|
|
|
|
example_url = url_class.GetExampleURL()
|
|
|
|
( url_type, match_name, can_parse, cannot_parse_reason ) = self._network_engine.domain_manager.GetURLParseCapability( example_url )
|
|
|
|
if can_parse:
|
|
|
|
try:
|
|
|
|
( url_to_fetch, parser ) = self._network_engine.domain_manager.GetURLToFetchAndParser( example_url )
|
|
|
|
parsers_to_include.add( parser )
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
existing_data = self._listctrl.GetData()
|
|
|
|
return [ p for p in parsers_to_include if p not in existing_data ]
|
|
|
|
|
|
def _GetURLClassesToInclude( self, gugs ):
|
|
|
|
url_classes_to_include = set()
|
|
|
|
for gug in gugs:
|
|
|
|
if isinstance( gug, ClientNetworkingGUG.GalleryURLGenerator ):
|
|
|
|
example_urls = ( gug.GetExampleURL(), )
|
|
|
|
elif isinstance( gug, ClientNetworkingGUG.NestedGalleryURLGenerator ):
|
|
|
|
example_urls = gug.GetExampleURLs()
|
|
|
|
|
|
for example_url in example_urls:
|
|
|
|
try:
|
|
|
|
url_class = self._network_engine.domain_manager.GetURLClass( example_url )
|
|
|
|
except HydrusExceptions.URLClassException:
|
|
|
|
continue
|
|
|
|
|
|
if url_class is not None:
|
|
|
|
url_classes_to_include.add( url_class )
|
|
|
|
# add post url matches from same domain
|
|
|
|
domain = ClientNetworkingFunctions.ConvertURLIntoSecondLevelDomain( example_url )
|
|
|
|
for um in list( self._network_engine.domain_manager.GetURLClasses() ):
|
|
|
|
if ClientNetworkingFunctions.ConvertURLIntoSecondLevelDomain( um.GetExampleURL() ) == domain and um.GetURLType() in ( HC.URL_TYPE_POST, HC.URL_TYPE_FILE ):
|
|
|
|
url_classes_to_include.add( um )
|
|
|
|
|
|
|
|
|
|
|
|
|
|
existing_data = self._listctrl.GetData()
|
|
|
|
return [ u for u in url_classes_to_include if u not in existing_data ]
|
|
|
|
|
|
class EditContentParserPanel( ClientGUIScrolledPanels.EditPanel ):
|
|
|
|
contentTypeChanged = QC.Signal( int )
|
|
|
|
def __init__( self, parent: QW.QWidget, content_parser: ClientParsing.ContentParser, test_data: ClientParsing.ParsingTestData, permitted_content_types ):
|
|
|
|
self._original_content_parser = content_parser
|
|
|
|
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
|
|
|
#
|
|
|
|
menu_items = []
|
|
|
|
page_func = HydrusData.Call( ClientPaths.LaunchPathInWebBrowser, os.path.join( HC.HELP_DIR, 'downloader_parsers_content_parsers.html#content_parsers' ) )
|
|
|
|
menu_items.append( ( 'normal', 'open the content parsers help', 'Open the help page for content parsers 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 -->', object_name = 'HydrusIndeterminate' )
|
|
|
|
#
|
|
|
|
test_panel = ClientGUICommon.StaticBox( self, 'test' )
|
|
|
|
self._test_panel = ClientGUIParsingTest.TestPanel( test_panel, self.GetValue, test_data = test_data )
|
|
|
|
#
|
|
|
|
self._edit_panel = ClientGUICommon.StaticBox( self, 'edit' )
|
|
|
|
self._name = QW.QLineEdit( self._edit_panel )
|
|
|
|
self._content_panel = ClientGUICommon.StaticBox( self._edit_panel, 'content type' )
|
|
|
|
self._content_type = ClientGUICommon.BetterChoice( self._content_panel )
|
|
|
|
types_to_str = {}
|
|
|
|
types_to_str[ HC.CONTENT_TYPE_URLS ] = 'urls'
|
|
types_to_str[ HC.CONTENT_TYPE_MAPPINGS ] = 'tags'
|
|
types_to_str[ HC.CONTENT_TYPE_NOTES ] = 'notes'
|
|
types_to_str[ HC.CONTENT_TYPE_HASH ] = 'file hash'
|
|
types_to_str[ HC.CONTENT_TYPE_TIMESTAMP ] = 'timestamp'
|
|
types_to_str[ HC.CONTENT_TYPE_TITLE ] = 'watcher title'
|
|
types_to_str[ HC.CONTENT_TYPE_VETO ] = 'veto'
|
|
types_to_str[ HC.CONTENT_TYPE_VARIABLE ] = 'temporary variable'
|
|
|
|
for permitted_content_type in permitted_content_types:
|
|
|
|
self._content_type.addItem( types_to_str[ permitted_content_type ], permitted_content_type )
|
|
|
|
|
|
self._content_type.currentIndexChanged.connect( self.EventContentTypeChange )
|
|
|
|
#
|
|
|
|
self._urls_panel = QW.QWidget( self._content_panel )
|
|
|
|
self._url_type = ClientGUICommon.BetterChoice( self._urls_panel )
|
|
|
|
self._url_type.addItem( 'url to download/pursue (file/post url)', HC.URL_TYPE_DESIRED )
|
|
self._url_type.addItem( 'POST parsers only: url to associate (source url)', HC.URL_TYPE_SOURCE )
|
|
self._url_type.addItem( 'GALLERY parsers only: next gallery page (not queued if no post/file urls found)', HC.URL_TYPE_NEXT )
|
|
self._url_type.addItem( 'GALLERY parsers only: sub-gallery page (is queued even if no post/file urls found--be careful, only use if you know you need it)', HC.URL_TYPE_SUB_GALLERY )
|
|
|
|
self._file_priority = ClientGUICommon.BetterSpinBox( self._urls_panel, min=0, max=100 )
|
|
self._file_priority.setValue( 50 )
|
|
|
|
#
|
|
|
|
self._mappings_panel = QW.QWidget( self._content_panel )
|
|
|
|
self._namespace = QW.QLineEdit( self._mappings_panel )
|
|
|
|
#
|
|
|
|
self._notes_panel = QW.QWidget( self._content_panel )
|
|
|
|
self._note_name = QW.QLineEdit( self._notes_panel )
|
|
|
|
#
|
|
|
|
self._hash_panel = QW.QWidget( self._content_panel )
|
|
|
|
self._hash_type = ClientGUICommon.BetterChoice( self._hash_panel )
|
|
|
|
for hash_type in ( 'md5', 'sha1', 'sha256', 'sha512' ):
|
|
|
|
self._hash_type.addItem( hash_type, hash_type )
|
|
|
|
|
|
self._hash_encoding = ClientGUICommon.BetterChoice( self._hash_panel )
|
|
|
|
for hash_encoding in ( 'hex', 'base64' ):
|
|
|
|
self._hash_encoding.addItem( hash_encoding, hash_encoding )
|
|
|
|
|
|
#
|
|
|
|
self._timestamp_panel = QW.QWidget( self._content_panel )
|
|
|
|
self._timestamp_type = ClientGUICommon.BetterChoice( self._timestamp_panel )
|
|
|
|
self._timestamp_type.addItem( 'source time', HC.TIMESTAMP_TYPE_SOURCE )
|
|
|
|
#
|
|
|
|
self._title_panel = QW.QWidget( self._content_panel )
|
|
|
|
self._title_priority = ClientGUICommon.BetterSpinBox( self._title_panel, min=0, max=100 )
|
|
self._title_priority.setValue( 50 )
|
|
|
|
#
|
|
|
|
self._veto_panel = QW.QWidget( self._content_panel )
|
|
|
|
self._veto_if_matches_found = QW.QCheckBox( self._veto_panel )
|
|
self._string_match = ClientGUIStringPanels.EditStringMatchPanel( self._veto_panel, ClientStrings.StringMatch() )
|
|
|
|
#
|
|
|
|
self._temp_variable_panel = QW.QWidget( self._content_panel )
|
|
|
|
self._temp_variable_name = QW.QLineEdit( self._temp_variable_panel )
|
|
|
|
#
|
|
|
|
( name, content_type, formula, additional_info ) = content_parser.ToTuple()
|
|
|
|
self._formula = ClientGUIParsingFormulae.EditFormulaPanel( self._edit_panel, formula, self._test_panel.GetTestDataForChild )
|
|
|
|
#
|
|
|
|
self._name.setText( name )
|
|
|
|
self._content_type.SetValue( content_type )
|
|
|
|
if content_type == HC.CONTENT_TYPE_URLS:
|
|
|
|
( url_type, priority ) = additional_info
|
|
|
|
self._url_type.SetValue( url_type )
|
|
self._file_priority.setValue( priority )
|
|
|
|
elif content_type == HC.CONTENT_TYPE_MAPPINGS:
|
|
|
|
namespace = additional_info
|
|
|
|
self._namespace.setText( namespace )
|
|
|
|
elif content_type == HC.CONTENT_TYPE_NOTES:
|
|
|
|
note_name = additional_info
|
|
|
|
self._note_name.setText( note_name )
|
|
|
|
elif content_type == HC.CONTENT_TYPE_HASH:
|
|
|
|
( hash_type, hash_encoding ) = additional_info
|
|
|
|
self._hash_type.SetValue( hash_type )
|
|
self._hash_encoding.SetValue( hash_encoding )
|
|
|
|
elif content_type == HC.CONTENT_TYPE_TIMESTAMP:
|
|
|
|
timestamp_type = additional_info
|
|
|
|
self._timestamp_type.SetValue( timestamp_type )
|
|
|
|
elif content_type == HC.CONTENT_TYPE_TITLE:
|
|
|
|
priority = additional_info
|
|
|
|
self._title_priority.setValue( priority )
|
|
|
|
elif content_type == HC.CONTENT_TYPE_VETO:
|
|
|
|
( veto_if_matches_found, string_match ) = additional_info
|
|
|
|
self._veto_if_matches_found.setChecked( veto_if_matches_found )
|
|
self._string_match.SetValue( string_match )
|
|
|
|
elif content_type == HC.CONTENT_TYPE_VARIABLE:
|
|
|
|
temp_variable_name = additional_info
|
|
|
|
self._temp_variable_name.setText( temp_variable_name )
|
|
|
|
|
|
#
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'url type: ', self._url_type ) )
|
|
rows.append( ( 'url quality precedence (higher is better): ', self._file_priority ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( self._urls_panel, rows )
|
|
|
|
self._urls_panel.setLayout( gridbox )
|
|
|
|
#
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'namespace: ', self._namespace ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( self._mappings_panel, rows )
|
|
|
|
self._mappings_panel.setLayout( gridbox )
|
|
|
|
#
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'note name: ', self._note_name ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( self._notes_panel, rows )
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
label = 'Try to make sure you will only ever parse one text result here. A single content parser with a single note name producing eleven different note texts is going to be conflict hell for the user at the end.'
|
|
|
|
st = ClientGUICommon.BetterStaticText( self._notes_panel, label = label )
|
|
|
|
st.setWordWrap( True )
|
|
|
|
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
|
|
self._notes_panel.setLayout( vbox )
|
|
|
|
#
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'hash type: ', self._hash_type ) )
|
|
rows.append( ( 'hash encoding: ', self._hash_encoding ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( self._hash_panel, rows )
|
|
|
|
self._hash_panel.setLayout( gridbox )
|
|
|
|
#
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'timestamp type: ', self._timestamp_type ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( self._timestamp_panel, rows )
|
|
|
|
self._timestamp_panel.setLayout( gridbox )
|
|
|
|
#
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'title precedence (higher is better): ', self._title_priority ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( self._title_panel, rows )
|
|
|
|
self._title_panel.setLayout( gridbox )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'veto if match found (OFF means \'veto if match not found\'): ', self._veto_if_matches_found ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( self._veto_panel, rows )
|
|
|
|
QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._string_match, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self._veto_panel.setLayout( vbox )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'variable name: ', self._temp_variable_name ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( self._temp_variable_panel, rows )
|
|
|
|
QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
|
|
self._temp_variable_panel.setLayout( vbox )
|
|
|
|
#
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'content type: ', self._content_type ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( self._content_panel, rows )
|
|
|
|
self._content_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._content_panel.Add( self._urls_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._content_panel.Add( self._mappings_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._content_panel.Add( self._notes_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._content_panel.Add( self._hash_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._content_panel.Add( self._timestamp_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._content_panel.Add( self._title_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._content_panel.Add( self._veto_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._content_panel.Add( self._temp_variable_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'name or description (optional): ', self._name ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( self._edit_panel, rows )
|
|
|
|
self._edit_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._edit_panel.Add( self._content_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._edit_panel.Add( self._formula, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
#
|
|
|
|
test_panel.Add( self._test_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
#
|
|
|
|
hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( hbox, self._edit_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( hbox, test_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, help_hbox, CC.FLAGS_ON_RIGHT )
|
|
QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
self.EventContentTypeChange( None )
|
|
|
|
|
|
def EventContentTypeChange( self, index ):
|
|
|
|
content_type = self._content_type.GetValue()
|
|
|
|
self._urls_panel.setVisible( False )
|
|
self._mappings_panel.setVisible( False )
|
|
self._notes_panel.setVisible( False )
|
|
self._hash_panel.setVisible( False )
|
|
self._timestamp_panel.setVisible( False )
|
|
self._title_panel.setVisible( False )
|
|
self._veto_panel.setVisible( False )
|
|
self._temp_variable_panel.setVisible( False )
|
|
|
|
collapse_newlines = content_type != HC.CONTENT_TYPE_NOTES
|
|
|
|
self._test_panel.SetCollapseNewlines( collapse_newlines )
|
|
self._formula.SetCollapseNewlines( collapse_newlines )
|
|
|
|
if content_type == HC.CONTENT_TYPE_URLS:
|
|
|
|
self._urls_panel.show()
|
|
|
|
elif content_type == HC.CONTENT_TYPE_MAPPINGS:
|
|
|
|
self._mappings_panel.show()
|
|
|
|
elif content_type == HC.CONTENT_TYPE_NOTES:
|
|
|
|
self._notes_panel.show()
|
|
|
|
elif content_type == HC.CONTENT_TYPE_HASH:
|
|
|
|
self._hash_panel.show()
|
|
|
|
elif content_type == HC.CONTENT_TYPE_TIMESTAMP:
|
|
|
|
self._timestamp_panel.show()
|
|
|
|
elif content_type == HC.CONTENT_TYPE_TITLE:
|
|
|
|
self._title_panel.show()
|
|
|
|
elif content_type == HC.CONTENT_TYPE_VETO:
|
|
|
|
self._veto_panel.show()
|
|
|
|
elif content_type == HC.CONTENT_TYPE_VARIABLE:
|
|
|
|
self._temp_variable_panel.show()
|
|
|
|
|
|
self.contentTypeChanged.emit( content_type )
|
|
|
|
|
|
def GetValue( self ):
|
|
|
|
name = self._name.text()
|
|
|
|
content_type = self._content_type.GetValue()
|
|
|
|
formula = self._formula.GetValue()
|
|
|
|
if content_type == HC.CONTENT_TYPE_URLS:
|
|
|
|
url_type = self._url_type.GetValue()
|
|
priority = self._file_priority.value()
|
|
|
|
additional_info = ( url_type, priority )
|
|
|
|
elif content_type == HC.CONTENT_TYPE_MAPPINGS:
|
|
|
|
namespace = self._namespace.text()
|
|
|
|
additional_info = namespace
|
|
|
|
elif content_type == HC.CONTENT_TYPE_NOTES:
|
|
|
|
note_name = self._note_name.text()
|
|
|
|
if note_name == '':
|
|
|
|
note_name = 'note'
|
|
|
|
|
|
additional_info = note_name
|
|
|
|
elif content_type == HC.CONTENT_TYPE_HASH:
|
|
|
|
hash_type = self._hash_type.GetValue()
|
|
hash_encoding = self._hash_encoding.GetValue()
|
|
|
|
additional_info = ( hash_type, hash_encoding )
|
|
|
|
elif content_type == HC.CONTENT_TYPE_TIMESTAMP:
|
|
|
|
timestamp_type = self._timestamp_type.GetValue()
|
|
|
|
additional_info = timestamp_type
|
|
|
|
elif content_type == HC.CONTENT_TYPE_TITLE:
|
|
|
|
priority = self._title_priority.value()
|
|
|
|
additional_info = priority
|
|
|
|
elif content_type == HC.CONTENT_TYPE_VETO:
|
|
|
|
veto_if_matches_found = self._veto_if_matches_found.isChecked()
|
|
string_match = self._string_match.GetValue()
|
|
|
|
additional_info = ( veto_if_matches_found, string_match )
|
|
|
|
elif content_type == HC.CONTENT_TYPE_VARIABLE:
|
|
|
|
temp_variable_name = self._temp_variable_name.text()
|
|
|
|
additional_info = temp_variable_name
|
|
|
|
|
|
content_parser = ClientParsing.ContentParser( name = name, content_type = content_type, formula = formula, additional_info = additional_info )
|
|
|
|
return content_parser
|
|
|
|
|
|
def UserIsOKToCancel( self ):
|
|
|
|
if self._original_content_parser.GetSerialisableTuple() != self.GetValue().GetSerialisableTuple():
|
|
|
|
text = 'It looks like you have made changes to the content parser--are you sure you want to cancel?'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, text )
|
|
|
|
return result == QW.QDialog.Accepted
|
|
|
|
else:
|
|
|
|
return True
|
|
|
|
|
|
|
|
class EditContentParsersPanel( ClientGUICommon.StaticBox ):
|
|
|
|
def __init__( self, parent: QW.QWidget, test_data_callable: typing.Callable[ [], ClientParsing.ParsingTestData ], permitted_content_types ):
|
|
|
|
ClientGUICommon.StaticBox.__init__( self, parent, 'content parsers' )
|
|
|
|
self._test_data_callable = test_data_callable
|
|
self._permitted_content_types = permitted_content_types
|
|
|
|
content_parsers_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
|
|
|
|
self._content_parsers = ClientGUIListCtrl.BetterListCtrl( content_parsers_panel, CGLC.COLUMN_LIST_CONTENT_PARSERS.ID, 6, self._ConvertContentParserToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )
|
|
|
|
content_parsers_panel.SetListCtrl( self._content_parsers )
|
|
|
|
content_parsers_panel.AddButton( 'add', self._Add )
|
|
content_parsers_panel.AddButton( 'edit', self._Edit, enabled_only_on_selection = True )
|
|
content_parsers_panel.AddDeleteButton()
|
|
content_parsers_panel.AddSeparator()
|
|
content_parsers_panel.AddImportExportButtons( ( ClientParsing.ContentParser, ), self._AddContentParser )
|
|
|
|
#
|
|
|
|
self.Add( content_parsers_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
|
|
def _Add( self ):
|
|
|
|
dlg_title = 'edit content node'
|
|
|
|
test_data = self._test_data_callable()
|
|
|
|
if test_data.LooksLikeJSON():
|
|
|
|
formula = ClientParsing.ParseFormulaJSON()
|
|
|
|
else:
|
|
|
|
formula = ClientParsing.ParseFormulaHTML()
|
|
|
|
|
|
content_parser = ClientParsing.ContentParser( 'new content parser', formula = formula )
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit content parser', frame_key = 'deeply_nested_dialog' ) as dlg_edit:
|
|
|
|
panel = EditContentParserPanel( dlg_edit, content_parser, test_data, self._permitted_content_types )
|
|
|
|
dlg_edit.SetPanel( panel )
|
|
|
|
if dlg_edit.exec() == QW.QDialog.Accepted:
|
|
|
|
new_content_parser = panel.GetValue()
|
|
|
|
self._AddContentParser( new_content_parser )
|
|
|
|
|
|
|
|
|
|
def _AddContentParser( self, content_parser ):
|
|
|
|
HydrusSerialisable.SetNonDupeName( content_parser, self._GetExistingNames() )
|
|
|
|
self._content_parsers.AddDatas( ( content_parser, ) )
|
|
|
|
self._content_parsers.Sort()
|
|
|
|
|
|
def _ConvertContentParserToListCtrlTuples( self, content_parser ):
|
|
|
|
name = content_parser.GetName()
|
|
|
|
produces = list( content_parser.GetParsableContent() )
|
|
|
|
pretty_name = name
|
|
|
|
pretty_produces = ClientParsing.ConvertParsableContentToPrettyString( produces, include_veto = True )
|
|
|
|
# produces has some garbage stuff like StringMatch that doesn't sort nice, so sort on pretty produces
|
|
|
|
display_tuple = ( pretty_name, pretty_produces )
|
|
sort_tuple = ( name, pretty_produces )
|
|
|
|
return ( display_tuple, sort_tuple )
|
|
|
|
|
|
def _Edit( self ):
|
|
|
|
edited_datas = []
|
|
|
|
content_parsers = self._content_parsers.GetData( only_selected = True )
|
|
|
|
for content_parser in content_parsers:
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit content parser', frame_key = 'deeply_nested_dialog' ) as dlg:
|
|
|
|
test_data = self._test_data_callable()
|
|
|
|
panel = EditContentParserPanel( dlg, content_parser, test_data, self._permitted_content_types )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
edited_content_parser = panel.GetValue()
|
|
|
|
self._content_parsers.DeleteDatas( ( content_parser, ) )
|
|
|
|
HydrusSerialisable.SetNonDupeName( edited_content_parser, self._GetExistingNames() )
|
|
|
|
self._content_parsers.AddDatas( ( edited_content_parser, ) )
|
|
|
|
edited_datas.append( edited_content_parser )
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
self._content_parsers.SelectDatas( edited_datas )
|
|
|
|
self._content_parsers.Sort()
|
|
|
|
|
|
def _GetExistingNames( self ):
|
|
|
|
names = { content_parser.GetName() for content_parser in self._content_parsers.GetData() }
|
|
|
|
return names
|
|
|
|
|
|
def GetData( self ):
|
|
|
|
return self._content_parsers.GetData()
|
|
|
|
|
|
def AddDatas( self, content_parsers ):
|
|
|
|
self._content_parsers.AddDatas( content_parsers )
|
|
|
|
self._content_parsers.Sort()
|
|
|
|
|
|
class EditPageParserPanel( ClientGUIScrolledPanels.EditPanel ):
|
|
|
|
def __init__( self, parent, parser: ClientParsing.PageParser, formula = None, test_data = None ):
|
|
|
|
self._original_parser = parser
|
|
|
|
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
|
|
|
if test_data is None:
|
|
|
|
example_parsing_context = parser.GetExampleParsingContext()
|
|
example_data = ''
|
|
|
|
test_data = ClientParsing.ParsingTestData( example_parsing_context, ( example_data, ) )
|
|
|
|
|
|
#
|
|
|
|
menu_items = []
|
|
|
|
page_func = HydrusData.Call( ClientPaths.LaunchPathInWebBrowser, os.path.join( HC.HELP_DIR, 'downloader_parsers_page_parsers.html#page_parsers' ) )
|
|
|
|
menu_items.append( ( 'normal', 'open the page parser help', 'Open the help page for page parsers 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 -->', object_name = 'HydrusIndeterminate' )
|
|
|
|
#
|
|
|
|
edit_panel = ClientGUICommon.StaticBox( self, 'edit' )
|
|
|
|
edit_notebook = QW.QTabWidget( edit_panel )
|
|
|
|
#
|
|
|
|
main_panel = QW.QWidget( edit_notebook )
|
|
|
|
self._name = QW.QLineEdit( main_panel )
|
|
|
|
#
|
|
|
|
conversion_panel = ClientGUICommon.StaticBox( main_panel, 'pre-parsing conversion' )
|
|
|
|
string_converter = parser.GetStringConverter()
|
|
|
|
self._string_converter = ClientGUIStringControls.StringConverterButton( conversion_panel, string_converter )
|
|
|
|
#
|
|
|
|
test_panel = ClientGUICommon.StaticBox( self, 'test' )
|
|
|
|
test_url_fetch_panel = ClientGUICommon.StaticBox( test_panel, 'fetch test data from url' )
|
|
|
|
self._test_url = QW.QLineEdit( test_url_fetch_panel )
|
|
self._test_referral_url = QW.QLineEdit( test_url_fetch_panel )
|
|
self._fetch_example_data = ClientGUICommon.BetterButton( test_url_fetch_panel, 'fetch test data from url', self._FetchExampleData )
|
|
self._test_network_job_control = ClientGUINetworkJobControl.NetworkJobControl( test_url_fetch_panel )
|
|
|
|
if formula is None:
|
|
|
|
self._test_panel = ClientGUIParsingTest.TestPanelPageParser( test_panel, self.GetValue, self._string_converter.GetValue, test_data = test_data )
|
|
|
|
else:
|
|
|
|
self._test_panel = ClientGUIParsingTest.TestPanelPageParserSubsidiary( test_panel, self.GetValue, self._string_converter.GetValue, self.GetFormula, test_data = test_data )
|
|
|
|
self._test_panel.SetCollapseNewlines( False )
|
|
|
|
|
|
#
|
|
|
|
example_urls_panel = ClientGUICommon.StaticBox( main_panel, 'example urls' )
|
|
|
|
self._example_urls = ClientGUIListBoxes.AddEditDeleteListBox( example_urls_panel, 6, str, self._AddExampleURL, self._EditExampleURL )
|
|
|
|
#
|
|
|
|
formula_panel = QW.QWidget( edit_notebook )
|
|
|
|
self._formula = ClientGUIParsingFormulae.EditFormulaPanel( formula_panel, formula, self._test_panel.GetTestData )
|
|
|
|
self._formula.SetCollapseNewlines( False )
|
|
|
|
#
|
|
|
|
sub_page_parsers_notebook_panel = QW.QWidget( edit_notebook )
|
|
|
|
#
|
|
|
|
sub_page_parsers_panel = ClientGUIListCtrl.BetterListCtrlPanel( sub_page_parsers_notebook_panel )
|
|
|
|
self._sub_page_parsers = ClientGUIListCtrl.BetterListCtrl( sub_page_parsers_panel, CGLC.COLUMN_LIST_SUB_PAGE_PARSERS.ID, 4, self._ConvertSubPageParserToListCtrlTuples, use_simple_delete = True, activation_callback = self._EditSubPageParser )
|
|
|
|
sub_page_parsers_panel.SetListCtrl( self._sub_page_parsers )
|
|
|
|
sub_page_parsers_panel.AddButton( 'add', self._AddSubPageParser )
|
|
sub_page_parsers_panel.AddButton( 'edit', self._EditSubPageParser, enabled_only_on_selection = True )
|
|
sub_page_parsers_panel.AddDeleteButton()
|
|
|
|
#
|
|
|
|
content_parsers_panel = QW.QWidget( edit_notebook )
|
|
|
|
#
|
|
|
|
permitted_content_types = [ HC.CONTENT_TYPE_URLS, HC.CONTENT_TYPE_MAPPINGS, HC.CONTENT_TYPE_NOTES, HC.CONTENT_TYPE_HASH, HC.CONTENT_TYPE_TIMESTAMP, HC.CONTENT_TYPE_TITLE, HC.CONTENT_TYPE_VETO ]
|
|
|
|
self._content_parsers = EditContentParsersPanel( content_parsers_panel, self._test_panel.GetTestDataForChild, permitted_content_types )
|
|
|
|
#
|
|
|
|
name = parser.GetName()
|
|
|
|
( sub_page_parsers, content_parsers ) = parser.GetContentParsers()
|
|
|
|
example_urls = parser.GetExampleURLs()
|
|
|
|
if len( example_urls ) > 0:
|
|
|
|
self._test_url.setText( example_urls[0] )
|
|
|
|
|
|
self._name.setText( name )
|
|
|
|
self._sub_page_parsers.AddDatas( sub_page_parsers )
|
|
|
|
self._sub_page_parsers.Sort()
|
|
|
|
self._content_parsers.AddDatas( content_parsers )
|
|
|
|
self._example_urls.AddDatas( example_urls )
|
|
|
|
#
|
|
|
|
st = ClientGUICommon.BetterStaticText( conversion_panel, 'If the data this parser gets is wrapped in some quote marks or is otherwise encoded,\nyou can convert it to neat HTML/JSON first with this.' )
|
|
|
|
conversion_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
conversion_panel.Add( self._string_converter, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
example_urls_panel.Add( self._example_urls, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'name or description (optional): ', self._name ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( main_panel, rows )
|
|
|
|
QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, conversion_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, example_urls_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
main_panel.setLayout( vbox )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._formula, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
formula_panel.setLayout( vbox )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, sub_page_parsers_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
sub_page_parsers_notebook_panel.setLayout( vbox )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._content_parsers, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
content_parsers_panel.setLayout( vbox )
|
|
|
|
#
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'url: ', self._test_url ) )
|
|
rows.append( ( 'referral url (optional): ', self._test_referral_url ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( test_url_fetch_panel, rows )
|
|
|
|
test_url_fetch_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
test_url_fetch_panel.Add( self._fetch_example_data, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
test_url_fetch_panel.Add( self._test_network_job_control, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
test_panel.Add( test_url_fetch_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
test_panel.Add( self._test_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
if formula is not None:
|
|
|
|
test_url_fetch_panel.hide()
|
|
|
|
|
|
#
|
|
|
|
if formula is None:
|
|
|
|
formula_panel.setVisible( False )
|
|
|
|
else:
|
|
|
|
example_urls_panel.hide()
|
|
edit_notebook.addTab( formula_panel, 'separation formula' )
|
|
|
|
|
|
edit_notebook.addTab( main_panel, 'main' )
|
|
edit_notebook.setCurrentWidget( main_panel )
|
|
edit_notebook.addTab( sub_page_parsers_notebook_panel, 'subsidiary page parsers' )
|
|
edit_notebook.addTab( content_parsers_panel, 'content parsers' )
|
|
|
|
edit_panel.Add( edit_notebook, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
#
|
|
|
|
hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( hbox, edit_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( hbox, test_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, help_hbox, CC.FLAGS_ON_RIGHT )
|
|
QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
|
|
def _AddExampleURL( self ):
|
|
|
|
url = ''
|
|
|
|
return self._EditExampleURL( url )
|
|
|
|
|
|
def _AddSubPageParser( self ):
|
|
|
|
formula = ClientParsing.ParseFormulaHTML( tag_rules = [ ClientParsing.ParseRuleHTML( rule_type = ClientParsing.HTML_RULE_TYPE_DESCENDING, tag_name = 'div', tag_attributes = { 'class' : 'thumb' } ) ], content_to_fetch = ClientParsing.HTML_CONTENT_HTML )
|
|
page_parser = ClientParsing.PageParser( 'new sub page parser' )
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit sub page parser', frame_key = 'deeply_nested_dialog' ) as dlg:
|
|
|
|
panel = EditPageParserPanel( dlg, page_parser, formula = formula, test_data = self._test_panel.GetTestDataForChild() )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
new_page_parser = panel.GetValue()
|
|
|
|
new_formula = panel.GetFormula()
|
|
|
|
new_sub_page_parser = ( new_formula, new_page_parser )
|
|
|
|
self._sub_page_parsers.AddDatas( ( new_sub_page_parser, ) )
|
|
|
|
self._sub_page_parsers.Sort()
|
|
|
|
|
|
|
|
|
|
def _ConvertSubPageParserToListCtrlTuples( self, sub_page_parser ):
|
|
|
|
( formula, page_parser ) = sub_page_parser
|
|
|
|
name = page_parser.GetName()
|
|
|
|
produces = page_parser.GetParsableContent()
|
|
|
|
produces = sorted( produces, key = lambda row: ( row[0], row[1] ) ) # ( name, content_type ), ignores potentially unsortable StringMatch etc.. in additional info in case of dupe
|
|
|
|
pretty_name = name
|
|
pretty_formula = formula.ToPrettyString()
|
|
pretty_produces = ClientParsing.ConvertParsableContentToPrettyString( produces )
|
|
|
|
display_tuple = ( pretty_name, pretty_formula, pretty_produces )
|
|
sort_tuple = ( name, pretty_formula, pretty_produces )
|
|
|
|
return ( display_tuple, sort_tuple )
|
|
|
|
|
|
def _EditExampleURL( self, example_url ):
|
|
|
|
message = 'Enter example URL.'
|
|
|
|
with ClientGUIDialogs.DialogTextEntry( self, message, default = example_url ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
return dlg.GetValue()
|
|
|
|
else:
|
|
|
|
raise HydrusExceptions.VetoException()
|
|
|
|
|
|
|
|
|
|
def _EditSubPageParser( self ):
|
|
|
|
edited_datas = []
|
|
|
|
selected_data = self._sub_page_parsers.GetData( only_selected = True )
|
|
|
|
for sub_page_parser in selected_data:
|
|
|
|
( formula, page_parser ) = sub_page_parser
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit sub page parser', frame_key = 'deeply_nested_dialog' ) as dlg:
|
|
|
|
panel = EditPageParserPanel( dlg, page_parser, formula = formula, test_data = self._test_panel.GetTestDataForChild() )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
self._sub_page_parsers.DeleteDatas( ( sub_page_parser, ) )
|
|
|
|
new_page_parser = panel.GetValue()
|
|
|
|
new_formula = panel.GetFormula()
|
|
|
|
new_sub_page_parser = ( new_formula, new_page_parser )
|
|
|
|
self._sub_page_parsers.AddDatas( ( new_sub_page_parser, ) )
|
|
|
|
edited_datas.append( new_sub_page_parser )
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
self._sub_page_parsers.SelectDatas( edited_datas )
|
|
|
|
self._sub_page_parsers.Sort()
|
|
|
|
|
|
def _FetchExampleData( self ):
|
|
|
|
def wait_and_do_it( network_job ):
|
|
|
|
def qt_tidy_up( example_data, example_bytes, error ):
|
|
|
|
if not self or not QP.isValid( self ):
|
|
|
|
return
|
|
|
|
|
|
example_parsing_context = self._test_panel.GetExampleParsingContext()
|
|
|
|
example_parsing_context[ 'url' ] = url
|
|
example_parsing_context[ 'post_index' ] = '0'
|
|
|
|
self._test_panel.SetExampleParsingContext( example_parsing_context )
|
|
|
|
self._test_panel.SetExampleData( example_data, example_bytes = example_bytes )
|
|
|
|
self._test_network_job_control.ClearNetworkJob()
|
|
|
|
if error is not None:
|
|
|
|
self._test_network_job_control.SetError( error )
|
|
|
|
|
|
|
|
example_bytes = None
|
|
error = None
|
|
|
|
try:
|
|
|
|
network_job.WaitUntilDone()
|
|
|
|
example_data = network_job.GetContentText()
|
|
|
|
example_bytes = network_job.GetContentBytes()
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
example_data = 'fetch cancelled'
|
|
|
|
except Exception as e:
|
|
|
|
error = traceback.format_exc()
|
|
|
|
try:
|
|
|
|
stuff_read = network_job.GetContentText()
|
|
|
|
except:
|
|
|
|
stuff_read = 'no response'
|
|
|
|
|
|
example_data = 'fetch failed: {}'.format( e ) + os.linesep * 2 + stuff_read
|
|
|
|
|
|
QP.CallAfter( qt_tidy_up, example_data, example_bytes, error )
|
|
|
|
|
|
url = self._test_url.text()
|
|
referral_url = self._test_referral_url.text()
|
|
|
|
if referral_url == '':
|
|
|
|
referral_url = None
|
|
|
|
|
|
network_job = ClientNetworkingJobs.NetworkJob( 'GET', url, referral_url = referral_url )
|
|
|
|
network_job.OnlyTryConnectionOnce()
|
|
|
|
self._test_network_job_control.ClearError()
|
|
self._test_network_job_control.SetNetworkJob( network_job )
|
|
|
|
network_job.OverrideBandwidth()
|
|
|
|
HG.client_controller.network_engine.AddJob( network_job )
|
|
|
|
HG.client_controller.CallToThread( wait_and_do_it, network_job )
|
|
|
|
|
|
def GetFormula( self ):
|
|
|
|
return self._formula.GetValue()
|
|
|
|
|
|
def GetValue( self ):
|
|
|
|
name = self._name.text()
|
|
|
|
parser_key = self._original_parser.GetParserKey()
|
|
|
|
string_converter = self._string_converter.GetValue()
|
|
|
|
sub_page_parsers = self._sub_page_parsers.GetData()
|
|
|
|
content_parsers = self._content_parsers.GetData()
|
|
|
|
example_urls = self._example_urls.GetData()
|
|
|
|
example_parsing_context = self._test_panel.GetExampleParsingContext()
|
|
|
|
parser = ClientParsing.PageParser( name, parser_key = parser_key, string_converter = string_converter, sub_page_parsers = sub_page_parsers, content_parsers = content_parsers, example_urls = example_urls, example_parsing_context = example_parsing_context )
|
|
|
|
return parser
|
|
|
|
|
|
def UserIsOKToCancel( self ):
|
|
|
|
original_parser = self._original_parser.Duplicate()
|
|
current_parser = self.GetValue()
|
|
|
|
original_parser.NullifyTestData()
|
|
current_parser.NullifyTestData()
|
|
|
|
if original_parser.GetSerialisableTuple() != current_parser.GetSerialisableTuple():
|
|
|
|
text = 'It looks like you have made changes to the parser--are you sure you want to cancel?'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, text )
|
|
|
|
return result == QW.QDialog.Accepted
|
|
|
|
else:
|
|
|
|
return True
|
|
|
|
|
|
|
|
class EditParsersPanel( ClientGUIScrolledPanels.EditPanel ):
|
|
|
|
def __init__( self, parent, parsers ):
|
|
|
|
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
|
|
|
parsers_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
|
|
|
|
self._parsers = ClientGUIListCtrl.BetterListCtrl( parsers_panel, CGLC.COLUMN_LIST_PARSERS.ID, 20, self._ConvertParserToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )
|
|
|
|
parsers_panel.SetListCtrl( self._parsers )
|
|
|
|
parsers_panel.AddButton( 'add', self._Add )
|
|
parsers_panel.AddButton( 'edit', self._Edit, enabled_only_on_selection = True )
|
|
parsers_panel.AddDeleteButton()
|
|
parsers_panel.AddSeparator()
|
|
parsers_panel.AddImportExportButtons( ( ClientParsing.PageParser, ), self._AddParser )
|
|
parsers_panel.AddSeparator()
|
|
parsers_panel.AddDefaultsButton( ClientDefaults.GetDefaultParsers, self._AddParser )
|
|
|
|
#
|
|
|
|
self._parsers.AddDatas( parsers )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, parsers_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
|
|
def _Add( self ):
|
|
|
|
new_parser = ClientParsing.PageParser( 'new page parser' )
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit parser', frame_key = 'deeply_nested_dialog' ) as dlg_edit:
|
|
|
|
panel = EditPageParserPanel( dlg_edit, new_parser )
|
|
|
|
dlg_edit.SetPanel( panel )
|
|
|
|
if dlg_edit.exec() == QW.QDialog.Accepted:
|
|
|
|
new_parser = panel.GetValue()
|
|
|
|
self._AddParser( new_parser )
|
|
|
|
self._parsers.Sort()
|
|
|
|
|
|
|
|
|
|
def _AddParser( self, parser ):
|
|
|
|
HydrusSerialisable.SetNonDupeName( parser, self._GetExistingNames() )
|
|
|
|
parser.RegenerateParserKey()
|
|
|
|
self._parsers.AddDatas( ( parser, ) )
|
|
|
|
|
|
def _ConvertParserToListCtrlTuples( self, parser ):
|
|
|
|
name = parser.GetName()
|
|
|
|
example_urls = sorted( parser.GetExampleURLs() )
|
|
|
|
produces = parser.GetParsableContent()
|
|
|
|
pretty_produces = ClientParsing.ConvertParsableContentToPrettyString( produces )
|
|
|
|
sort_produces = pretty_produces
|
|
|
|
pretty_name = name
|
|
pretty_example_urls = ', '.join( example_urls )
|
|
|
|
display_tuple = ( pretty_name, pretty_example_urls, pretty_produces )
|
|
sort_tuple = ( name, example_urls, sort_produces )
|
|
|
|
return ( display_tuple, sort_tuple )
|
|
|
|
|
|
def _Edit( self ):
|
|
|
|
edited_datas = []
|
|
|
|
parsers = self._parsers.GetData( only_selected = True )
|
|
|
|
for parser in parsers:
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit parser', frame_key = 'deeply_nested_dialog' ) as dlg:
|
|
|
|
panel = EditPageParserPanel( dlg, parser )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
edited_parser = panel.GetValue()
|
|
|
|
self._parsers.DeleteDatas( ( parser, ) )
|
|
|
|
HydrusSerialisable.SetNonDupeName( edited_parser, self._GetExistingNames() )
|
|
|
|
self._parsers.AddDatas( ( edited_parser, ) )
|
|
|
|
edited_datas.append( edited_parser )
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
self._parsers.SelectDatas( edited_datas )
|
|
|
|
self._parsers.Sort()
|
|
|
|
|
|
def _GetExistingNames( self ):
|
|
|
|
names = { parser.GetName() for parser in self._parsers.GetData() }
|
|
|
|
return names
|
|
|
|
|
|
def GetValue( self ):
|
|
|
|
return self._parsers.GetData()
|
|
|