1261 lines
43 KiB
Python
1261 lines
43 KiB
Python
import collections
|
|
import os
|
|
import time
|
|
import typing
|
|
|
|
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 HydrusLists
|
|
from hydrus.core import HydrusThreading
|
|
from hydrus.core import HydrusTime
|
|
from hydrus.core.files.images import HydrusImageMetadata
|
|
from hydrus.core.files.images import HydrusImageOpening
|
|
|
|
from hydrus.client import ClientApplicationCommand as CAC
|
|
from hydrus.client import ClientConstants as CC
|
|
from hydrus.client import ClientGlobals as CG
|
|
from hydrus.client import ClientFiles
|
|
from hydrus.client import ClientPaths
|
|
from hydrus.client import ClientPDFHandling
|
|
from hydrus.client import ClientThreading
|
|
from hydrus.client.gui import ClientGUIAsync
|
|
from hydrus.client.gui import ClientGUIDialogsMessage
|
|
from hydrus.client.gui import ClientGUIDialogsQuick
|
|
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
|
|
from hydrus.client.gui import ClientGUIScrolledPanelsReview
|
|
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
|
|
from hydrus.client.gui.exporting import ClientGUIExport
|
|
from hydrus.client.gui.media import ClientGUIMediaSimpleActions
|
|
from hydrus.client.media import ClientMedia
|
|
from hydrus.client.metadata import ClientContentUpdates
|
|
from hydrus.client.metadata import ClientTags
|
|
|
|
def ApplyContentApplicationCommandToMedia( win: QW.QWidget, command: CAC.ApplicationCommand, media: typing.Collection[ ClientMedia.MediaSingleton ] ):
|
|
|
|
if not command.IsContentCommand():
|
|
|
|
return
|
|
|
|
|
|
service_key = command.GetContentServiceKey()
|
|
action = command.GetContentAction()
|
|
value = command.GetContentValue()
|
|
|
|
try:
|
|
|
|
service = CG.client_controller.services_manager.GetService( service_key )
|
|
|
|
except HydrusExceptions.DataMissing:
|
|
|
|
command_processed = False
|
|
|
|
return command_processed
|
|
|
|
|
|
service_type = service.GetServiceType()
|
|
|
|
if service_type == HC.LOCAL_FILE_DOMAIN:
|
|
|
|
if value is not None:
|
|
|
|
source_service_key = value
|
|
|
|
else:
|
|
|
|
source_service_key = None
|
|
|
|
|
|
MoveOrDuplicateLocalFiles( win, service_key, action, media, source_service_key = source_service_key )
|
|
|
|
else:
|
|
|
|
content_updates = []
|
|
|
|
if service_type in HC.REAL_TAG_SERVICES:
|
|
|
|
tag = value
|
|
|
|
content_updates = GetContentUpdatesForAppliedContentApplicationCommandTags( win, service_key, service_type, action, media, tag )
|
|
|
|
elif service_type in HC.RATINGS_SERVICES:
|
|
|
|
if action in ( HC.CONTENT_UPDATE_SET, HC.CONTENT_UPDATE_FLIP ):
|
|
|
|
if action == HC.CONTENT_UPDATE_FLIP and service_type == HC.LOCAL_RATING_INCDEC:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
rating = value
|
|
|
|
content_updates = GetContentUpdatesForAppliedContentApplicationCommandRatingsSetFlip( service_key, action, media, rating )
|
|
|
|
|
|
elif action in ( HC.CONTENT_UPDATE_INCREMENT, HC.CONTENT_UPDATE_DECREMENT ):
|
|
|
|
if service_type == HC.LOCAL_RATING_NUMERICAL:
|
|
|
|
one_star_value = service.GetOneStarValue()
|
|
|
|
content_updates = GetContentUpdatesForAppliedContentApplicationCommandRatingsNumericalIncDec( service_key, one_star_value, action, media )
|
|
|
|
elif service_type == HC.LOCAL_RATING_INCDEC:
|
|
|
|
content_updates = GetContentUpdatesForAppliedContentApplicationCommandRatingsIncDec( service_key, action, media )
|
|
|
|
|
|
else:
|
|
|
|
return True
|
|
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
|
if len( content_updates ) > 0:
|
|
|
|
CG.client_controller.Write( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdates( service_key, content_updates ) )
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
def ClearDeleteRecord( win, media ):
|
|
|
|
clearable_media = [ m for m in media if CC.COMBINED_LOCAL_FILE_SERVICE_KEY in m.GetLocationsManager().GetDeleted() ]
|
|
|
|
if len( clearable_media ) == 0:
|
|
|
|
return
|
|
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( win, 'Clear the deletion record for {} previously deleted files?.'.format( HydrusData.ToHumanInt( len( clearable_media ) ) ) )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
for chunk_of_media in HydrusData.SplitIteratorIntoChunks( clearable_media, 64 ):
|
|
|
|
clearee_hashes = [ m.GetHash() for m in chunk_of_media ]
|
|
|
|
content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_CLEAR_DELETE_RECORD, clearee_hashes )
|
|
|
|
content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, content_update )
|
|
|
|
CG.client_controller.Write( 'content_updates', content_update_package )
|
|
|
|
|
|
|
|
|
|
|
|
def CopyHashesToClipboard( win: QW.QWidget, hash_type: str, medias: typing.Sequence[ ClientMedia.Media ] ):
|
|
|
|
if len( medias ) == 0:
|
|
|
|
return
|
|
|
|
|
|
hex_it = True
|
|
|
|
desired_hashes = []
|
|
|
|
flat_media = ClientMedia.FlattenMedia( medias )
|
|
|
|
sha256_hashes = [ media.GetHash() for media in flat_media ]
|
|
|
|
if hash_type in ( 'pixel_hash', 'blurhash' ):
|
|
|
|
file_info_managers = [ media.GetFileInfoManager() for media in flat_media ]
|
|
|
|
if hash_type == 'pixel_hash':
|
|
|
|
desired_hashes = [ fim.pixel_hash for fim in file_info_managers if fim.pixel_hash is not None ]
|
|
|
|
elif hash_type == 'blurhash':
|
|
|
|
desired_hashes = [ fim.blurhash for fim in file_info_managers if fim.blurhash is not None ]
|
|
|
|
hex_it = False
|
|
|
|
|
|
elif hash_type == 'sha256':
|
|
|
|
desired_hashes = sha256_hashes
|
|
|
|
else:
|
|
|
|
num_hashes = len( sha256_hashes )
|
|
num_remote_medias = len( [ not media.GetLocationsManager().IsLocal() for media in flat_media ] )
|
|
|
|
source_to_desired = CG.client_controller.Read( 'file_hashes', sha256_hashes, 'sha256', hash_type )
|
|
|
|
desired_hashes = [ source_to_desired[ source_hash ] for source_hash in sha256_hashes if source_hash in source_to_desired ]
|
|
|
|
num_missing = num_hashes - len( desired_hashes )
|
|
|
|
if num_missing > 0:
|
|
|
|
if num_missing == num_hashes:
|
|
|
|
message = 'Unfortunately, none of the {} hashes could be found.'.format( hash_type )
|
|
|
|
else:
|
|
|
|
message = 'Unfortunately, {} of the {} hashes could not be found.'.format( HydrusData.ToHumanInt( num_missing ), hash_type )
|
|
|
|
|
|
if num_remote_medias > 0:
|
|
|
|
message += ' {} of the files you wanted are not currently in this client. If they have never visited this client, the lookup is impossible.'.format( HydrusData.ToHumanInt( num_remote_medias ) )
|
|
|
|
|
|
if num_remote_medias < num_hashes:
|
|
|
|
message += ' It could be that some of the local files are currently missing this information in the hydrus database. A file maintenance job (under the database menu) can repopulate this data.'
|
|
|
|
|
|
ClientGUIDialogsMessage.ShowWarning( win, message )
|
|
|
|
|
|
|
|
if len( desired_hashes ) > 0:
|
|
|
|
if hex_it:
|
|
|
|
text_lines = [ desired_hash.hex() for desired_hash in desired_hashes ]
|
|
|
|
else:
|
|
|
|
text_lines = desired_hashes
|
|
|
|
|
|
if CG.client_controller.new_options.GetBoolean( 'prefix_hash_when_copying' ):
|
|
|
|
text_lines = [ '{}:{}'.format( hash_type, hex_hash ) for hex_hash in text_lines ]
|
|
|
|
|
|
hex_hashes_text = '\n'.join( text_lines )
|
|
|
|
CG.client_controller.pub( 'clipboard', 'text', hex_hashes_text )
|
|
|
|
job_status = ClientThreading.JobStatus()
|
|
|
|
job_status.SetStatusText( '{} {} hashes copied'.format( HydrusData.ToHumanInt( len( desired_hashes ) ), hash_type ) )
|
|
|
|
CG.client_controller.pub( 'message', job_status )
|
|
|
|
job_status.FinishAndDismiss( 2 )
|
|
|
|
|
|
|
|
def DoClearFileViewingStats( win: QW.QWidget, flat_medias: typing.Collection[ ClientMedia.MediaSingleton ] ):
|
|
|
|
if len( flat_medias ) == 0:
|
|
|
|
return
|
|
|
|
|
|
if len( flat_medias ) == 1:
|
|
|
|
insert = 'this file'
|
|
|
|
else:
|
|
|
|
insert = 'these {} files'.format( HydrusData.ToHumanInt( len( flat_medias ) ) )
|
|
|
|
|
|
message = 'Clear the file viewing count/duration and \'last viewed time\' for {}?'.format( insert )
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( win, message )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
hashes = { m.GetHash() for m in flat_medias }
|
|
|
|
content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILE_VIEWING_STATS, HC.CONTENT_UPDATE_DELETE, hashes )
|
|
|
|
CG.client_controller.Write( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, content_update ) )
|
|
|
|
|
|
|
|
def DoOpenKnownURLFromShortcut( win, media ):
|
|
|
|
urls = media.GetLocationsManager().GetURLs()
|
|
|
|
matched_labels_and_urls = []
|
|
unmatched_urls = []
|
|
|
|
if len( urls ) > 0:
|
|
|
|
for url in urls:
|
|
|
|
try:
|
|
|
|
url_class = CG.client_controller.network_engine.domain_manager.GetURLClass( url )
|
|
|
|
except HydrusExceptions.URLClassException:
|
|
|
|
continue
|
|
|
|
|
|
if url_class is None:
|
|
|
|
unmatched_urls.append( url )
|
|
|
|
else:
|
|
|
|
label = url_class.GetName() + ': ' + url
|
|
|
|
matched_labels_and_urls.append( ( label, url ) )
|
|
|
|
|
|
|
|
matched_labels_and_urls.sort()
|
|
unmatched_urls.sort()
|
|
|
|
|
|
if len( matched_labels_and_urls ) == 0:
|
|
|
|
return
|
|
|
|
elif len( matched_labels_and_urls ) == 1:
|
|
|
|
url = matched_labels_and_urls[0][1]
|
|
|
|
else:
|
|
|
|
matched_labels_and_urls.extend( ( url, url ) for url in unmatched_urls )
|
|
|
|
try:
|
|
|
|
url = ClientGUIDialogsQuick.SelectFromList( win, 'Select which URL', matched_labels_and_urls, sort_tuples = False )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
|
|
ClientPaths.LaunchURLInWebBrowser( url )
|
|
|
|
|
|
# this isn't really a 'media' guy, and it edits the options in place, so maybe move/edit/whatever!
|
|
def EditDuplicateContentMergeOptions( win: QW.QWidget, duplicate_type: int ):
|
|
|
|
new_options = CG.client_controller.new_options
|
|
|
|
duplicate_content_merge_options = new_options.GetDuplicateContentMergeOptions( duplicate_type )
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( win, 'edit duplicate merge options' ) as dlg:
|
|
|
|
panel = ClientGUIScrolledPanelsEdit.EditDuplicateContentMergeOptionsPanel( dlg, duplicate_type, duplicate_content_merge_options )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
duplicate_content_merge_options = panel.GetValue()
|
|
|
|
new_options.SetDuplicateContentMergeOptions( duplicate_type, duplicate_content_merge_options )
|
|
|
|
|
|
|
|
|
|
|
|
def EditFileNotes( win: QW.QWidget, media: ClientMedia.MediaSingleton, name_to_start_on = typing.Optional[ str ] ):
|
|
|
|
names_to_notes = media.GetNotesManager().GetNamesToNotes()
|
|
|
|
title = 'manage notes'
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( win, title ) as dlg:
|
|
|
|
panel = ClientGUIScrolledPanelsEdit.EditFileNotesPanel( dlg, names_to_notes, name_to_start_on = name_to_start_on )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
hash = media.GetHash()
|
|
|
|
( names_to_notes, deletee_names ) = panel.GetValue()
|
|
|
|
content_updates = [ ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_NOTES, HC.CONTENT_UPDATE_SET, ( hash, name, note ) ) for ( name, note ) in names_to_notes.items() ]
|
|
content_updates.extend( [ ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_NOTES, HC.CONTENT_UPDATE_DELETE, ( hash, name ) ) for name in deletee_names ] )
|
|
|
|
content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdates( CC.LOCAL_NOTES_SERVICE_KEY, content_updates )
|
|
|
|
CG.client_controller.Write( 'content_updates', content_update_package )
|
|
|
|
|
|
|
|
|
|
def EditFileTimestamps( win: QW.QWidget, ordered_medias: typing.List[ ClientMedia.MediaSingleton ] ):
|
|
|
|
title = 'manage times'
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( win, title ) as dlg:
|
|
|
|
panel = ClientGUIScrolledPanelsEdit.EditFileTimestampsPanel( dlg, ordered_medias )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
content_update_package = panel.GetContentUpdatePackage()
|
|
|
|
if content_update_package.HasContent():
|
|
|
|
CG.client_controller.Write( 'content_updates', content_update_package )
|
|
|
|
result = panel.GetFileModifiedUpdateData()
|
|
|
|
if result is not None:
|
|
|
|
( hashes_to_alter_modified_dates, file_modified_timestamp_ms, step_ms ) = result
|
|
|
|
hashes_to_alter_modified_dates = set( hashes_to_alter_modified_dates )
|
|
|
|
medias_to_alter_modified_dates = [ m for m in ordered_medias if m.GetHash() in hashes_to_alter_modified_dates ]
|
|
|
|
def do_it():
|
|
|
|
job_status = ClientThreading.JobStatus( cancellable = True )
|
|
|
|
job_status.SetStatusTitle( 'setting file modified dates' )
|
|
|
|
time_started = HydrusTime.GetNow()
|
|
showed_popup = False
|
|
|
|
num_to_do = len( medias_to_alter_modified_dates )
|
|
|
|
for ( i, m ) in enumerate( medias_to_alter_modified_dates ):
|
|
|
|
job_status.SetStatusText( HydrusData.ConvertValueRangeToPrettyString( i, num_to_do ) )
|
|
job_status.SetVariable( 'popup_gauge_1', ( i, num_to_do ) )
|
|
|
|
if not showed_popup and HydrusTime.TimeHasPassed( time_started + 3 ):
|
|
|
|
CG.client_controller.pub( 'message', job_status )
|
|
|
|
showed_popup = True
|
|
|
|
|
|
if job_status.IsCancelled():
|
|
|
|
break
|
|
|
|
|
|
final_time_ms = file_modified_timestamp_ms + ( i * step_ms )
|
|
|
|
CG.client_controller.client_files_manager.UpdateFileModifiedTimestampMS( m, final_time_ms )
|
|
|
|
|
|
job_status.FinishAndDismiss()
|
|
|
|
|
|
CG.client_controller.CallToThread( do_it )
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ExportFiles( win: QW.QWidget, medias: typing.Collection[ ClientMedia.Media ], do_export_and_then_quit = False ):
|
|
|
|
flat_media = ClientMedia.FlattenMedia( medias )
|
|
|
|
flat_media = [ m for m in flat_media if m.GetLocationsManager().IsLocal() ]
|
|
|
|
if len( flat_media ) > 0:
|
|
|
|
frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( win, 'export files' )
|
|
|
|
panel = ClientGUIExport.ReviewExportFilesPanel( frame, flat_media, do_export_and_then_quit = do_export_and_then_quit )
|
|
|
|
frame.SetPanel( panel )
|
|
|
|
|
|
|
|
def GetContentUpdatesForAppliedContentApplicationCommandRatingsSetFlip( service_key: bytes, action: int, media: typing.Collection[ ClientMedia.MediaSingleton ], rating: typing.Optional[ float ] ):
|
|
|
|
hashes = set()
|
|
|
|
for m in media:
|
|
|
|
hashes.add( m.GetHash() )
|
|
|
|
|
|
can_set = False
|
|
can_unset = False
|
|
|
|
for m in media:
|
|
|
|
ratings_manager = m.GetRatingsManager()
|
|
|
|
current_rating = ratings_manager.GetRating( service_key )
|
|
|
|
if current_rating == rating and action == HC.CONTENT_UPDATE_FLIP:
|
|
|
|
can_unset = True
|
|
|
|
else:
|
|
|
|
can_set = True
|
|
|
|
|
|
|
|
if can_set:
|
|
|
|
row = ( rating, hashes )
|
|
|
|
elif can_unset:
|
|
|
|
row = ( None, hashes )
|
|
|
|
else:
|
|
|
|
return []
|
|
|
|
|
|
content_updates = [ ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, row ) ]
|
|
|
|
return content_updates
|
|
|
|
|
|
def GetContentUpdatesForAppliedContentApplicationCommandRatingsIncDec( service_key: bytes, action: int, media: typing.Collection[ ClientMedia.MediaSingleton ] ):
|
|
|
|
if action == HC.CONTENT_UPDATE_INCREMENT:
|
|
|
|
direction = 1
|
|
|
|
elif action == HC.CONTENT_UPDATE_DECREMENT:
|
|
|
|
direction = -1
|
|
|
|
else:
|
|
|
|
return []
|
|
|
|
|
|
ratings_to_hashes = collections.defaultdict( set )
|
|
|
|
for m in media:
|
|
|
|
ratings_manager = m.GetRatingsManager()
|
|
|
|
current_rating = ratings_manager.GetRating( service_key )
|
|
|
|
new_rating = current_rating + direction
|
|
|
|
ratings_to_hashes[ new_rating ].add( m.GetHash() )
|
|
|
|
|
|
content_updates = [ ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( rating, hashes ) ) for ( rating, hashes ) in ratings_to_hashes.items() ]
|
|
|
|
return content_updates
|
|
|
|
|
|
def GetContentUpdatesForAppliedContentApplicationCommandRatingsNumericalIncDec( service_key: bytes, one_star_value: float, action: int, media: typing.Collection[ ClientMedia.MediaSingleton ] ):
|
|
|
|
if action == HC.CONTENT_UPDATE_INCREMENT:
|
|
|
|
direction = 1
|
|
initialisation_rating = 0.0
|
|
|
|
elif action == HC.CONTENT_UPDATE_DECREMENT:
|
|
|
|
direction = -1
|
|
initialisation_rating = 1.0
|
|
|
|
else:
|
|
|
|
return []
|
|
|
|
|
|
ratings_to_hashes = collections.defaultdict( set )
|
|
|
|
for m in media:
|
|
|
|
ratings_manager = m.GetRatingsManager()
|
|
|
|
current_rating = ratings_manager.GetRating( service_key )
|
|
|
|
if current_rating is None:
|
|
|
|
new_rating = initialisation_rating
|
|
|
|
else:
|
|
|
|
new_rating = current_rating + ( one_star_value * direction )
|
|
|
|
new_rating = max( min( new_rating, 1.0 ), 0.0 )
|
|
|
|
|
|
if current_rating != new_rating:
|
|
|
|
ratings_to_hashes[ new_rating ].add( m.GetHash() )
|
|
|
|
|
|
|
|
content_updates = [ ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( rating, hashes ) ) for ( rating, hashes ) in ratings_to_hashes.items() ]
|
|
|
|
return content_updates
|
|
|
|
|
|
def GetContentUpdatesForAppliedContentApplicationCommandTags( win: QW.QWidget, service_key: bytes, service_type: int, action: int, media: typing.Collection[ ClientMedia.MediaSingleton ], tag: str ):
|
|
|
|
hashes = set()
|
|
|
|
for m in media:
|
|
|
|
hashes.add( m.GetHash() )
|
|
|
|
|
|
rows = [ ( tag, hashes ) ]
|
|
|
|
can_add = False
|
|
can_pend = False
|
|
can_delete = False
|
|
can_petition = True
|
|
can_rescind_pend = False
|
|
can_rescind_petition = False
|
|
|
|
for m in media:
|
|
|
|
tags_manager = m.GetTagsManager()
|
|
|
|
current = tags_manager.GetCurrent( service_key, ClientTags.TAG_DISPLAY_STORAGE )
|
|
pending = tags_manager.GetPending( service_key, ClientTags.TAG_DISPLAY_STORAGE )
|
|
petitioned = tags_manager.GetPetitioned( service_key, ClientTags.TAG_DISPLAY_STORAGE )
|
|
|
|
if tag not in current:
|
|
|
|
can_add = True
|
|
|
|
|
|
if tag not in current and tag not in pending:
|
|
|
|
can_pend = True
|
|
|
|
|
|
if tag in current and action == HC.CONTENT_UPDATE_FLIP:
|
|
|
|
can_delete = True
|
|
|
|
|
|
if tag in current and tag not in petitioned and action == HC.CONTENT_UPDATE_FLIP:
|
|
|
|
can_petition = True
|
|
|
|
|
|
if tag in pending and action == HC.CONTENT_UPDATE_FLIP:
|
|
|
|
can_rescind_pend = True
|
|
|
|
|
|
if tag in petitioned:
|
|
|
|
can_rescind_petition = True
|
|
|
|
|
|
|
|
reason = None
|
|
|
|
if service_type == HC.LOCAL_TAG:
|
|
|
|
if can_add:
|
|
|
|
content_update_action = HC.CONTENT_UPDATE_ADD
|
|
|
|
elif can_delete:
|
|
|
|
content_update_action = HC.CONTENT_UPDATE_DELETE
|
|
|
|
else:
|
|
|
|
return []
|
|
|
|
|
|
else:
|
|
|
|
if can_rescind_petition:
|
|
|
|
content_update_action = HC.CONTENT_UPDATE_RESCIND_PETITION
|
|
|
|
elif can_pend:
|
|
|
|
content_update_action = HC.CONTENT_UPDATE_PEND
|
|
|
|
elif can_rescind_pend:
|
|
|
|
content_update_action = HC.CONTENT_UPDATE_RESCIND_PEND
|
|
|
|
elif can_petition:
|
|
|
|
message = 'Enter a reason for this tag to be removed. A janitor will review your petition.'
|
|
|
|
from hydrus.client.gui import ClientGUIDialogs
|
|
|
|
with ClientGUIDialogs.DialogTextEntry( win, message ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
content_update_action = HC.CONTENT_UPDATE_PETITION
|
|
|
|
reason = dlg.GetValue()
|
|
|
|
else:
|
|
|
|
return []
|
|
|
|
|
|
|
|
else:
|
|
|
|
return []
|
|
|
|
|
|
|
|
content_updates = [ ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, content_update_action, row, reason = reason ) for row in rows ]
|
|
|
|
return content_updates
|
|
|
|
|
|
def MoveOrDuplicateLocalFiles( win: QW.QWidget, dest_service_key: bytes, action: int, media: typing.Collection[ ClientMedia.MediaSingleton ], source_service_key: typing.Optional[ bytes ] = None ):
|
|
|
|
dest_service_name = CG.client_controller.services_manager.GetName( dest_service_key )
|
|
|
|
applicable_media = [ m for m in media if m.GetLocationsManager().IsLocal() and dest_service_key not in m.GetLocationsManager().GetCurrent() and m.GetMime() not in HC.HYDRUS_UPDATE_FILES ]
|
|
|
|
if len( applicable_media ) == 0:
|
|
|
|
return
|
|
|
|
|
|
( local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys ) = ClientGUIMediaSimpleActions.GetLocalFileActionServiceKeys( media )
|
|
|
|
do_yes_no = do_yes_no = CG.client_controller.new_options.GetBoolean( 'confirm_multiple_local_file_services_copy' )
|
|
yes_no_text = 'Add {} files to {}?'.format( HydrusData.ToHumanInt( len( applicable_media ) ), dest_service_name )
|
|
|
|
if action == HC.CONTENT_UPDATE_MOVE:
|
|
|
|
do_yes_no = CG.client_controller.new_options.GetBoolean( 'confirm_multiple_local_file_services_move' )
|
|
|
|
local_moveable_from_and_to_file_service_keys = { pair for pair in local_moveable_from_and_to_file_service_keys if pair[1] == dest_service_key }
|
|
|
|
potential_source_service_keys = { pair[0] for pair in local_moveable_from_and_to_file_service_keys }
|
|
|
|
potential_source_service_keys_to_applicable_media = collections.defaultdict( list )
|
|
|
|
for m in applicable_media:
|
|
|
|
current = m.GetLocationsManager().GetCurrent()
|
|
|
|
for potential_source_service_key in potential_source_service_keys:
|
|
|
|
if potential_source_service_key in current:
|
|
|
|
potential_source_service_keys_to_applicable_media[ potential_source_service_key ].append( m )
|
|
|
|
|
|
|
|
|
|
if source_service_key is None:
|
|
|
|
if len( potential_source_service_keys ) == 0:
|
|
|
|
return
|
|
|
|
elif len( potential_source_service_keys ) == 1:
|
|
|
|
( source_service_key, ) = potential_source_service_keys
|
|
|
|
else:
|
|
|
|
do_yes_no = False
|
|
|
|
choice_tuples = []
|
|
|
|
for potential_source_service_key in potential_source_service_keys:
|
|
|
|
potential_source_service_name = CG.client_controller.services_manager.GetName( potential_source_service_key )
|
|
|
|
text = 'move {} in "{}" to "{}"'.format( len( potential_source_service_keys_to_applicable_media[ potential_source_service_key ] ), potential_source_service_name, dest_service_name )
|
|
|
|
description = 'Move from {} to {}.'.format( potential_source_service_name, dest_service_name )
|
|
|
|
choice_tuples.append( ( text, potential_source_service_key, description ) )
|
|
|
|
|
|
choice_tuples.sort()
|
|
|
|
try:
|
|
|
|
source_service_key = ClientGUIDialogsQuick.SelectFromListButtons( win, 'select source service', choice_tuples, message = 'Select where we are moving from. Note this may not cover all files.' )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
source_service_name = CG.client_controller.services_manager.GetName( source_service_key )
|
|
|
|
applicable_media = potential_source_service_keys_to_applicable_media[ source_service_key ]
|
|
|
|
yes_no_text = 'Move {} files from {} to {}?'.format( HydrusData.ToHumanInt( len( applicable_media ) ), source_service_name, dest_service_name )
|
|
|
|
|
|
if len( applicable_media ) == 0:
|
|
|
|
return
|
|
|
|
|
|
if do_yes_no:
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( win, yes_no_text )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
|
|
def work_callable():
|
|
|
|
job_status = ClientThreading.JobStatus( cancellable = True )
|
|
|
|
title = 'moving files' if action == HC.CONTENT_UPDATE_MOVE else 'adding files'
|
|
|
|
job_status.SetStatusTitle( title )
|
|
|
|
BLOCK_SIZE = 64
|
|
|
|
pauser = HydrusThreading.BigJobPauser()
|
|
|
|
num_to_do = len( applicable_media )
|
|
|
|
if num_to_do > BLOCK_SIZE:
|
|
|
|
CG.client_controller.pub( 'message', job_status )
|
|
|
|
|
|
now_ms = HydrusTime.GetNowMS()
|
|
|
|
for ( i, block_of_media ) in enumerate( HydrusLists.SplitListIntoChunks( applicable_media, BLOCK_SIZE ) ):
|
|
|
|
if job_status.IsCancelled():
|
|
|
|
break
|
|
|
|
|
|
job_status.SetStatusText( HydrusData.ConvertValueRangeToPrettyString( i * BLOCK_SIZE, num_to_do ) )
|
|
job_status.SetVariable( 'popup_gauge_1', ( i * BLOCK_SIZE, num_to_do ) )
|
|
|
|
content_updates = []
|
|
undelete_hashes = set()
|
|
|
|
for m in block_of_media:
|
|
|
|
if dest_service_key in m.GetLocationsManager().GetDeleted():
|
|
|
|
undelete_hashes.add( m.GetHash() )
|
|
|
|
else:
|
|
|
|
content_updates.append( ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ADD, ( m.GetMediaResult().GetFileInfoManager(), now_ms ) ) )
|
|
|
|
|
|
|
|
if len( undelete_hashes ) > 0:
|
|
|
|
content_updates.append( ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_UNDELETE, undelete_hashes ) )
|
|
|
|
|
|
CG.client_controller.WriteSynchronous( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdates( dest_service_key, content_updates ) )
|
|
|
|
if action == HC.CONTENT_UPDATE_MOVE:
|
|
|
|
block_of_hashes = [ m.GetHash() for m in block_of_media ]
|
|
|
|
content_updates = [ ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE_FROM_SOURCE_AFTER_MIGRATE, block_of_hashes, reason = 'Moved to {}'.format( dest_service_name ) ) ]
|
|
|
|
CG.client_controller.WriteSynchronous( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdates( source_service_key, content_updates ) )
|
|
|
|
|
|
pauser.Pause()
|
|
|
|
|
|
job_status.FinishAndDismiss()
|
|
|
|
|
|
def publish_callable( result ):
|
|
|
|
pass
|
|
|
|
|
|
job = ClientGUIAsync.AsyncQtJob( win, work_callable, publish_callable )
|
|
|
|
job.start()
|
|
|
|
|
|
def OpenURLs( win: QW.QWidget, urls ):
|
|
|
|
urls = sorted( urls )
|
|
|
|
if len( urls ) > 1:
|
|
|
|
message = 'Open the {} URLs in your web browser?'.format( len( urls ) )
|
|
|
|
if len( urls ) > 10:
|
|
|
|
message += ' This will take some time.'
|
|
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( win, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
|
|
def do_it( urls ):
|
|
|
|
job_status = None
|
|
|
|
num_urls = len( urls )
|
|
|
|
if num_urls > 5:
|
|
|
|
job_status = ClientThreading.JobStatus( pausable = True, cancellable = True )
|
|
|
|
job_status.SetStatusTitle( 'Opening URLs' )
|
|
|
|
CG.client_controller.pub( 'message', job_status )
|
|
|
|
|
|
try:
|
|
|
|
for ( i, url ) in enumerate( urls ):
|
|
|
|
if job_status is not None:
|
|
|
|
( i_paused, should_quit ) = job_status.WaitIfNeeded()
|
|
|
|
if should_quit:
|
|
|
|
return
|
|
|
|
|
|
job_status.SetStatusText( HydrusData.ConvertValueRangeToPrettyString( i + 1, num_urls ) )
|
|
job_status.SetVariable( 'popup_gauge_1', ( i + 1, num_urls ) )
|
|
|
|
|
|
ClientPaths.LaunchURLInWebBrowser( url )
|
|
|
|
time.sleep( 1 )
|
|
|
|
|
|
finally:
|
|
|
|
if job_status is not None:
|
|
|
|
job_status.FinishAndDismiss( 1 )
|
|
|
|
|
|
|
|
|
|
CG.client_controller.CallToThread( do_it, urls )
|
|
|
|
|
|
def OpenMediaURLs( win: QW.QWidget, medias ):
|
|
|
|
urls = set()
|
|
|
|
for media in medias:
|
|
|
|
media_urls = media.GetLocationsManager().GetURLs()
|
|
|
|
urls.update( media_urls )
|
|
|
|
|
|
OpenURLs( win, urls )
|
|
|
|
|
|
def OpenMediaURLClassURLs( win: QW.QWidget, medias, url_class ):
|
|
|
|
urls = set()
|
|
|
|
for media in medias:
|
|
|
|
media_urls = media.GetLocationsManager().GetURLs()
|
|
|
|
for url in media_urls:
|
|
|
|
# can't do 'url_class.matches', as it will match too many
|
|
if CG.client_controller.network_engine.domain_manager.GetURLClass( url ) == url_class:
|
|
|
|
urls.add( url )
|
|
|
|
|
|
|
|
|
|
OpenURLs( win, urls )
|
|
|
|
|
|
def SetFilesForcedFiletypes( win: QW.QWidget, medias: typing.Collection[ ClientMedia.Media ] ):
|
|
|
|
# boot a panel, it shows the user what current mimes are, what forced mimes are, and they have the choice to set all to x
|
|
# if it comes back yes, we save to db
|
|
|
|
medias = ClientMedia.FlattenMedia( medias )
|
|
|
|
file_info_managers = [ media.GetFileInfoManager() for media in medias ]
|
|
|
|
original_mimes_count = collections.Counter( file_info_manager.GetOriginalMime() for file_info_manager in file_info_managers )
|
|
forced_mimes_count = collections.Counter( file_info_manager.mime for file_info_manager in file_info_managers if file_info_manager.FiletypeIsForced() )
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( win, 'force filetypes' ) as dlg:
|
|
|
|
panel = ClientGUIScrolledPanelsEdit.EditFilesForcedFiletypePanel( dlg, original_mimes_count, forced_mimes_count )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
forced_mime = panel.GetValue()
|
|
|
|
def work_callable():
|
|
|
|
job_status = ClientThreading.JobStatus( cancellable = True )
|
|
|
|
job_status.SetStatusTitle( 'forcing filetypes' )
|
|
|
|
BLOCK_SIZE = 64
|
|
|
|
pauser = HydrusThreading.BigJobPauser()
|
|
|
|
num_to_do = len( medias )
|
|
|
|
if num_to_do > BLOCK_SIZE:
|
|
|
|
CG.client_controller.pub( 'message', job_status )
|
|
|
|
|
|
for ( i, block_of_media ) in enumerate( HydrusLists.SplitListIntoChunks( medias, BLOCK_SIZE ) ):
|
|
|
|
if job_status.IsCancelled():
|
|
|
|
break
|
|
|
|
|
|
job_status.SetStatusText( HydrusData.ConvertValueRangeToPrettyString( i * BLOCK_SIZE, num_to_do ) )
|
|
job_status.SetVariable( 'popup_gauge_1', ( i * BLOCK_SIZE, num_to_do ) )
|
|
|
|
hashes = { media.GetHash() for media in block_of_media }
|
|
|
|
CG.client_controller.WriteSynchronous( 'force_filetype', hashes, forced_mime )
|
|
|
|
hashes_we_needed_to_dupe = set()
|
|
|
|
for media in block_of_media:
|
|
|
|
hash = media.GetHash()
|
|
|
|
current_mime = media.GetMime()
|
|
mime_to_move_to = forced_mime
|
|
|
|
if mime_to_move_to is None:
|
|
|
|
mime_to_move_to = media.GetFileInfoManager().GetOriginalMime()
|
|
|
|
|
|
needed_to_dupe_the_file = CG.client_controller.client_files_manager.ChangeFileExt( hash, current_mime, mime_to_move_to )
|
|
|
|
if needed_to_dupe_the_file:
|
|
|
|
hashes_we_needed_to_dupe.add( hash )
|
|
|
|
|
|
|
|
if len( hashes_we_needed_to_dupe ) > 0:
|
|
|
|
CG.client_controller.WriteSynchronous( 'file_maintenance_add_jobs_hashes', hashes_we_needed_to_dupe, ClientFiles.REGENERATE_FILE_DATA_JOB_DELETE_NEIGHBOUR_DUPES, HydrusTime.GetNow() + 3600 )
|
|
|
|
|
|
pauser.Pause()
|
|
|
|
|
|
job_status.FinishAndDismiss()
|
|
|
|
|
|
def publish_callable( result ):
|
|
|
|
pass
|
|
|
|
|
|
job = ClientGUIAsync.AsyncQtJob( win, work_callable, publish_callable )
|
|
|
|
job.start()
|
|
|
|
|
|
|
|
|
|
def ShowFileEmbeddedMetadata( win: QW.QWidget, media: ClientMedia.MediaSingleton ):
|
|
|
|
if not media.GetLocationsManager().IsLocal():
|
|
|
|
ClientGUIDialogsMessage.ShowWarning( win, 'The file is not local to this computer!' )
|
|
|
|
return
|
|
|
|
|
|
exif_dict = None
|
|
file_text = None
|
|
|
|
extra_rows = []
|
|
|
|
hash = media.GetHash()
|
|
mime = media.GetMime()
|
|
|
|
path = CG.client_controller.client_files_manager.GetFilePath( hash, mime )
|
|
|
|
if mime == HC.APPLICATION_PDF:
|
|
|
|
try:
|
|
|
|
file_text = ClientPDFHandling.GetHumanReadableEmbeddedMetadata( path )
|
|
|
|
except HydrusExceptions.LimitedSupportFileException:
|
|
|
|
file_text = 'Could not read PDF metadata!'
|
|
|
|
|
|
else:
|
|
|
|
raw_pil_image = HydrusImageOpening.RawOpenPILImage( path )
|
|
|
|
if mime in HC.FILES_THAT_CAN_HAVE_EXIF:
|
|
|
|
exif_dict = HydrusImageMetadata.GetEXIFDict( raw_pil_image )
|
|
|
|
|
|
if mime in HC.FILES_THAT_CAN_HAVE_HUMAN_READABLE_EMBEDDED_METADATA:
|
|
|
|
file_text = HydrusImageMetadata.GetEmbeddedFileText( raw_pil_image )
|
|
|
|
|
|
if mime == HC.IMAGE_JPEG:
|
|
|
|
extra_rows.append( ( 'progressive', 'yes' if 'progression' in raw_pil_image.info else 'no' ) )
|
|
|
|
extra_rows.append( ( 'subsampling', HydrusImageMetadata.GetJpegSubsampling( raw_pil_image )) )
|
|
|
|
|
|
|
|
if exif_dict is None and file_text is None and len( extra_rows ) == 0:
|
|
|
|
ClientGUIDialogsMessage.ShowWarning( win, 'Sorry, could not see any human-readable information in this file! Hydrus should have known this, so if this keeps happening, you may need to schedule a rescan of this info in file maintenance.' )
|
|
|
|
return
|
|
|
|
|
|
frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( win, 'Embedded Metadata' )
|
|
|
|
panel = ClientGUIScrolledPanelsReview.ReviewFileEmbeddedMetadata( frame, exif_dict, file_text, extra_rows )
|
|
|
|
frame.SetPanel( panel )
|
|
|
|
|
|
def UndeleteMedia( win, media ):
|
|
|
|
undeletable_media = [ m for m in media if m.GetLocationsManager().IsLocal() ]
|
|
|
|
if len( undeletable_media ) == 0:
|
|
|
|
return
|
|
|
|
|
|
media_deleted_service_keys = HydrusData.MassUnion( ( m.GetLocationsManager().GetDeleted() for m in undeletable_media ) )
|
|
|
|
local_file_services = CG.client_controller.services_manager.GetServices( ( HC.LOCAL_FILE_DOMAIN, ) )
|
|
|
|
undeletable_services = [ local_file_service for local_file_service in local_file_services if local_file_service.GetServiceKey() in media_deleted_service_keys ]
|
|
|
|
if len( undeletable_services ) > 0:
|
|
|
|
do_it = False
|
|
|
|
if len( undeletable_services ) > 1:
|
|
|
|
choice_tuples = []
|
|
|
|
for ( i, service ) in enumerate( undeletable_services ):
|
|
|
|
choice_tuples.append( ( service.GetName(), service, 'Undelete back to {}.'.format( service.GetName() ) ) )
|
|
|
|
|
|
if len( choice_tuples ) > 1:
|
|
|
|
service = CG.client_controller.services_manager.GetService( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY )
|
|
|
|
choice_tuples.append( ( 'all the above', service, 'Undelete back to all services the files have been deleted from.' ) )
|
|
|
|
|
|
try:
|
|
|
|
undelete_service = ClientGUIDialogsQuick.SelectFromListButtons( win, 'Undelete for?', choice_tuples )
|
|
|
|
do_it = True
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
else:
|
|
|
|
( undelete_service, ) = undeletable_services
|
|
|
|
if HC.options[ 'confirm_trash' ]:
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( win, 'Undelete this file back to {}?'.format( undelete_service.GetName() ) )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
do_it = True
|
|
|
|
|
|
else:
|
|
|
|
do_it = True
|
|
|
|
|
|
|
|
if do_it:
|
|
|
|
for chunk_of_media in HydrusData.SplitIteratorIntoChunks( undeletable_media, 64 ):
|
|
|
|
service_key = undelete_service.GetServiceKey()
|
|
|
|
undeletee_hashes = [ m.GetHash() for m in chunk_of_media if service_key in m.GetLocationsManager().GetDeleted() ]
|
|
|
|
content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_UNDELETE, undeletee_hashes )
|
|
|
|
content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( service_key, content_update )
|
|
|
|
CG.client_controller.Write( 'content_updates', content_update_package )
|
|
|
|
|
|
|
|
|