hydrus/hydrus/client/gui/metadata/ClientGUIMetadataMigrationE...

441 lines
18 KiB
Python

import os
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusText
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientParsing
from hydrus.client.gui import ClientGUIDialogs
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIScrolledPanels
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.metadata import ClientGUIMetadataMigrationCommon
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.metadata import ClientMetadataMigrationExporters
choice_tuple_label_lookup = {
ClientMetadataMigrationExporters.SingleFileMetadataExporterMediaTags : 'a file\'s tags',
ClientMetadataMigrationExporters.SingleFileMetadataExporterMediaURLs : 'a file\'s URLs',
ClientMetadataMigrationExporters.SingleFileMetadataExporterTXT : 'a .txt sidecar',
ClientMetadataMigrationExporters.SingleFileMetadataExporterJSON : 'a .json sidecar'
}
choice_tuple_description_lookup = {
ClientMetadataMigrationExporters.SingleFileMetadataExporterMediaTags : 'The tags that a file has on a particular service.',
ClientMetadataMigrationExporters.SingleFileMetadataExporterMediaURLs : 'The known URLs that a file has.',
ClientMetadataMigrationExporters.SingleFileMetadataExporterTXT : 'A list of raw newline-separated texts in a .txt file.',
ClientMetadataMigrationExporters.SingleFileMetadataExporterJSON : 'Strings somewhere in a JSON file.'
}
def SelectClass( win: QW.QWidget, allowed_exporter_classes: list ):
choice_tuples = [ ( choice_tuple_label_lookup[ c ], c, choice_tuple_description_lookup[ c ] ) for c in allowed_exporter_classes ]
message = 'Which kind of destination are we going to use?'
exporter_class = ClientGUIDialogsQuick.SelectFromListButtons( win, 'Which type?', choice_tuples, message = message )
return exporter_class
class EditSingleFileMetadataExporterPanel( ClientGUIScrolledPanels.EditPanel ):
def __init__( self, parent: QW.QWidget, exporter: ClientMetadataMigrationExporters.SingleFileMetadataExporter, allowed_exporter_classes: list ):
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
self._original_exporter = exporter
self._allowed_exporter_classes = allowed_exporter_classes
self._current_exporter_class = type( exporter )
self._service_key = CC.DEFAULT_LOCAL_TAG_SERVICE_KEY
#
self._change_type_button = ClientGUICommon.BetterButton( self, 'change type', self._ChangeType )
#
self._service_selection_panel = QW.QWidget( self )
self._service_selection_button = ClientGUICommon.BetterButton( self._service_selection_panel, 'service', self._SelectService )
hbox = ClientGUICommon.WrapInText( self._service_selection_button, self._service_selection_panel, 'tag service: ' )
self._service_selection_panel.setLayout( hbox )
#
self._sidecar_help_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().help, self._ShowSidecarHelp )
self._nested_object_names_panel = QW.QWidget( self )
self._nested_object_names_list = ClientGUIListBoxes.QueueListBox( self, 4, str, self._AddObjectName, self._EditObjectName )
tt = 'If you leave this empty, the strings will be exported as a simple list. If you set it as [files,tags], the exported string list will be placed under nested objects with keys "files"->"tags". Note that this will also update an existing file, so, if you are feeling clever, you can have multiple routers writing tags and URLs to different destinations in the same file!'
self._nested_object_names_list.setToolTip( tt )
vbox = QP.VBoxLayout()
message = 'JSON Objects structure'
st = ClientGUICommon.BetterStaticText( self._nested_object_names_panel, message )
st.setToolTip( self._nested_object_names_list.toolTip() )
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._nested_object_names_list, CC.FLAGS_EXPAND_BOTH_WAYS )
self._nested_object_names_panel.setLayout( vbox )
#
self._txt_separator_panel = ClientGUIMetadataMigrationCommon.EditSidecarTXTSeparator( self )
#
self._sidecar_panel = ClientGUIMetadataMigrationCommon.EditSidecarDetailsPanel( self )
#
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._change_type_button, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._service_selection_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._sidecar_help_button, CC.FLAGS_ON_RIGHT )
QP.AddToLayout( vbox, self._nested_object_names_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( vbox, self._txt_separator_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._sidecar_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.addStretch( 1 )
self.widget().setLayout( vbox )
self._SetValue( exporter )
def _AddObjectName( self ):
object_name = ''
return self._EditObjectName( object_name )
def _ChangeType( self ):
allowed_exporter_classes = list( self._allowed_exporter_classes )
if self._current_exporter_class in allowed_exporter_classes:
allowed_exporter_classes.remove( self._current_exporter_class )
if len( allowed_exporter_classes ) == 0:
message = 'Sorry, you can only have this one!'
QW.QMessageBox.information( self, 'Information', message )
try:
exporter_class = SelectClass( self, allowed_exporter_classes )
except HydrusExceptions.CancelledException:
return
exporter = exporter_class()
# it is nice to preserve old values as we flip from one type to another. more pleasant than making the user cancel and re-open
if isinstance( exporter, ClientMetadataMigrationExporters.SingleFileMetadataExporterSidecar ):
remove_actual_filename_ext = self._sidecar_panel.GetRemoveActualFilenameExt()
suffix = self._sidecar_panel.GetSuffix()
filename_string_converter = self._sidecar_panel.GetFilenameStringConverter()
exporter.SetRemoveActualFilenameExt( remove_actual_filename_ext )
exporter.SetSuffix( suffix )
exporter.SetFilenameStringConverter( filename_string_converter )
if isinstance( exporter, ClientMetadataMigrationExporters.SingleFileMetadataExporterMediaTags ):
exporter.SetServiceKey( self._service_key )
elif isinstance( exporter, ClientMetadataMigrationExporters.SingleFileMetadataExporterMediaURLs ):
pass
elif isinstance( exporter, ClientMetadataMigrationExporters.SingleFileMetadataExporterTXT ):
exporter.SetSeparator( self._txt_separator_panel.GetValue() )
elif isinstance( exporter, ClientMetadataMigrationExporters.SingleFileMetadataExporterJSON ):
exporter.SetNestedObjectNames( self._nested_object_names_list.GetData() )
self._SetValue( exporter )
def _EditObjectName( self, object_name ):
with ClientGUIDialogs.DialogTextEntry( self, 'enter the JSON Object name', default = object_name, allow_blank = False ) as dlg:
if dlg.exec() == QW.QDialog.Accepted:
object_name = dlg.GetValue()
return object_name
else:
raise HydrusExceptions.VetoException()
def _GetExampleTestData( self ):
example_parsing_context = dict()
exporter = self._GetValue()
texts = sorted( exporter.GetExampleStrings() )
return ClientParsing.ParsingTestData( example_parsing_context, texts )
def _GetValue( self ) -> ClientMetadataMigrationExporters.SingleFileMetadataExporter:
if self._current_exporter_class == ClientMetadataMigrationExporters.SingleFileMetadataExporterMediaTags:
try:
HG.client_controller.services_manager.GetName( self._service_key )
except HydrusExceptions.DataMissing:
raise HydrusExceptions.VetoException( 'Sorry, your exporter needs a valid tag service! The selected one is missing!' )
exporter = ClientMetadataMigrationExporters.SingleFileMetadataExporterMediaTags( service_key = self._service_key )
elif self._current_exporter_class == ClientMetadataMigrationExporters.SingleFileMetadataExporterMediaURLs:
exporter = ClientMetadataMigrationExporters.SingleFileMetadataExporterMediaURLs()
elif self._current_exporter_class == ClientMetadataMigrationExporters.SingleFileMetadataExporterTXT:
remove_actual_filename_ext = self._sidecar_panel.GetRemoveActualFilenameExt()
suffix = self._sidecar_panel.GetSuffix()
filename_string_converter = self._sidecar_panel.GetFilenameStringConverter()
separator = self._txt_separator_panel.GetValue()
exporter = ClientMetadataMigrationExporters.SingleFileMetadataExporterTXT( remove_actual_filename_ext = remove_actual_filename_ext, suffix = suffix, filename_string_converter = filename_string_converter, separator = separator )
elif self._current_exporter_class == ClientMetadataMigrationExporters.SingleFileMetadataExporterJSON:
remove_actual_filename_ext = self._sidecar_panel.GetRemoveActualFilenameExt()
suffix = self._sidecar_panel.GetSuffix()
filename_string_converter = self._sidecar_panel.GetFilenameStringConverter()
nested_object_names = self._nested_object_names_list.GetData()
exporter = ClientMetadataMigrationExporters.SingleFileMetadataExporterJSON( remove_actual_filename_ext = remove_actual_filename_ext, suffix = suffix, filename_string_converter = filename_string_converter, nested_object_names = nested_object_names )
else:
raise Exception( 'Did not understand the current exporter type!' )
return exporter
def _SelectService( self ):
service_key = ClientGUIDialogsQuick.SelectServiceKey( service_types = HC.REAL_TAG_SERVICES, unallowed = [ self._service_key ] )
if service_key is None:
return
self._service_key = service_key
self._UpdateServiceKeyButtonLabel()
def _SetValue( self, exporter: ClientMetadataMigrationExporters.SingleFileMetadataExporter ):
self._current_exporter_class = type( exporter )
self._change_type_button.setText( choice_tuple_label_lookup[ self._current_exporter_class ] )
self._service_selection_panel.setVisible( False )
self._sidecar_help_button.setVisible( False )
self._nested_object_names_panel.setVisible( False )
self._txt_separator_panel.setVisible( False )
self._sidecar_panel.setVisible( False )
if isinstance( exporter, ClientMetadataMigrationExporters.SingleFileMetadataExporterSidecar ):
self._sidecar_help_button.setVisible( True )
remove_actual_filename_ext = exporter.GetRemoveActualFilenameExt()
suffix = exporter.GetSuffix()
filename_string_converter = exporter.GetFilenameStringConverter()
self._sidecar_panel.SetRemoveActualFilenameExt( remove_actual_filename_ext )
self._sidecar_panel.SetSuffix( suffix )
self._sidecar_panel.SetFilenameStringConverter( filename_string_converter )
self._sidecar_panel.setVisible( True )
if isinstance( exporter, ClientMetadataMigrationExporters.SingleFileMetadataExporterMediaTags ):
self._service_key = exporter.GetServiceKey()
self._UpdateServiceKeyButtonLabel()
self._service_selection_panel.setVisible( True )
if not HG.client_controller.services_manager.ServiceExists( self._service_key ):
message = 'Hey, the tag service for your exporter does not seem to exist! Maybe it was deleted. Please select a new one that does.'
QW.QMessageBox.warning( self, 'Warning', message )
elif isinstance( exporter, ClientMetadataMigrationExporters.SingleFileMetadataExporterMediaURLs ):
pass
elif isinstance( exporter, ClientMetadataMigrationExporters.SingleFileMetadataExporterTXT ):
self._sidecar_panel.SetSidecarExt( 'txt' )
self._sidecar_panel.SetExampleInput( '01234564789abcdef.jpg' )
self._txt_separator_panel.SetValue( exporter.GetSeparator() )
self._txt_separator_panel.setVisible( True )
elif isinstance( exporter, ClientMetadataMigrationExporters.SingleFileMetadataExporterJSON ):
self._sidecar_panel.SetSidecarExt( 'json' )
self._sidecar_panel.SetExampleInput( '01234564789abcdef.jpg' )
nested_object_names = exporter.GetNestedObjectNames()
self._nested_object_names_list.Clear()
self._nested_object_names_list.AddDatas( nested_object_names )
self._nested_object_names_panel.setVisible( True )
else:
raise Exception( 'Did not understand the new exporter type!' )
def _ShowSidecarHelp( self ):
message = 'Sidecars are typically named just as their associated file but with the additional extension. \'image.jpg\' makes \'image.jpg.txt\', and so on.'
message += os.linesep * 2
message += 'Sidecar exporters will overwrite whatever is at their set destination, so be careful if you intend to set up multiple simultaneous exports, or the second will overwrite the first. You can safely export to two or more different locations in the same .json file, but if you export to .txt, use the \'suffix\' control to export to different files.'
message += os.linesep * 2
message += 'If there is no content to write, no new file will be created.'
QW.QMessageBox.information( self, 'Sidecars', message )
def _UpdateServiceKeyButtonLabel( self ):
try:
name = HG.client_controller.services_manager.GetName( self._service_key )
except HydrusExceptions.DataMissing:
name = 'unknown'
self._service_selection_button.setText( name )
def GetValue( self ) -> ClientMetadataMigrationExporters.SingleFileMetadataExporter:
exporter = self._GetValue()
return exporter
class SingleFileMetadataExporterButton( QW.QPushButton ):
valueChanged = QC.Signal()
def __init__( self, parent: QW.QWidget, exporter: ClientMetadataMigrationExporters.SingleFileMetadataExporter, allowed_exporter_classes: list ):
QW.QPushButton.__init__( self, parent )
self._exporter = exporter
self._allowed_exporter_classes = allowed_exporter_classes
self._RefreshLabel()
self.clicked.connect( self._Edit )
def _Edit( self ):
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit metadata migration exporter' ) as dlg:
panel = EditSingleFileMetadataExporterPanel( dlg, self._exporter, self._allowed_exporter_classes )
dlg.SetPanel( panel )
if dlg.exec() == QW.QDialog.Accepted:
value = panel.GetValue()
self.SetValue( value )
self.valueChanged.emit()
def _RefreshLabel( self ):
text = self._exporter.ToString()
elided_text = HydrusText.ElideText( text, 64 )
self.setText( elided_text )
self.setToolTip( text )
def GetValue( self ):
return self._exporter
def SetValue( self, exporter: ClientMetadataMigrationExporters.SingleFileMetadataExporter ):
self._exporter = exporter
self._RefreshLabel()