Version 471

This commit is contained in:
Hydrus Network Developer 2022-01-26 15:57:04 -06:00
parent dab05074da
commit fffadc4d0c
46 changed files with 1150 additions and 476 deletions

View File

@ -8,6 +8,43 @@
<div class="content">
<h3 id="changelog"><a href="#changelog">changelog</a></h3>
<ul>
<li><h3 id="version_471"><a href="#version_471">version 471</a></h3></li>
<ul>
<li>times:</li>
<li>if you have file viewing stats turned on (by default it is), the client will now track the 'last viewed time' of your files, both in preview and media viewers. a record is only made assuming they pass the viewtime checks under _options->file viewing statistics_ (so if you scroll through really quick but have it set to only record after five seconds of viewing, it will not save that as the last viewed time). this last viewed time is shown on the right-click menu with the normal file viewing statistics</li>
<li>sorting by 'import time' and 'modified time' are moved to a new 'time' subgroup in the sort button menu</li>
<li>also added to 'time' is 'last viewed time'. note that this has not been tracked until now, so you will have to look at a bunch of things for a few seconds each to get some data to sort with</li>
<li>to go with 'x time' pattern, 'time imported' is renamed to 'import time' across the program. both should work for system predicate parsing</li>
<li>system:'import time' and 'modified time' are now bundled into a new 'system:time' stub in the system predicates list. the window launched from here is an experimental new paged panel. I am not sure I really like it, but let's see how it works IRL</li>
<li>'system:last view time' is added to search the new field! give it a go once you have some data</li>
<li>also note that the search and sort of last viewed time works on the 'media viewer' number. those users who use preview or combined numbers for stuff, let me know if and how you would like that to work here--sort/search for both media and preview, try to combine based on the logic in the options, or something else?</li>
<li>.</li>
<li>loading serialised pngs:</li>
<li>the client can now load serialised downloader-pngs if they are a perfect RGB conversion of an original greyscale export.</li>
<li>the pngs don't technically have to be pngs anymore! if you drag and drop an image from firefox, the temporary bitmap exported and attached to the DnD _should_ work!</li>
<li>the lain easy downloader import now has a clipboard paste button. it can take regular json text, and now, bitmap data!</li>
<li>the 'import->from clipboard' button action in many multiple column lists across the program (e.g. manage parsers) (but not every list, a couple are working on older code) also now accepts bitmap data on the clipboard</li>
<li>the various load errors here are also improved</li>
<li>.</li>
<li>custom widget colors:</li>
<li>(advanced users only for now)</li>
<li>after banging my head against it, I finally figured out an ok way to send colors from a QSS style file to python code. this means I can convert my custom widgets to inherit colours from the current QSS. I expect to migrate pretty much everything currently fixed over to this, except tag colours and maybe some thumbnail border stuff, and retire the old darkmode</li>
<li>if you are a QSS lad, please check out the new entries at the bottom of default_hydrus.qss and play around with them in your own QSSes. please do not send me any updates to be folded in to the install yet as I still have a bunch of other colours to add. this week is just a test--please let me know how it works for you</li>
<li>.</li>
<li>misc:</li>
<li>mouse release events no longer trigger a command in the shortcuts system if the release happens more than about 20 pixels from the original mouse down. this is tricky, so if you are into clever shortcuts, let me know how it works for you</li>
<li>the file maintenance manager (which has been getting a lot of work recently with icc profiles, pixel dupes, some thumb regen, and new audio channel checks), now saves its work and publishes updates faster to the UI, at least once every ten seconds</li>
<li>the sort entries in the page sort control are now always sorted according to their full (type, name) string, and the mouse-wheel-to-navigate is now fixed to always mirror this</li>
<li>improved some 'delete file reason' handling. currently, a file deletion reason should only be applied when a file is entering trash. there was a bug that force-physical-deleting files from trash would overwrite their original deletion reason. this is now fixed. the advanced delete files dialog now disables the whole reason panel better when needed, never sends a file reason down to the database when there should be no reason, disables the panel if all the files are in the trash, and at the database level when file deletion reasons are being set, all files are filtered for status beforehand to ensure none are accidentally set by other means. I am about to make trash more intelligent as part of multiple local file services, so I expect to revisit this soon</li>
<li>the new ICC Profile conversion no longer occurs on I or F mode files. there are weird 32/64 bit monochrome files, and mode/ICC conversion goes whack with current PIL code</li>
<li>replaced the critical hamming test in the duplicate files system with a different bit-counting strategy that works about 9% faster. hamming test is used in all duplicate file searching, so this should help out a tiny bit in a lot of places</li>
<li>.</li>
<li>boring cleanup:</li>
<li>cleaned up how media viewer canvas type is stored and tested in many places</li>
<li>all across the program, file viewing statistics are now tracked by type rather than a hardcoded double of preview & media viewer. it will take a moment to update the database to reflect this this week</li>
<li>cleaned up a ton of file viewing stats code</li>
<li>cleared out the last twenty or so uses of the old 'execute many select' database access routine in favour of the new lower-overhead and more query-optimisable temporary integer tables method</li>
</ul>
<li><h3 id="version_470"><a href="#version_470">version 470</a></h3></li>
<ul>
<li>multiple file services:</li>

View File

@ -1280,6 +1280,7 @@
<li>14 - modified time (oldest first/newest first)</li>
<li>15 - framerate (slowest first/fastest first)</li>
<li>16 - number of frames (smallest first/largest first)</li>
<li>18 - last viewed time (oldest firste/newest first)</li>
</ul>
<p>Response description: The full list of numerical file ids that match the search.</p>
<li>

View File

@ -10,6 +10,18 @@ BLANK_PERCEPTUAL_HASH = b'\x80\x00\x00\x00\x00\x00\x00\x00' # first bit 1 but ev
CAN_HIDE_MOUSE = True
CANVAS_MEDIA_VIEWER = 0
CANVAS_PREVIEW = 1
CANVAS_MEDIA_VIEWER_DUPLICATES = 2
CANVAS_MEDIA_VIEWER_TYPES = { CANVAS_MEDIA_VIEWER, CANVAS_MEDIA_VIEWER_DUPLICATES }
canvas_type_str_lookup = {}
canvas_type_str_lookup[ CANVAS_MEDIA_VIEWER ] = 'media viewer'
canvas_type_str_lookup[ CANVAS_PREVIEW ] = 'preview'
canvas_type_str_lookup[ CANVAS_MEDIA_VIEWER_DUPLICATES ] = 'duplicates filter'
# Hue is generally 200, Sat and Lum changes based on need
COLOUR_LIGHT_SELECTED = QG.QColor( 235, 248, 255 )
COLOUR_SELECTED = QG.QColor( 217, 242, 255 )
@ -309,27 +321,29 @@ SORT_FILES_BY_FILE_MODIFIED_TIMESTAMP = 14
SORT_FILES_BY_FRAMERATE = 15
SORT_FILES_BY_NUM_FRAMES = 16
SORT_FILES_BY_NUM_COLLECTION_FILES = 17
SORT_FILES_BY_LAST_VIEWED_TIME = 18
SYSTEM_SORT_TYPES = []
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_HEIGHT )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_WIDTH )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_RATIO )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_NUM_PIXELS )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_DURATION )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_FRAMERATE )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_NUM_COLLECTION_FILES )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_NUM_FRAMES )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_FILESIZE )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_IMPORT_TIME )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_APPROX_BITRATE )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_HAS_AUDIO )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_MIME )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_FILE_MODIFIED_TIMESTAMP )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_RANDOM )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_NUM_TAGS )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_MEDIA_VIEWS )
SYSTEM_SORT_TYPES.append( SORT_FILES_BY_MEDIA_VIEWTIME )
SYSTEM_SORT_TYPES = {
SORT_FILES_BY_NUM_COLLECTION_FILES,
SORT_FILES_BY_HEIGHT,
SORT_FILES_BY_WIDTH,
SORT_FILES_BY_RATIO,
SORT_FILES_BY_NUM_PIXELS,
SORT_FILES_BY_DURATION,
SORT_FILES_BY_FRAMERATE,
SORT_FILES_BY_NUM_FRAMES,
SORT_FILES_BY_FILESIZE,
SORT_FILES_BY_APPROX_BITRATE,
SORT_FILES_BY_HAS_AUDIO,
SORT_FILES_BY_MIME,
SORT_FILES_BY_RANDOM,
SORT_FILES_BY_NUM_TAGS,
SORT_FILES_BY_MEDIA_VIEWS,
SORT_FILES_BY_MEDIA_VIEWTIME,
SORT_FILES_BY_IMPORT_TIME,
SORT_FILES_BY_FILE_MODIFIED_TIMESTAMP,
SORT_FILES_BY_LAST_VIEWED_TIME
}
system_sort_type_submetatype_string_lookup = {}
@ -345,10 +359,11 @@ system_sort_type_submetatype_string_lookup[ SORT_FILES_BY_APPROX_BITRATE ] = 'fi
system_sort_type_submetatype_string_lookup[ SORT_FILES_BY_FILESIZE ] = 'file'
system_sort_type_submetatype_string_lookup[ SORT_FILES_BY_MIME ] = 'file'
system_sort_type_submetatype_string_lookup[ SORT_FILES_BY_HAS_AUDIO ] = 'file'
system_sort_type_submetatype_string_lookup[ SORT_FILES_BY_IMPORT_TIME ] = 'file'
system_sort_type_submetatype_string_lookup[ SORT_FILES_BY_FILE_MODIFIED_TIMESTAMP ] = 'file'
system_sort_type_submetatype_string_lookup[ SORT_FILES_BY_RANDOM ] = None
system_sort_type_submetatype_string_lookup[ SORT_FILES_BY_NUM_TAGS ] = 'tags'
system_sort_type_submetatype_string_lookup[ SORT_FILES_BY_IMPORT_TIME ] = 'time'
system_sort_type_submetatype_string_lookup[ SORT_FILES_BY_FILE_MODIFIED_TIMESTAMP ] = 'time'
system_sort_type_submetatype_string_lookup[ SORT_FILES_BY_LAST_VIEWED_TIME ] = 'time'
system_sort_type_submetatype_string_lookup[ SORT_FILES_BY_MEDIA_VIEWS ] = 'views'
system_sort_type_submetatype_string_lookup[ SORT_FILES_BY_MEDIA_VIEWTIME ] = 'views'
@ -366,8 +381,9 @@ sort_type_basic_string_lookup[ SORT_FILES_BY_APPROX_BITRATE ] = 'approximate bit
sort_type_basic_string_lookup[ SORT_FILES_BY_FILESIZE ] = 'filesize'
sort_type_basic_string_lookup[ SORT_FILES_BY_MIME ] = 'filetype'
sort_type_basic_string_lookup[ SORT_FILES_BY_HAS_AUDIO ] = 'has audio'
sort_type_basic_string_lookup[ SORT_FILES_BY_IMPORT_TIME ] = 'time imported'
sort_type_basic_string_lookup[ SORT_FILES_BY_IMPORT_TIME ] = 'import time'
sort_type_basic_string_lookup[ SORT_FILES_BY_FILE_MODIFIED_TIMESTAMP ] = 'modified time'
sort_type_basic_string_lookup[ SORT_FILES_BY_LAST_VIEWED_TIME ] = 'last viewed time'
sort_type_basic_string_lookup[ SORT_FILES_BY_RANDOM ] = 'random'
sort_type_basic_string_lookup[ SORT_FILES_BY_NUM_TAGS ] = 'number of tags'
sort_type_basic_string_lookup[ SORT_FILES_BY_MEDIA_VIEWS ] = 'media views'
@ -388,6 +404,8 @@ for sort_type in SYSTEM_SORT_TYPES:
sort_type_string_lookup[ sort_type ] = s
SYSTEM_SORT_TYPES_SORT_CONTROL_SORTED = sorted( SYSTEM_SORT_TYPES, key = lambda sst: sort_type_string_lookup[ sst ] )
SORT_ASC = 0
SORT_DESC = 1

View File

@ -335,6 +335,10 @@ class Controller( HydrusController.HydrusController ):
def _ShutdownManagers( self ):
self.files_maintenance_manager.Shutdown()
self.quick_download_manager.Shutdown()
managers = [ self.subscriptions_manager, self.tag_display_maintenance_manager ]
for manager in managers:
@ -601,6 +605,20 @@ class Controller( HydrusController.HydrusController ):
def ClipboardHasImage( self ):
try:
self.GetClipboardImage()
return True
except HydrusExceptions.DataMissing:
return False
def ClosePageKeys( self, page_keys ):
with self._page_key_lock:
@ -858,11 +876,23 @@ class Controller( HydrusController.HydrusController ):
def GetClipboardImage( self ):
clipboard_image = QW.QApplication.clipboard().image()
if clipboard_image is None or clipboard_image.isNull():
raise HydrusExceptions.DataMissing( 'No bitmap on the clipboard!' )
return clipboard_image
def GetClipboardText( self ):
clipboard_text = QW.QApplication.clipboard().text()
if not clipboard_text:
if clipboard_text is None:
raise HydrusExceptions.DataMissing( 'No text on the clipboard!' )
@ -1939,14 +1969,6 @@ class Controller( HydrusController.HydrusController ):
self.frame_splash_status.SetSubtext( 'files maintenance manager' )
self.files_maintenance_manager.Shutdown()
self.frame_splash_status.SetSubtext( 'download manager' )
self.quick_download_manager.Shutdown()
self.frame_splash_status.SetSubtext( '' )
try:

View File

@ -1894,6 +1894,7 @@ class FilesMaintenanceManager( object ):
big_pauser = HydrusData.BigJobPauser( wait_time = 0.8 )
last_time_jobs_were_cleared = HydrusData.GetNow()
cleared_jobs = []
num_to_do = len( media_results )
@ -2027,7 +2028,7 @@ class FilesMaintenanceManager( object ):
cleared_jobs.append( ( hash, job_type, additional_data ) )
if len( cleared_jobs ) > 100:
if HydrusData.TimeHasPassed( last_time_jobs_were_cleared + 10 ) or len( cleared_jobs ) > 256:
self._controller.WriteSynchronous( 'file_maintenance_clear_jobs', cleared_jobs )

View File

@ -253,76 +253,64 @@ class FileViewingStatsManager( object ):
self._my_flush_job = self._controller.CallRepeating( 5, 60, self.REPEATINGFlush )
def _GenerateViewsRow( self, viewtype, viewtime_delta ):
def _GenerateViewsRow( self, canvas_type, view_timestamp, viewtime_delta ):
new_options = HG.client_controller.new_options
preview_views_delta = 0
preview_viewtime_delta = 0
media_views_delta = 0
media_viewtime_delta = 0
result_views_delta = 0
result_viewtime_delta = 0
if viewtype == 'preview':
do_it = True
if canvas_type == CC.CANVAS_PREVIEW:
preview_min = new_options.GetNoneableInteger( 'file_viewing_statistics_preview_min_time' )
preview_max = new_options.GetNoneableInteger( 'file_viewing_statistics_preview_max_time' )
viewtime_min = new_options.GetNoneableInteger( 'file_viewing_statistics_preview_min_time' )
viewtime_max = new_options.GetNoneableInteger( 'file_viewing_statistics_preview_max_time' )
if preview_max is not None:
elif canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
viewtime_min = new_options.GetNoneableInteger( 'file_viewing_statistics_media_min_time' )
viewtime_max = new_options.GetNoneableInteger( 'file_viewing_statistics_media_max_time' )
if canvas_type == CC.CANVAS_MEDIA_VIEWER_DUPLICATES and not new_options.GetBoolean( 'file_viewing_statistics_active_on_dupe_filter' ):
viewtime_delta = min( viewtime_delta, preview_max )
if preview_min is None or viewtime_delta >= preview_min:
preview_views_delta = 1
preview_viewtime_delta = viewtime_delta
elif viewtype in ( 'media', 'media_duplicates_filter' ):
do_it = True
if viewtype == 'media_duplicates_filter' and not new_options.GetBoolean( 'file_viewing_statistics_active_on_dupe_filter' ):
canvas_type = CC.CANVAS_MEDIA_VIEWER
do_it = False
if do_it:
if do_it:
# if a cap on max viewtime, cap it
if viewtime_max is not None:
media_min = new_options.GetNoneableInteger( 'file_viewing_statistics_media_min_time' )
media_max = new_options.GetNoneableInteger( 'file_viewing_statistics_media_max_time' )
viewtime_delta = min( viewtime_delta, viewtime_max )
if media_max is not None:
viewtime_delta = min( viewtime_delta, media_max )
# if a min on viewtime, then maybe don't do anything
if viewtime_min is None or viewtime_delta >= viewtime_min:
if media_min is None or viewtime_delta >= media_min:
media_views_delta = 1
media_viewtime_delta = viewtime_delta
result_views_delta = 1
result_viewtime_delta = viewtime_delta
return ( preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta )
return ( canvas_type, ( view_timestamp, result_views_delta, result_viewtime_delta ) )
def _RowMakesChanges( self, row ):
( preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) = row
( view_timestamp, views_delta, viewtime_delta ) = row
preview_change = preview_views_delta != 0 or preview_viewtime_delta != 0
media_change = media_views_delta != 0 or media_viewtime_delta != 0
return preview_change or media_change
return views_delta != 0 or viewtime_delta != 0
def _PubSubRow( self, hash, row ):
def _PubSubRow( self, hash, canvas_type, row ):
( preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) = row
( view_timestamp, views_delta, viewtime_delta ) = row
pubsub_row = ( hash, preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta )
pubsub_row = ( hash, canvas_type, view_timestamp, views_delta, viewtime_delta )
content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILE_VIEWING_STATS, HC.CONTENT_UPDATE_ADD, pubsub_row )
@ -332,11 +320,6 @@ class FileViewingStatsManager( object ):
HG.client_controller.pub( 'content_updates_gui', service_keys_to_content_updates )
def REPEATINGFlush( self ):
self.Flush()
def Flush( self ):
with self._lock:
@ -345,9 +328,9 @@ class FileViewingStatsManager( object ):
content_updates = []
for ( hash, ( preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) ) in self._pending_updates.items():
for ( ( hash, canvas_type ), ( view_timestamp, views_delta, viewtime_delta ) ) in self._pending_updates.items():
row = ( hash, preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta )
row = ( hash, canvas_type, view_timestamp, views_delta, viewtime_delta )
content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILE_VIEWING_STATS, HC.CONTENT_UPDATE_ADD, row )
@ -364,7 +347,7 @@ class FileViewingStatsManager( object ):
def FinishViewing( self, viewtype, hash, viewtime_delta ):
def FinishViewing( self, hash, canvas_type, view_timestamp, viewtime_delta ):
if not HG.client_controller.new_options.GetBoolean( 'file_viewing_statistics_active' ):
@ -373,28 +356,35 @@ class FileViewingStatsManager( object ):
with self._lock:
row = self._GenerateViewsRow( viewtype, viewtime_delta )
( canvas_type, row ) = self._GenerateViewsRow( canvas_type, view_timestamp, viewtime_delta )
if not self._RowMakesChanges( row ):
return
if hash not in self._pending_updates:
key = ( hash, canvas_type )
if key not in self._pending_updates:
self._pending_updates[ hash ] = row
self._pending_updates[ key ] = row
else:
( preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) = row
( view_timestamp, views_delta, viewtime_delta ) = row
( existing_preview_views_delta, existing_preview_viewtime_delta, existing_media_views_delta, existing_media_viewtime_delta ) = self._pending_updates[ hash ]
( existing_view_timestamp, existing_views_delta, existing_viewtime_delta ) = self._pending_updates[ key ]
self._pending_updates[ hash ] = ( existing_preview_views_delta + preview_views_delta, existing_preview_viewtime_delta + preview_viewtime_delta, existing_media_views_delta + media_views_delta, existing_media_viewtime_delta + media_viewtime_delta )
self._pending_updates[ key ] = ( max( view_timestamp, existing_view_timestamp ), existing_views_delta + views_delta, existing_viewtime_delta + viewtime_delta )
self._PubSubRow( hash, row )
self._PubSubRow( hash, canvas_type, row )
def REPEATINGFlush( self ):
self.Flush()
class UndoManager( object ):

View File

@ -210,7 +210,16 @@ class ImageRenderer( object ):
self._path = client_files_manager.GetFilePath( self._hash, self._mime )
self._numpy_image = ClientImageHandling.GenerateNumPyImage( self._path, self._mime )
try:
self._numpy_image = ClientImageHandling.GenerateNumPyImage( self._path, self._mime )
except Exception as e:
HydrusData.ShowText( 'Problem rendering image at "{}"! Error follows:'.format( self._path ) )
HydrusData.ShowException( e )
if not self._this_is_for_metadata_alone:

View File

@ -63,6 +63,8 @@ PREDICATE_TYPE_SYSTEM_NUM_NOTES = 38
PREDICATE_TYPE_SYSTEM_NOTES = 39
PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME = 40
PREDICATE_TYPE_SYSTEM_HAS_ICC_PROFILE = 41
PREDICATE_TYPE_SYSTEM_TIME = 42
PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME = 43
SYSTEM_PREDICATE_TYPES = {
PREDICATE_TYPE_SYSTEM_EVERYTHING,
@ -73,6 +75,7 @@ SYSTEM_PREDICATE_TYPES = {
PREDICATE_TYPE_SYSTEM_LIMIT,
PREDICATE_TYPE_SYSTEM_SIZE,
PREDICATE_TYPE_SYSTEM_AGE,
PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME,
PREDICATE_TYPE_SYSTEM_MODIFIED_TIME,
PREDICATE_TYPE_SYSTEM_HASH,
PREDICATE_TYPE_SYSTEM_WIDTH,
@ -100,7 +103,8 @@ SYSTEM_PREDICATE_TYPES = {
PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_COUNT,
PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_KING,
PREDICATE_TYPE_SYSTEM_KNOWN_URLS,
PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS
PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS,
PREDICATE_TYPE_SYSTEM_TIME
}
IGNORED_TAG_SEARCH_CHARACTERS = '[](){}/\\"\'-_'
@ -357,13 +361,18 @@ class FileSystemPredicates( object ):
self._common_info[ 'hash' ] = ( hashes, hash_type )
if predicate_type in ( PREDICATE_TYPE_SYSTEM_AGE, PREDICATE_TYPE_SYSTEM_MODIFIED_TIME ):
if predicate_type in ( PREDICATE_TYPE_SYSTEM_AGE, PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, PREDICATE_TYPE_SYSTEM_MODIFIED_TIME ):
if predicate_type == PREDICATE_TYPE_SYSTEM_AGE:
min_label = 'min_import_timestamp'
max_label = 'max_import_timestamp'
elif predicate_type == PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME:
min_label = 'min_last_viewed_timestamp'
max_label = 'max_last_viewed_timestamp'
elif predicate_type == PREDICATE_TYPE_SYSTEM_MODIFIED_TIME:
min_label = 'min_modified_timestamp'
@ -1591,7 +1600,7 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
self._value = ( tuple( [ bytes.fromhex( serialisable_hash ) for serialisable_hash in serialisable_hashes ] ), hash_type )
elif self._predicate_type in ( PREDICATE_TYPE_SYSTEM_AGE, PREDICATE_TYPE_SYSTEM_MODIFIED_TIME ):
elif self._predicate_type in ( PREDICATE_TYPE_SYSTEM_AGE, PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, PREDICATE_TYPE_SYSTEM_MODIFIED_TIME ):
( operator, age_type, age_value ) = serialisable_value
@ -1924,7 +1933,7 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
return False
if self._predicate_type in ( PREDICATE_TYPE_SYSTEM_AGE, PREDICATE_TYPE_SYSTEM_MODIFIED_TIME ):
if self._predicate_type in ( PREDICATE_TYPE_SYSTEM_AGE, PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, PREDICATE_TYPE_SYSTEM_MODIFIED_TIME ):
# age_type
if self._value[1] != ideal_value[1]:
@ -2018,6 +2027,7 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_LOCAL: base = 'local'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NOT_LOCAL: base = 'not local'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_DIMENSIONS: base = 'dimensions'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_TIME: base = 'time'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NOTES: base = 'notes'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS: base = 'file relationships'
elif self._predicate_type in ( PREDICATE_TYPE_SYSTEM_WIDTH, PREDICATE_TYPE_SYSTEM_HEIGHT, PREDICATE_TYPE_SYSTEM_NUM_NOTES, PREDICATE_TYPE_SYSTEM_NUM_WORDS, PREDICATE_TYPE_SYSTEM_NUM_FRAMES ):
@ -2199,11 +2209,15 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
base += ' is ' + HydrusData.ToHumanInt( value )
elif self._predicate_type in ( PREDICATE_TYPE_SYSTEM_AGE, PREDICATE_TYPE_SYSTEM_MODIFIED_TIME ):
elif self._predicate_type in ( PREDICATE_TYPE_SYSTEM_AGE, PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, PREDICATE_TYPE_SYSTEM_MODIFIED_TIME ):
if self._predicate_type == PREDICATE_TYPE_SYSTEM_AGE:
base = 'time imported'
base = 'import time'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME:
base = 'last view time'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_MODIFIED_TIME:

View File

@ -172,6 +172,7 @@ pred_generators = {
SystemPredicateParser.Predicate.URL_CLASS : lambda o, v, u: url_class_pred_generator( True, v ),
SystemPredicateParser.Predicate.NO_URL_CLASS : lambda o, v, u: url_class_pred_generator( False, v ),
SystemPredicateParser.Predicate.MOD_DATE : lambda o, v, u: date_pred_generator( ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME, o, v ),
SystemPredicateParser.Predicate.LAST_VIEWED_TIME : lambda o, v, u: date_pred_generator( ClientSearch.PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, o, v ),
SystemPredicateParser.Predicate.TIME_IMPORTED : lambda o, v, u: date_pred_generator( ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, o, v ),
SystemPredicateParser.Predicate.FILE_SERVICE : file_service_pred_generator,
SystemPredicateParser.Predicate.NUM_FILE_RELS : num_file_relationships_pred_generator

View File

@ -122,18 +122,7 @@ def CreateTopImage( width, title, payload_description, text ):
del painter
data_bytearray = top_qt_image.bits()
if QP.qtpy.PYSIDE2:
data_bytes = bytes( data_bytearray )
elif QP.qtpy.PYQT5:
data_bytes = data_bytearray.asstring( top_height * width * 3 )
top_image_rgb = numpy.fromstring( data_bytes, dtype = 'uint8' ).reshape( ( top_height, width, 3 ) )
top_image_rgb = ClientGUIFunctions.ConvertQtImageToNumPy( top_qt_image )
top_image = cv2.cvtColor( top_image_rgb, cv2.COLOR_RGB2GRAY )
@ -249,6 +238,15 @@ def GetPayloadDescriptionAndBytes( payload_obj ):
return ( payload_description, payload_bytes )
def LoadFromQtImage( qt_image: QG.QImage ):
# assume this for now
depth = 3
numpy_image = ClientGUIFunctions.ConvertQtImageToNumPy( qt_image )
return LoadFromNumPyImage( numpy_image )
def LoadFromPNG( path ):
# this is to deal with unicode paths, which cv2 can't handle
@ -282,7 +280,7 @@ def LoadFromPNG( path ):
HydrusData.ShowException( e )
raise Exception( 'That did not appear to be a valid image!' )
raise Exception( '"{}" did not appear to be a valid image!'.format( path ) )
@ -291,6 +289,10 @@ def LoadFromPNG( path ):
HydrusTemp.CleanUpTempPath( os_file_handle, temp_path )
return LoadFromNumPyImage( numpy_image )
def LoadFromNumPyImage( numpy_image: numpy.array ):
try:
height = numpy_image.shape[0]
@ -302,7 +304,7 @@ def LoadFromPNG( path ):
if depth != 1:
raise Exception( 'The file did not appear to be monochrome!' )
numpy_image = numpy_image[:,:,0] # let's fetch one channel. if the png is a perfect RGB conversion of the original (or, let's say, a Firefox bmp export), this actually works

View File

@ -2738,11 +2738,10 @@ class DB( HydrusDB.HydrusDB ):
self._Execute( 'CREATE TABLE IF NOT EXISTS url_map ( hash_id INTEGER, url_id INTEGER, PRIMARY KEY ( hash_id, url_id ) );' )
self._CreateIndex( 'url_map', [ 'url_id' ] )
self._Execute( 'CREATE TABLE IF NOT EXISTS file_viewing_stats ( hash_id INTEGER PRIMARY KEY, preview_views INTEGER, preview_viewtime INTEGER, media_views INTEGER, media_viewtime INTEGER );' )
self._CreateIndex( 'file_viewing_stats', [ 'preview_views' ] )
self._CreateIndex( 'file_viewing_stats', [ 'preview_viewtime' ] )
self._CreateIndex( 'file_viewing_stats', [ 'media_views' ] )
self._CreateIndex( 'file_viewing_stats', [ 'media_viewtime' ] )
self._Execute( 'CREATE TABLE IF NOT EXISTS file_viewing_stats ( hash_id INTEGER, canvas_type INTEGER, last_viewed_timestamp INTEGER, views INTEGER, viewtime INTEGER, PRIMARY KEY ( hash_id, canvas_type ) );' )
self._CreateIndex( 'file_viewing_stats', [ 'last_viewed_timestamp' ] )
self._CreateIndex( 'file_viewing_stats', [ 'views' ] )
self._CreateIndex( 'file_viewing_stats', [ 'viewtime' ] )
# inserts
@ -2927,22 +2926,22 @@ class DB( HydrusDB.HydrusDB ):
if media_min is not None:
self._Execute( 'UPDATE file_viewing_stats SET media_views = CAST( media_viewtime / ? AS INTEGER ) WHERE media_views * ? > media_viewtime;', ( media_min, media_min ) )
self._Execute( 'UPDATE file_viewing_stats SET views = CAST( viewtime / ? AS INTEGER ) WHERE views * ? > viewtime AND canvas_type = ?;', ( media_min, media_min, CC.CANVAS_MEDIA_VIEWER ) )
if media_max is not None:
self._Execute( 'UPDATE file_viewing_stats SET media_viewtime = media_views * ? WHERE media_viewtime > media_views * ?;', ( media_max, media_max ) )
self._Execute( 'UPDATE file_viewing_stats SET viewtime = views * ? WHERE viewtime > views * ? AND canvas_type = ?;', ( media_max, media_max, CC.CANVAS_MEDIA_VIEWER ) )
if preview_min is not None:
self._Execute( 'UPDATE file_viewing_stats SET preview_views = CAST( preview_viewtime / ? AS INTEGER ) WHERE preview_views * ? > preview_viewtime;', ( preview_min, preview_min ) )
self._Execute( 'UPDATE file_viewing_stats SET views = CAST( viewtime / ? AS INTEGER ) WHERE views * ? > viewtime AND canvas_type = ?;', ( preview_min, preview_min, CC.CANVAS_PREVIEW ) )
if preview_max is not None:
self._Execute( 'UPDATE file_viewing_stats SET preview_viewtime = preview_views * ? WHERE preview_viewtime > preview_views * ?;', ( preview_max, preview_max ) )
self._Execute( 'UPDATE file_viewing_stats SET viewtime = views * ? WHERE viewtime > views * ? AND canvas_type = ?;', ( preview_max, preview_max, CC.CANVAS_PREVIEW ) )
@ -4488,22 +4487,20 @@ class DB( HydrusDB.HydrusDB ):
boned_stats[ 'size_archive' ] = size_archive
boned_stats[ 'size_deleted' ] = size_deleted
total_viewtime = self._Execute( 'SELECT SUM( media_views ), SUM( media_viewtime ), SUM( preview_views ), SUM( preview_viewtime ) FROM file_viewing_stats;' ).fetchone()
canvas_types_to_total_viewtimes = { canvas_type : ( views, viewtime ) for ( canvas_type, views, viewtime ) in self._Execute( 'SELECT canvas_type, SUM( views ), SUM( viewtime ) FROM file_viewing_stats GROUP BY canvas_type;' ) }
if total_viewtime is None:
if CC.CANVAS_PREVIEW not in canvas_types_to_total_viewtimes:
total_viewtime = ( 0, 0, 0, 0 )
canvas_types_to_total_viewtimes[ CC.CANVAS_PREVIEW ] = ( 0, 0 )
else:
if CC.CANVAS_MEDIA_VIEWER not in canvas_types_to_total_viewtimes:
( media_views, media_viewtime, preview_views, preview_viewtime ) = total_viewtime
if media_views is None:
total_viewtime = ( 0, 0, 0, 0 )
canvas_types_to_total_viewtimes[ CC.CANVAS_MEDIA_VIEWER ] = ( 0, 0 )
total_viewtime = canvas_types_to_total_viewtimes[ CC.CANVAS_MEDIA_VIEWER ] + canvas_types_to_total_viewtimes[ CC.CANVAS_PREVIEW ]
#
earliest_import_time = 0
@ -4812,8 +4809,7 @@ class DB( HydrusDB.HydrusDB ):
blank_pred_types.update( [
ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE,
ClientSearch.PREDICATE_TYPE_SYSTEM_AGE,
ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME,
ClientSearch.PREDICATE_TYPE_SYSTEM_TIME,
ClientSearch.PREDICATE_TYPE_SYSTEM_DIMENSIONS,
ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION,
ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_AUDIO,
@ -5104,35 +5100,45 @@ class DB( HydrusDB.HydrusDB ):
include_media = 'media' in viewing_locations
include_preview = 'preview' in viewing_locations
group_by_phrase = ''
if view_type == 'views':
content_phrase = 'views'
elif view_type == 'viewtime':
content_phrase = 'viewtime'
if include_media and include_preview:
views_phrase = 'media_views + preview_views'
viewtime_phrase = 'media_viewtime + preview_viewtime'
group_by_phrase = ' GROUP BY hash_id'
if view_type == 'views':
content_phrase = 'SUM( views )'
elif view_type == 'viewtime':
content_phrase = 'SUM( viewtime )'
canvas_type_predicate = '1=1'
elif include_media:
views_phrase = 'media_views'
viewtime_phrase = 'media_viewtime'
canvas_type_predicate = 'canvas_type = {}'.format( CC.CANVAS_MEDIA_VIEWER )
elif include_preview:
views_phrase = 'preview_views'
viewtime_phrase = 'preview_viewtime'
canvas_type_predicate = 'canvas_type = {}'.format( CC.CANVAS_PREVIEW )
else:
return []
if view_type == 'views':
content_phrase = views_phrase
elif view_type == 'viewtime':
content_phrase = viewtime_phrase
if operator == CC.UNICODE_ALMOST_EQUAL_TO:
lower_bound = int( 0.8 * viewing_value )
@ -5145,7 +5151,7 @@ class DB( HydrusDB.HydrusDB ):
test_phrase = content_phrase + operator + str( viewing_value )
select_statement = 'SELECT hash_id FROM file_viewing_stats WHERE ' + test_phrase + ';'
select_statement = 'SELECT hash_id FROM file_viewing_stats WHERE {} AND {}{};'.format( test_phrase, canvas_type_predicate, group_by_phrase )
hash_ids = self._STS( self._Execute( select_statement ) )
@ -5662,6 +5668,20 @@ class DB( HydrusDB.HydrusDB ):
query_hash_ids = intersection_update_qhi( query_hash_ids, modified_timestamp_hash_ids )
last_viewed_timestamp_predicates = []
if 'min_last_viewed_timestamp' in simple_preds: last_viewed_timestamp_predicates.append( 'last_viewed_timestamp >= ' + str( simple_preds[ 'min_last_viewed_timestamp' ] ) )
if 'max_last_viewed_timestamp' in simple_preds: last_viewed_timestamp_predicates.append( 'last_viewed_timestamp <= ' + str( simple_preds[ 'max_last_viewed_timestamp' ] ) )
if len( last_viewed_timestamp_predicates ) > 0:
pred_string = ' AND '.join( last_viewed_timestamp_predicates )
last_viewed_timestamp_hash_ids = self._STS( self._Execute( 'SELECT hash_id FROM file_viewing_stats WHERE canvas_type = ? AND {};'.format( pred_string ), ( CC.CANVAS_MEDIA_VIEWER, ) ) )
query_hash_ids = intersection_update_qhi( query_hash_ids, last_viewed_timestamp_hash_ids )
#
if system_predicates.HasSimilarTo():
@ -7378,7 +7398,9 @@ class DB( HydrusDB.HydrusDB ):
hash_ids_to_names_and_notes = HydrusData.BuildKeyToListDict( ( ( hash_id, ( name, note ) ) for ( hash_id, name, note ) in self._Execute( 'SELECT file_notes.hash_id, label, note FROM {} CROSS JOIN file_notes USING ( hash_id ), labels, notes ON ( file_notes.name_id = labels.label_id AND file_notes.note_id = notes.note_id );'.format( temp_table_name ) ) ) )
hash_ids_to_file_viewing_stats_managers = { hash_id : ClientMediaManagers.FileViewingStatsManager( preview_views, preview_viewtime, media_views, media_viewtime ) for ( hash_id, preview_views, preview_viewtime, media_views, media_viewtime ) in self._Execute( 'SELECT hash_id, preview_views, preview_viewtime, media_views, media_viewtime FROM {} CROSS JOIN file_viewing_stats USING ( hash_id );'.format( temp_table_name ) ) }
hash_ids_to_file_viewing_stats = HydrusData.BuildKeyToListDict( ( ( hash_id, ( canvas_type, last_viewed_timestamp, views, viewtime ) ) for ( hash_id, canvas_type, last_viewed_timestamp, views, viewtime ) in self._Execute( 'SELECT hash_id, canvas_type, last_viewed_timestamp, views, viewtime FROM {} CROSS JOIN file_viewing_stats USING ( hash_id );'.format( temp_table_name ) ) ) )
hash_ids_to_file_viewing_stats_managers = { hash_id : ClientMediaManagers.FileViewingStatsManager( file_viewing_stats ) for ( hash_id, file_viewing_stats ) in hash_ids_to_file_viewing_stats.items() }
hash_ids_to_file_modified_timestamps = dict( self._Execute( 'SELECT hash_id, file_modified_timestamp FROM {} CROSS JOIN file_modified_timestamps USING ( hash_id );'.format( temp_table_name ) ) )
@ -9697,7 +9719,13 @@ class DB( HydrusDB.HydrusDB ):
reason = content_update.GetReason()
self.modules_files_storage.SetFileDeletionReason( hash_ids, reason )
# at the moment, we only set a deletion reason when a file leaves a real file domain. not on second delete from trash, so if file in trash, no new delete reason will be set
location_context = ClientLocation.LocationContext( current_service_keys = ( service_key, ) )
reason_setting_hash_ids = self.modules_files_storage.FilterHashIds( location_context, hash_ids )
self.modules_files_storage.SetFileDeletionReason( reason_setting_hash_ids, reason )
@ -9817,13 +9845,13 @@ class DB( HydrusDB.HydrusDB ):
elif action == HC.CONTENT_UPDATE_ADD:
( hash, preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) = row
( hash, canvas_type, view_timestamp, views_delta, viewtime_delta ) = row
hash_id = self.modules_hashes_local_cache.GetHashId( hash )
self._Execute( 'INSERT OR IGNORE INTO file_viewing_stats ( hash_id, preview_views, preview_viewtime, media_views, media_viewtime ) VALUES ( ?, ?, ?, ?, ? );', ( hash_id, 0, 0, 0, 0 ) )
self._Execute( 'INSERT OR IGNORE INTO file_viewing_stats ( hash_id, canvas_type, last_viewed_timestamp, views, viewtime ) VALUES ( ?, ?, ?, ?, ? );', ( hash_id, canvas_type, 0, 0, 0 ) )
self._Execute( 'UPDATE file_viewing_stats SET preview_views = preview_views + ?, preview_viewtime = preview_viewtime + ?, media_views = media_views + ?, media_viewtime = media_viewtime + ? WHERE hash_id = ?;', ( preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta, hash_id ) )
self._Execute( 'UPDATE file_viewing_stats SET last_viewed_timestamp = ?, views = views + ?, viewtime = viewtime + ? WHERE hash_id = ? AND canvas_type = ?;', ( view_timestamp, views_delta, viewtime_delta, hash_id, canvas_type ) )
elif action == HC.CONTENT_UPDATE_DELETE:
@ -12226,7 +12254,6 @@ class DB( HydrusDB.HydrusDB ):
sort_order = sort_by.sort_order
query = None
select_args_iterator = None
if sort_metadata == 'system':
@ -12245,6 +12272,7 @@ class DB( HydrusDB.HydrusDB ):
simple_sorts.append( CC.SORT_FILES_BY_MEDIA_VIEWTIME )
simple_sorts.append( CC.SORT_FILES_BY_APPROX_BITRATE )
simple_sorts.append( CC.SORT_FILES_BY_FILE_MODIFIED_TIMESTAMP )
simple_sorts.append( CC.SORT_FILES_BY_LAST_VIEWED_TIME )
if sort_data in simple_sorts:
@ -12263,58 +12291,60 @@ class DB( HydrusDB.HydrusDB ):
current_files_table_name = ClientDBFilesStorage.GenerateFilesTableName( file_service_id, HC.CONTENT_STATUS_CURRENT )
query = 'SELECT hash_id, timestamp FROM {} WHERE hash_id = ?;'.format( current_files_table_name )
query = 'SELECT hash_id, timestamp FROM {} CROSS JOIN {} USING ( hash_id );'.format( '{}', current_files_table_name )
elif sort_data == CC.SORT_FILES_BY_FILESIZE:
query = 'SELECT hash_id, size FROM files_info WHERE hash_id = ?;'
query = 'SELECT hash_id, size FROM {} CROSS JOIN files_info USING ( hash_id );'
elif sort_data == CC.SORT_FILES_BY_DURATION:
query = 'SELECT hash_id, duration FROM files_info WHERE hash_id = ?;'
query = 'SELECT hash_id, duration FROM {} CROSS JOIN files_info USING ( hash_id );'
elif sort_data == CC.SORT_FILES_BY_FRAMERATE:
query = 'SELECT hash_id, num_frames, duration FROM files_info WHERE hash_id = ?;'
query = 'SELECT hash_id, num_frames, duration FROM {} CROSS JOIN files_info USING ( hash_id );'
elif sort_data == CC.SORT_FILES_BY_NUM_FRAMES:
query = 'SELECT hash_id, num_frames FROM files_info WHERE hash_id = ?;'
query = 'SELECT hash_id, num_frames FROM {} CROSS JOIN files_info USING ( hash_id );'
elif sort_data == CC.SORT_FILES_BY_WIDTH:
query = 'SELECT hash_id, width FROM files_info WHERE hash_id = ?;'
query = 'SELECT hash_id, width FROM {} CROSS JOIN files_info USING ( hash_id );'
elif sort_data == CC.SORT_FILES_BY_HEIGHT:
query = 'SELECT hash_id, height FROM files_info WHERE hash_id = ?;'
query = 'SELECT hash_id, height FROM {} CROSS JOIN files_info USING ( hash_id );'
elif sort_data == CC.SORT_FILES_BY_RATIO:
query = 'SELECT hash_id, width, height FROM files_info WHERE hash_id = ?;'
query = 'SELECT hash_id, width, height FROM {} CROSS JOIN files_info USING ( hash_id );'
elif sort_data == CC.SORT_FILES_BY_NUM_PIXELS:
query = 'SELECT hash_id, width, height FROM files_info WHERE hash_id = ?;'
query = 'SELECT hash_id, width, height FROM {} CROSS JOIN files_info USING ( hash_id );'
elif sort_data == CC.SORT_FILES_BY_MEDIA_VIEWS:
query = 'SELECT hash_id, media_views FROM file_viewing_stats WHERE hash_id = ?;'
query = 'SELECT hash_id, views FROM {} CROSS JOIN file_viewing_stats USING ( hash_id ) WHERE canvas_type = {};'.format( '{}', CC.CANVAS_MEDIA_VIEWER )
elif sort_data == CC.SORT_FILES_BY_MEDIA_VIEWTIME:
query = 'SELECT hash_id, media_viewtime FROM file_viewing_stats WHERE hash_id = ?;'
query = 'SELECT hash_id, viewtime FROM {} CROSS JOIN file_viewing_stats USING ( hash_id ) WHERE canvas_type = {};'.format( '{}', CC.CANVAS_MEDIA_VIEWER )
elif sort_data == CC.SORT_FILES_BY_APPROX_BITRATE:
query = 'SELECT hash_id, duration, num_frames, size, width, height FROM files_info WHERE hash_id = ?;'
query = 'SELECT hash_id, duration, num_frames, size, width, height FROM {} CROSS JOIN files_info USING ( hash_id );'
elif sort_data == CC.SORT_FILES_BY_FILE_MODIFIED_TIMESTAMP:
query = 'SELECT hash_id, file_modified_timestamp FROM file_modified_timestamps WHERE hash_id = ?;'
query = 'SELECT hash_id, file_modified_timestamp FROM {} CROSS JOIN file_modified_timestamps USING ( hash_id );'
elif sort_data == CC.SORT_FILES_BY_LAST_VIEWED_TIME:
query = 'SELECT hash_id, last_viewed_timestamp FROM {} CROSS JOIN file_viewing_stats USING ( hash_id ) WHERE canvas_type = {};'.format( '{}', CC.CANVAS_MEDIA_VIEWER )
select_args_iterator = ( ( hash_id, ) for hash_id in hash_ids )
if sort_data == CC.SORT_FILES_BY_RATIO:
@ -12451,9 +12481,10 @@ class DB( HydrusDB.HydrusDB ):
if query is not None:
hash_ids_and_other_data = list( self._ExecuteManySelect( query, select_args_iterator ) )
hash_ids_and_other_data.sort( key = key, reverse = reverse )
with self._MakeTemporaryIntegerTable( hash_ids, 'hash_id' ) as temp_hash_ids_table_name:
hash_ids_and_other_data = sorted( self._Execute( query.format( temp_hash_ids_table_name ) ), key = key, reverse = reverse )
original_hash_ids = set( hash_ids )
@ -14177,6 +14208,30 @@ class DB( HydrusDB.HydrusDB ):
if version == 470:
( result, ) = self._Execute( 'SELECT sql FROM sqlite_master WHERE name = ?;', ( 'file_viewing_stats', ) ).fetchone()
if 'preview_views' in result:
self._controller.frame_splash_status.SetSubtext( 'reworking file viewing stats' )
self._Execute( 'ALTER TABLE file_viewing_stats RENAME TO file_viewing_stats_old;' )
self._Execute( 'CREATE TABLE IF NOT EXISTS file_viewing_stats ( hash_id INTEGER, canvas_type INTEGER, last_viewed_timestamp INTEGER, views INTEGER, viewtime INTEGER, PRIMARY KEY ( hash_id, canvas_type ) );' )
self._CreateIndex( 'file_viewing_stats', [ 'last_viewed_timestamp' ] )
self._CreateIndex( 'file_viewing_stats', [ 'views' ] )
self._CreateIndex( 'file_viewing_stats', [ 'viewtime' ] )
self._Execute( 'INSERT INTO file_viewing_stats SELECT hash_id, ?, ?, preview_views, preview_viewtime FROM file_viewing_stats_old;', ( CC.CANVAS_PREVIEW, None ) )
self._Execute( 'INSERT INTO file_viewing_stats SELECT hash_id, ?, ?, media_views, media_viewtime FROM file_viewing_stats_old;', ( CC.CANVAS_MEDIA_VIEWER, None ) )
self.modules_db_maintenance.AnalyzeTable( 'file_viewing_stats' )
self._Execute( 'DROP TABLE file_viewing_stats_old;' )
self._controller.frame_splash_status.SetTitleText( 'updated db to v{}'.format( HydrusData.ToHumanInt( version + 1 ) ) )
self._Execute( 'UPDATE version SET version = ?;', ( version + 1, ) )

View File

@ -316,20 +316,20 @@ class ClientDBFilesDuplicates( ClientDBModule.ClientDBModule ):
def DuplicatesFilterKingHashIds( self, allowed_hash_ids ):
# can't just pull explicit king_hash_ids, since files not in the system are considered king of their group
# can't just pull explicit king_hash_ids, since files that do not have a media_id are still kings
# kings = hashes - explicitly not kings
if not isinstance( allowed_hash_ids, set ):
allowed_hash_ids = set( allowed_hash_ids )
query = 'SELECT king_hash_id FROM duplicate_files WHERE king_hash_id = ?;'
explicit_king_hash_ids = self._STS( self._ExecuteManySelectSingleParam( query, allowed_hash_ids ) )
query = 'SELECT hash_id FROM duplicate_file_members WHERE hash_id = ?;'
all_duplicate_member_hash_ids = self._STS( self._ExecuteManySelectSingleParam( query, allowed_hash_ids ) )
with self._MakeTemporaryIntegerTable( allowed_hash_ids, 'hash_id' ) as temp_hash_ids_table_name:
explicit_king_hash_ids = self._STS( self._Execute( 'SELECT king_hash_id FROM {} CROSS JOIN duplicate_files ON ( {}.hash_id = duplicate_files.king_hash_id );'.format( temp_hash_ids_table_name, temp_hash_ids_table_name ) ) )
all_duplicate_member_hash_ids = self._STS( self._Execute( 'SELECT hash_id FROM {} CROSS JOIN duplicate_file_members USING ( hash_id );'.format( temp_hash_ids_table_name ) ) )
all_non_king_hash_ids = all_duplicate_member_hash_ids.difference( explicit_king_hash_ids )

View File

@ -676,12 +676,6 @@ class ClientDBSimilarFiles( ClientDBModule.ClientDBModule ):
# the crash was in sqlite code, again presumably on subsequent fetch
# adding a delay in seemed to fix it as well. guess it was some memory maintenance buffer/bytes thing
# anyway, we now just get the whole lot of results first and then work on the whole lot
'''
#old method
select_statement = 'SELECT phash_id, phash, radius, inner_id, outer_id FROM shape_perceptual_hashes NATURAL JOIN shape_vptree WHERE phash_id = ?;'
results = list( self._ExecuteManySelectSingleParam( select_statement, group_of_current_potentials ) )
'''
with self._MakeTemporaryIntegerTable( group_of_current_potentials, 'phash_id' ) as temp_table_name:
@ -786,7 +780,7 @@ class ClientDBSimilarFiles( ClientDBModule.ClientDBModule ):
similar_hash_ids_and_distances = list( similar_hash_ids_to_distances.items() )
similar_hash_ids_and_distances.extend( similar_hash_ids_to_distances.items() )
similar_hash_ids_and_distances = HydrusData.DedupeList( similar_hash_ids_and_distances )

View File

@ -653,7 +653,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
elif qtpy.PYQT5:
from PyQt5.Qt import PYQT_VERSION_STR # pylint: disable=E0401
from PyQt5.Qt import PYQT_VERSION_STR # pylint: disable=E0401,E0611
from sip import SIP_VERSION_STR # pylint: disable=E0401
library_versions.append( ( 'PyQt5', PYQT_VERSION_STR ) )
@ -979,7 +979,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
def _ClearFileViewingStats( self ):
text = 'Are you sure you want to delete _all_ file viewing records? This cannot be undone.'
text = 'Are you sure you want to delete _all_ file view count/duration and \'last time viewed\' records? This cannot be undone.'
result = ClientGUIDialogsQuick.GetYesNo( self, text, yes_label = 'do it', no_label = 'forget it' )

View File

@ -1,3 +1,4 @@
import numpy
import typing
from qtpy import QtCore as QC
@ -65,6 +66,37 @@ def ConvertPixelsToTextWidth( window, pixels, round_down = False ) -> int:
return round( pixels / one_char_width )
def ConvertQtImageToNumPy( qt_image: QG.QImage ):
width = qt_image.width()
height = qt_image.height()
if qt_image.depth() == 1:
# this is probably super wrong, but whatever for now
depth = 1
else:
# 8, 24, 32 etc...
depth = qt_image.depth() // 8
data_bytearray = qt_image.bits()
if QP.qtpy.PYSIDE2:
data_bytes = bytes( data_bytearray )
elif QP.qtpy.PYQT5:
data_bytes = data_bytearray.asstring( height * width * depth )
numpy_image = numpy.fromstring( data_bytes, dtype = 'uint8' ).reshape( ( height, width, depth ) )
return numpy_image
def ConvertTextToPixels( window, char_dimensions ) -> typing.Tuple[ int, int ]:
( char_cols, char_rows ) = char_dimensions

View File

@ -12,6 +12,7 @@ from hydrus.core import HydrusImageHandling
from hydrus.core import HydrusPaths
from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
from hydrus.client.gui import ClientGUIMedia
from hydrus.client.gui import ClientGUIMediaControls
from hydrus.client.gui import ClientGUIShortcuts
@ -81,7 +82,7 @@ class mpvWidget( QW.QWidget ):
QW.QWidget.__init__( self, parent )
self._canvas_type = ClientGUICommon.CANVAS_PREVIEW
self._canvas_type = CC.CANVAS_PREVIEW
self._stop_for_slideshow = False
@ -143,14 +144,14 @@ class mpvWidget( QW.QWidget ):
def _GetAudioOptionNames( self ):
if self._canvas_type == ClientGUICommon.CANVAS_MEDIA_VIEWER:
if self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
if HG.client_controller.new_options.GetBoolean( 'media_viewer_uses_its_own_audio_volume' ):
return ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_MEDIA_VIEWER ]
elif self._canvas_type == ClientGUICommon.CANVAS_PREVIEW:
elif self._canvas_type == CC.CANVAS_PREVIEW:
if HG.client_controller.new_options.GetBoolean( 'preview_uses_its_own_audio_volume' ):
@ -167,11 +168,11 @@ class mpvWidget( QW.QWidget ):
mute_option_name = global_mute_option_name
if self._canvas_type == ClientGUICommon.CANVAS_MEDIA_VIEWER:
if self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
( mute_option_name, volume_option_name ) = ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_MEDIA_VIEWER ]
elif self._canvas_type == ClientGUICommon.CANVAS_PREVIEW:
elif self._canvas_type == CC.CANVAS_PREVIEW:
( mute_option_name, volume_option_name ) = ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_PREVIEW ]
@ -183,14 +184,14 @@ class mpvWidget( QW.QWidget ):
( mute_option_name, volume_option_name ) = ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_GLOBAL ]
if self._canvas_type == ClientGUICommon.CANVAS_MEDIA_VIEWER:
if self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
if HG.client_controller.new_options.GetBoolean( 'media_viewer_uses_its_own_audio_volume' ):
( mute_option_name, volume_option_name ) = ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_MEDIA_VIEWER ]
elif self._canvas_type == ClientGUICommon.CANVAS_PREVIEW:
elif self._canvas_type == CC.CANVAS_PREVIEW:
if HG.client_controller.new_options.GetBoolean( 'preview_uses_its_own_audio_volume' ):
@ -384,11 +385,11 @@ class mpvWidget( QW.QWidget ):
ClientGUIMedia.OpenExternally( self._media )
elif action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER and self._canvas_type == ClientGUICommon.CANVAS_MEDIA_VIEWER:
elif action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER and self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
self.window().close()
elif action == CAC.SIMPLE_LAUNCH_MEDIA_VIEWER and self._canvas_type == ClientGUICommon.CANVAS_PREVIEW:
elif action == CAC.SIMPLE_LAUNCH_MEDIA_VIEWER and self._canvas_type == CC.CANVAS_PREVIEW:
self.launchMediaViewer.emit()
@ -455,7 +456,7 @@ class mpvWidget( QW.QWidget ):
self._canvas_type = canvas_type
if self._canvas_type == ClientGUICommon.CANVAS_MEDIA_VIEWER:
if self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
shortcut_set = 'media_viewer_media_window'

View File

@ -141,7 +141,7 @@ def DoClearFileViewingStats( win: QW.QWidget, flat_medias: typing.Collection[ Cl
insert = 'these {} files'.format( HydrusData.ToHumanInt( len( flat_medias ) ) )
message = 'Clear the file viewing stats for {}?'.format( insert )
message = 'Clear the file viewing count/duration and \'last viewed time\' for {}?'.format( insert )
result = ClientGUIDialogsQuick.GetYesNo( win, message )
@ -362,14 +362,14 @@ def AddFileViewingStatsMenu( menu, medias: typing.Collection[ ClientMedia.Media
if view_style == CC.FILE_VIEWING_STATS_MENU_DISPLAY_MEDIA_AND_PREVIEW_SUMMED:
combined_line = fvsm.GetPrettyCombinedLine()
combined_line = fvsm.GetPrettyViewsLine( ( CC.CANVAS_MEDIA_VIEWER, CC.CANVAS_PREVIEW ) )
ClientGUIMenus.AppendMenuLabel( menu, combined_line )
else:
media_line = fvsm.GetPrettyMediaLine()
preview_line = fvsm.GetPrettyPreviewLine()
media_line = fvsm.GetPrettyViewsLine( ( CC.CANVAS_MEDIA_VIEWER, ) )
preview_line = fvsm.GetPrettyViewsLine( ( CC.CANVAS_PREVIEW, ) )
if view_style == CC.FILE_VIEWING_STATS_MENU_DISPLAY_MEDIA_ONLY:

View File

@ -150,7 +150,7 @@ class VolumeControl( QW.QWidget ):
self.setAttribute( QC.Qt.WA_ShowWithoutActivating )
if self._canvas_type == ClientGUICommon.CANVAS_MEDIA_VIEWER:
if self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
option_to_use = 'media_viewer_uses_its_own_audio_volume'
volume_type = AUDIO_MEDIA_VIEWER
@ -163,7 +163,7 @@ class VolumeControl( QW.QWidget ):
self._specific_mute = AudioMuteButton( self, volume_type )
self._specific_mute.setToolTip( 'Mute/unmute: {}'.format( ClientGUICommon.canvas_str_lookup[ self._canvas_type ] ) )
self._specific_mute.setToolTip( 'Mute/unmute: {}'.format( CC.canvas_type_str_lookup[ self._canvas_type ] ) )
if HG.client_controller.new_options.GetBoolean( option_to_use ):

View File

@ -456,6 +456,7 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
else:
self._action_radio.hide()
self._reason_panel.hide()
@ -511,11 +512,18 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
def _GetReason( self ):
reason = self._reason_radio.GetValue()
if reason is None:
if self._reason_panel.isEnabled():
reason = self._custom_reason.text()
reason = self._reason_radio.GetValue()
if reason is None:
reason = self._custom_reason.text()
else:
reason = None
return reason
@ -638,27 +646,27 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
( file_service_key, hashes, description ) = self._action_radio.GetValue()
reason_permitted = file_service_key in ( CC.LOCAL_FILE_SERVICE_KEY, 'physical_delete' )
# 'this includes service keys' because if we are deleting physically from the trash, then reason is already set
reason_permitted = file_service_key in ( CC.LOCAL_FILE_SERVICE_KEY, 'physical_delete' ) and self._this_dialog_includes_service_keys
if reason_permitted:
self._reason_radio.setEnabled( True )
self._reason_panel.setEnabled( True )
reason = self._reason_radio.GetValue()
if reason is None:
self._custom_reason.setEnabled( True )
else:
self._custom_reason.setEnabled( False )
else:
self._reason_radio.setEnabled( False )
self._custom_reason.setEnabled( False )
reason = self._reason_radio.GetValue()
if reason is None:
self._custom_reason.setEnabled( True )
else:
self._custom_reason.setEnabled( False )
self._reason_panel.setEnabled( False )

View File

@ -10,6 +10,7 @@ from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
from hydrus.core import HydrusCompression
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
@ -1762,6 +1763,9 @@ class ReviewDownloaderImport( ClientGUIScrolledPanels.ReviewPanel ):
self._repo_link = ClientGUICommon.BetterHyperLink( self, 'get user-made downloaders here', 'https://github.com/CuddleBear92/Hydrus-Presets-and-Scripts/tree/master/Downloaders' )
self._paste_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().paste, self._Paste )
self._paste_button.setToolTip( 'Or you can paste bitmaps from clipboard!' )
st = ClientGUICommon.BetterStaticText( self, label = 'Drop downloader-encoded pngs onto Lain to import.' )
lain_path = os.path.join( HC.STATIC_DIR, 'lain.jpg' )
@ -1782,6 +1786,7 @@ class ReviewDownloaderImport( ClientGUIScrolledPanels.ReviewPanel ):
QP.AddToLayout( vbox, help_hbox, CC.FLAGS_ON_RIGHT )
QP.AddToLayout( vbox, self._repo_link, CC.FLAGS_CENTER )
QP.AddToLayout( vbox, st, CC.FLAGS_CENTER )
QP.AddToLayout( vbox, self._paste_button, CC.FLAGS_ON_RIGHT )
QP.AddToLayout( vbox, win, CC.FLAGS_CENTER )
QP.AddToLayout( vbox, ClientGUICommon.WrapInText( self._select_from_list, self, 'select objects from list' ), CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
@ -1797,19 +1802,7 @@ class ReviewDownloaderImport( ClientGUIScrolledPanels.ReviewPanel ):
def _ImportPaths( self, paths ):
have_shown_load_error = False
gugs = []
url_classes = []
parsers = []
domain_metadatas = []
login_scripts = []
num_misc_objects = 0
bandwidth_manager = self._network_engine.bandwidth_manager
domain_manager = self._network_engine.domain_manager
login_manager = self._network_engine.login_manager
payloads = []
for path in paths:
@ -1824,6 +1817,30 @@ class ReviewDownloaderImport( ClientGUIScrolledPanels.ReviewPanel ):
return
payloads.append( ( 'file "{}"'.format( path ), payload ) )
self._ImportPayloads( payloads )
def _ImportPayloads( self, payloads ):
have_shown_load_error = False
gugs = []
url_classes = []
parsers = []
domain_metadatas = []
login_scripts = []
num_misc_objects = 0
bandwidth_manager = self._network_engine.bandwidth_manager
domain_manager = self._network_engine.domain_manager
login_manager = self._network_engine.login_manager
for ( payload_description, payload ) in payloads:
try:
obj_list = HydrusSerialisable.CreateFromNetworkBytes( payload, raise_error_on_future_version = True )
@ -1834,7 +1851,7 @@ class ReviewDownloaderImport( ClientGUIScrolledPanels.ReviewPanel ):
message = str( e )
if len( paths ) > 1:
if len( payloads ) > 1:
message += os.linesep * 2
message += 'If there are more unloadable objects in this import, they will be skipped silently.'
@ -1849,7 +1866,7 @@ class ReviewDownloaderImport( ClientGUIScrolledPanels.ReviewPanel ):
except:
QW.QMessageBox.critical( self, 'Error', 'I could not understand what was encoded in the file '+path+'!' )
QW.QMessageBox.critical( self, 'Error', 'I could not understand what was encoded in {}!'.format( payload_description ) )
continue
@ -1861,7 +1878,7 @@ class ReviewDownloaderImport( ClientGUIScrolledPanels.ReviewPanel ):
if not isinstance( obj_list, HydrusSerialisable.SerialisableList ):
QW.QMessageBox.warning( self, 'Warning', 'Unfortunately, '+path+' did not look like a package of download data! Instead, it looked like: '+obj_list.SERIALISABLE_NAME )
QW.QMessageBox.warning( self, 'Warning', 'Unfortunately, {} did not look like a package of download data! Instead, it looked like: {}'.format( payload_description, obj_list.SERIALISABLE_NAME ) )
continue
@ -2190,6 +2207,46 @@ class ReviewDownloaderImport( ClientGUIScrolledPanels.ReviewPanel ):
QW.QMessageBox.information( self, 'Information', final_message )
def _Paste( self ):
if HG.client_controller.ClipboardHasImage():
try:
qt_image = HG.client_controller.GetClipboardImage()
payload_description = 'clipboard image data'
payload = ClientSerialisable.LoadFromQtImage( qt_image )
except Exception as e:
QW.QMessageBox.critical( self, 'Error', 'Sorry, seemed to be a problem: {}'.format( str( e ) ) )
return
else:
try:
raw_text = HG.client_controller.GetClipboardText()
payload_description = 'clipboard text data'
payload = HydrusCompression.CompressStringToBytes( raw_text )
except HydrusExceptions.DataMissing as e:
QW.QMessageBox.critical( self, 'Error', str(e) )
return
payloads = [ ( payload_description, payload ) ]
self._ImportPayloads( payloads )
def EventLainClick( self, event ):
with QP.FileDialog( self, 'Select the pngs to add.', acceptMode = QW.QFileDialog.AcceptOpen, fileMode = QW.QFileDialog.ExistingFiles ) as dlg:

View File

@ -1059,6 +1059,8 @@ class ShortcutsHandler( QC.QObject ):
self._catch_mouse = catch_mouse
self._last_click_down_position = QC.QPoint( 0, 0 )
filter_target = parent
if alternate_filter_target is not None:
@ -1201,6 +1203,11 @@ class ShortcutsHandler( QC.QObject ):
if event.type() in ( QC.QEvent.MouseButtonPress, QC.QEvent.MouseButtonRelease, QC.QEvent.MouseButtonDblClick, QC.QEvent.Wheel ):
if event.type() == QC.QEvent.MouseButtonPress:
self._last_click_down_position = event.globalPos()
if event.type() != QC.QEvent.Wheel and self._ignore_activating_mouse_click and not HydrusData.TimeHasPassedPrecise( self._frame_activated_time + 0.017 ):
if event.type() == QC.QEvent.MouseButtonRelease:
@ -1211,6 +1218,22 @@ class ShortcutsHandler( QC.QObject ):
return False
if event.type() == QC.QEvent.MouseButtonRelease:
release_press_pos = event.globalPos()
delta = release_press_pos - self._last_click_down_position
approx_distance = delta.manhattanLength()
# if mouse release is some distance from mouse down (i.e. we are ending a drag), then don't fire off a release command
if approx_distance > 20:
return False
i_should_catch_shortcut_event = IShouldCatchShortcutEvent( self._filter_target, watched, event = event )
shortcut = ConvertMouseEventToShortcut( event )

View File

@ -5,6 +5,7 @@ import os
# If not explicitely set, prefer PySide2 instead of the qtpy default which is PyQt5
# It is important that this runs on startup *before* anything is imported from qtpy.
# Since test.py, client.py and client.pyw all import this module first before any other Qt related ones, this requirement is satisfied.
if not 'QT_API' in os.environ:
try:

View File

@ -64,7 +64,7 @@ def AddAudioVolumeMenu( menu, canvas_type ):
mute_volume_type = None
volume_volume_type = ClientGUIMediaControls.AUDIO_GLOBAL
if canvas_type == ClientGUICommon.CANVAS_MEDIA_VIEWER:
if canvas_type == CC.CANVAS_MEDIA_VIEWER:
mute_volume_type = ClientGUIMediaControls.AUDIO_MEDIA_VIEWER
@ -73,7 +73,7 @@ def AddAudioVolumeMenu( menu, canvas_type ):
volume_volume_type = ClientGUIMediaControls.AUDIO_MEDIA_VIEWER
elif canvas_type == ClientGUICommon.CANVAS_PREVIEW:
elif canvas_type == CC.CANVAS_PREVIEW:
mute_volume_type = ClientGUIMediaControls.AUDIO_PREVIEW
@ -343,6 +343,7 @@ def CalculateMediaSize( media, zoom ):
class Canvas( QW.QWidget ):
PREVIEW_WINDOW = False
CANVAS_TYPE = CC.CANVAS_MEDIA_VIEWER
def __init__( self, parent, location_context: ClientLocation.LocationContext ):
@ -364,15 +365,6 @@ class Canvas( QW.QWidget ):
self._current_media = None
if self.PREVIEW_WINDOW:
self._canvas_type = ClientGUICommon.CANVAS_PREVIEW
else:
self._canvas_type = ClientGUICommon.CANVAS_MEDIA_VIEWER
catch_mouse = True
# once we have catch_mouse full shortcut support for canvases, swap out this out for an option to swallow activating clicks
@ -384,7 +376,7 @@ class Canvas( QW.QWidget ):
self.installEventFilter( self._click_drag_reporting_filter )
self._media_container = ClientGUICanvasMedia.MediaContainer( self, self._canvas_type, self._click_drag_reporting_filter )
self._media_container = ClientGUICanvasMedia.MediaContainer( self, self.CANVAS_TYPE, self._click_drag_reporting_filter )
self._current_zoom = 1.0
self._canvas_zoom = 1.0
@ -1059,6 +1051,8 @@ class Canvas( QW.QWidget ):
now = HydrusData.GetNow()
view_timestamp = self._current_media_start_time
viewtime_delta = now - self._current_media_start_time
self._current_media_start_time = now
@ -1068,25 +1062,9 @@ class Canvas( QW.QWidget ):
return
if self.PREVIEW_WINDOW:
viewtype = 'preview'
else:
if isinstance( self, CanvasFilterDuplicates ):
viewtype = 'media_duplicates_filter'
else:
viewtype = 'media'
hash = self._current_media.GetHash()
HG.client_controller.file_viewing_stats_manager.FinishViewing( viewtype, hash, viewtime_delta )
HG.client_controller.file_viewing_stats_manager.FinishViewing( hash, self.CANVAS_TYPE, view_timestamp, viewtime_delta )
def _SeekDeltaCurrentMedia( self, direction, duration_ms ):
@ -1858,6 +1836,7 @@ class MediaContainerDragClickReportingFilter( QC.QObject ):
class CanvasPanel( Canvas ):
PREVIEW_WINDOW = True
CANVAS_TYPE = CC.CANVAS_PREVIEW
def __init__( self, parent, page_key, location_context: ClientLocation.LocationContext ):
@ -1947,7 +1926,7 @@ class CanvasPanel( Canvas ):
ClientGUIMenus.AppendSeparator( menu )
AddAudioVolumeMenu( menu, self._canvas_type )
AddAudioVolumeMenu( menu, self.CANVAS_TYPE )
if self._current_media is not None:
@ -2647,6 +2626,8 @@ class CanvasWithHovers( CanvasWithDetails ):
class CanvasFilterDuplicates( CanvasWithHovers ):
CANVAS_TYPE = CC.CANVAS_MEDIA_VIEWER_DUPLICATES
def __init__( self, parent, file_search_context: ClientSearch.FileSearchContext, both_files_match, pixel_dupes_preference, max_hamming_distance ):
location_context = file_search_context.GetLocationContext()
@ -4414,7 +4395,7 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
ClientGUIMenus.AppendMenu( menu, zoom_menu, 'current zoom: {}'.format( ClientData.ConvertZoomToPercentage( self._current_zoom ) ) )
AddAudioVolumeMenu( menu, self._canvas_type )
AddAudioVolumeMenu( menu, self.CANVAS_TYPE )
if self.parentWidget().isFullScreen():

View File

@ -904,7 +904,7 @@ class CanvasHoverFrameTop( CanvasHoverFrame ):
zoom_switch = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().zoom_switch, HG.client_controller.pub, 'canvas_application_command', CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_SWITCH_BETWEEN_100_PERCENT_AND_CANVAS_ZOOM_VIEWER_CENTER ), self._canvas_key )
zoom_switch.SetToolTipWithShortcuts( 'zoom switch', CAC.SIMPLE_SWITCH_BETWEEN_100_PERCENT_AND_CANVAS_ZOOM )
self._volume_control = ClientGUIMediaControls.VolumeControl( self, ClientGUICommon.CANVAS_MEDIA_VIEWER )
self._volume_control = ClientGUIMediaControls.VolumeControl( self, CC.CANVAS_MEDIA_VIEWER )
if not ClientGUIMPV.MPV_IS_AVAILABLE:

View File

@ -96,7 +96,7 @@ class Animation( QW.QWidget ):
self._canvas_qt_pixmap = None
if self._canvas_type == ClientGUICommon.CANVAS_MEDIA_VIEWER:
if self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
shortcut_set = 'media_viewer_media_window'
@ -335,11 +335,11 @@ class Animation( QW.QWidget ):
ClientGUIMedia.OpenExternally( self._media )
elif action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER and self._canvas_type == ClientGUICommon.CANVAS_MEDIA_VIEWER:
elif action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER and self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
self.window().close()
elif action == CAC.SIMPLE_LAUNCH_MEDIA_VIEWER and self._canvas_type == ClientGUICommon.CANVAS_PREVIEW:
elif action == CAC.SIMPLE_LAUNCH_MEDIA_VIEWER and self._canvas_type == CC.CANVAS_PREVIEW:
self.launchMediaViewer.emit()
@ -569,6 +569,14 @@ class AnimationBar( QW.QWidget ):
QW.QWidget.__init__( self, parent )
self._colours = {
'hab_border' : QG.QColor( 0, 0, 0 ),
'hab_background' : QG.QColor( 240, 240, 240 ),
'hab_nub' : QG.QColor( 96, 96, 96 )
}
self.setObjectName( 'HydrusAnimationBar' )
self.setCursor( QG.QCursor( QC.Qt.ArrowCursor ) )
self.setSizePolicy( QW.QSizePolicy.Fixed, QW.QSizePolicy.Fixed )
@ -584,9 +592,13 @@ class AnimationBar( QW.QWidget ):
def _DrawBlank( self, painter ):
self.setProperty( 'playing', False )
new_options = HG.client_controller.new_options
painter.setBackground( QP.GetSystemColour( QG.QPalette.Button ) )
background_colour = self._colours[ 'hab_background' ]
painter.setBackground( background_colour )
painter.eraseRect( painter.viewport() )
@ -638,12 +650,14 @@ class AnimationBar( QW.QWidget ):
( current_frame_index, current_timestamp_ms, paused, buffer_indices ) = self._last_drawn_info
self.setProperty( 'playing', not paused )
my_width = self.size().width()
my_height = self.size().height()
painter.setPen( QC.Qt.NoPen )
background_colour = QP.GetSystemColour( QG.QPalette.Button )
background_colour = self._colours[ 'hab_background' ]
if paused:
@ -708,7 +722,7 @@ class AnimationBar( QW.QWidget ):
painter.setBrush( QG.QBrush( QP.GetSystemColour( QG.QPalette.Shadow ) ) )
painter.setBrush( QG.QBrush( self._colours[ 'hab_nub' ] ) )
animated_scanbar_nub_width = HG.client_controller.new_options.GetInteger( 'animated_scanbar_nub_width' )
@ -761,7 +775,8 @@ class AnimationBar( QW.QWidget ):
#
painter.setBrush( QC.Qt.NoBrush )
painter.setPen( QG.QPen( QP.GetSystemColour( QG.QPalette.Shadow ) ) )
painter.setPen( QG.QPen( self._colours[ 'hab_border' ] ) )
painter.drawRect( 0, 0, my_width - 1, my_height - 1 )
@ -932,6 +947,40 @@ class AnimationBar( QW.QWidget ):
def get_hab_background( self ):
return self._colours[ 'hab_background' ]
def get_hab_border( self ):
return self._colours[ 'hab_border' ]
def get_hab_nub( self ):
return self._colours[ 'hab_nub' ]
def set_hab_background( self, colour ):
self._colours[ 'hab_background' ] = colour
def set_hab_border( self, colour ):
self._colours[ 'hab_border' ] = colour
def set_hab_nub( self, colour ):
self._colours[ 'hab_nub' ] = colour
hab_border = QC.Property( QG.QColor, get_hab_border, set_hab_border )
hab_background = QC.Property( QG.QColor, get_hab_background, set_hab_background )
hab_nub = QC.Property( QG.QColor, get_hab_nub, set_hab_nub )
class MediaContainer( QW.QWidget ):
launchMediaViewer = QC.Signal()
@ -1711,7 +1760,7 @@ class StaticImage( QW.QWidget ):
self._zoom = 1.0
if self._canvas_type == ClientGUICommon.CANVAS_MEDIA_VIEWER:
if self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
shortcut_set = 'media_viewer_media_window'
@ -2003,11 +2052,11 @@ class StaticImage( QW.QWidget ):
ClientGUIMedia.OpenExternally( self._media )
elif action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER and self._canvas_type == ClientGUICommon.CANVAS_MEDIA_VIEWER:
elif action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER and self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
self.window().close()
elif action == CAC.SIMPLE_LAUNCH_MEDIA_VIEWER and self._canvas_type == ClientGUICommon.CANVAS_PREVIEW:
elif action == CAC.SIMPLE_LAUNCH_MEDIA_VIEWER and self._canvas_type == CC.CANVAS_PREVIEW:
self.launchMediaViewer.emit()

View File

@ -1155,30 +1155,75 @@ class BetterListCtrlPanel( QW.QWidget ):
def _ImportFromClipboard( self ):
try:
if HG.client_controller.ClipboardHasImage():
raw_text = HG.client_controller.GetClipboardText()
try:
qt_image = HG.client_controller.GetClipboardImage()
except:
# no image on clipboard obviously
do_text = True
except HydrusExceptions.DataMissing as e:
try:
payload = ClientSerialisable.LoadFromQtImage( qt_image )
obj = HydrusSerialisable.CreateFromNetworkBytes( payload, raise_error_on_future_version = True )
except HydrusExceptions.SerialisationException as e:
QW.QMessageBox.critical( self, 'Problem loading', 'Problem loading that object: {}'.format( str( e ) ) )
return
except Exception as e:
QW.QMessageBox.critical( self, 'Error', 'I could not understand what was in the clipboard: {}'.format( str( e ) ) )
return
QW.QMessageBox.critical( self, 'Error', str(e) )
else:
return
try:
raw_text = HG.client_controller.GetClipboardText()
except HydrusExceptions.DataMissing as e:
QW.QMessageBox.critical( self, 'Error', str(e) )
return
try:
obj = HydrusSerialisable.CreateFromString( raw_text, raise_error_on_future_version = True )
except HydrusExceptions.SerialisationException as e:
QW.QMessageBox.critical( self, 'Problem loading', 'Problem loading that object: {}'.format( str( e ) ) )
return
except Exception as e:
QW.QMessageBox.critical( self, 'Error', 'I could not understand what was in the clipboard: {}'.format( str( e ) ) )
return
try:
obj = HydrusSerialisable.CreateFromString( raw_text, raise_error_on_future_version = True )
self._ImportObject( obj )
except HydrusExceptions.SerialisationException as e:
QW.QMessageBox.critical( self, 'Problem loading', str( e ) )
except Exception as e:
QW.QMessageBox.critical( self, 'Error', 'I could not understand what was in the clipboard' )
QW.QMessageBox.critical( self, 'Error', 'Problem importing: {}'.format( str( e ) ) )
self._listctrl.Sort()

View File

@ -256,7 +256,7 @@ class MediaSortControl( QW.QWidget ):
submetatypes_to_menus = {}
for system_sort_type in CC.SYSTEM_SORT_TYPES:
for system_sort_type in CC.SYSTEM_SORT_TYPES_SORT_CONTROL_SORTED:
sort_type = ( 'system', system_sort_type )

View File

@ -198,7 +198,7 @@ class PanelPredicateSystemAgeDate( PanelPredicateSystemSingle ):
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:time imported'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:import time'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._sign, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._date, CC.FLAGS_CENTER_PERPENDICULAR )
@ -263,7 +263,7 @@ class PanelPredicateSystemAgeDelta( PanelPredicateSystemSingle ):
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:time imported'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:import time'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._sign, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._years, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'years'), CC.FLAGS_CENTER_PERPENDICULAR )
@ -291,6 +291,125 @@ class PanelPredicateSystemAgeDelta( PanelPredicateSystemSingle ):
return predicates
class PanelPredicateSystemLastViewedDate( PanelPredicateSystemSingle ):
def __init__( self, parent, predicate ):
PanelPredicateSystemSingle.__init__( self, parent )
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'=','>'] )
self._date = QW.QCalendarWidget( self )
#
predicate = self._GetPredicateToInitialisePanelWith( predicate )
( sign, age_type, ( years, months, days ) ) = predicate.GetValue()
self._sign.SetStringSelection( sign )
qt_dt = QC.QDate( years, months, days )
self._date.setSelectedDate( qt_dt )
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:last viewed date'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._sign, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._date, CC.FLAGS_CENTER_PERPENDICULAR )
hbox.addStretch( 1 )
self.setLayout( hbox )
def GetDefaultPredicate( self ) -> ClientSearch.Predicate:
qt_dt = QC.QDate.currentDate()
qt_dt.addDays( -7 )
year = qt_dt.year()
month = qt_dt.month()
day = qt_dt.day()
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, ( '>', 'date', ( year, month, day ) ) )
def GetPredicates( self ):
qt_dt = self._date.selectedDate()
year = qt_dt.year()
month = qt_dt.month()
day = qt_dt.day()
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, ( self._sign.GetStringSelection(), 'date', ( year, month, day ) ) ), )
return predicates
class PanelPredicateSystemLastViewedDelta( PanelPredicateSystemSingle ):
def __init__( self, parent, predicate ):
PanelPredicateSystemSingle.__init__( self, parent )
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'>'] )
self._years = QP.MakeQSpinBox( self, max=30 )
self._months = QP.MakeQSpinBox( self, max=60 )
self._days = QP.MakeQSpinBox( self, max=90 )
self._hours = QP.MakeQSpinBox( self, max=24 )
#
predicate = self._GetPredicateToInitialisePanelWith( predicate )
( sign, age_type, ( years, months, days, hours ) ) = predicate.GetValue()
self._sign.SetStringSelection( sign )
self._years.setValue( years )
self._months.setValue( months )
self._days.setValue( days )
self._hours.setValue( hours )
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:last viewed'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._sign, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._years, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'years'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._months, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'months'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._days, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'days'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._hours, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'hours'), CC.FLAGS_CENTER_PERPENDICULAR )
hbox.addStretch( 1 )
self.setLayout( hbox )
def GetDefaultPredicate( self ):
return ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, ( '<', 'delta', ( 0, 0, 7, 0 ) ) )
def GetPredicates( self ):
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, ( self._sign.GetStringSelection(), 'delta', ( self._years.value(), self._months.value(), self._days.value(), self._hours.value() ) ) ), )
return predicates
class PanelPredicateSystemModifiedDate( PanelPredicateSystemSingle ):
def __init__( self, parent, predicate ):

View File

@ -23,6 +23,7 @@ from hydrus.client.gui.widgets import ClientGUICommon
EDIT_PRED_TYPES = {
ClientSearch.PREDICATE_TYPE_SYSTEM_AGE,
ClientSearch.PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME,
ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME,
ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT,
ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH,
@ -69,6 +70,7 @@ FLESH_OUT_SYSTEM_PRED_TYPES = {
ClientSearch.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER,
ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS,
ClientSearch.PREDICATE_TYPE_SYSTEM_NOTES,
ClientSearch.PREDICATE_TYPE_SYSTEM_TIME,
ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS
}
@ -213,6 +215,7 @@ class EditPredicatesPanel( ClientGUIScrolledPanels.EditPanel ):
# also it would be nice to have proper rating editing here, think about it
AGE_DELTA_PRED = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ( '>', 'delta', ( 2000, 1, 1, 1 ) ) )
LAST_VIEWED_DELTA_PRED = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, ( '>', 'delta', ( 2000, 1, 1, 1 ) ) )
MODIFIED_DELTA_PRED = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME, ( '>', 'delta', ( 2000, 1, 1, 1 ) ) )
KNOWN_URL_EXACT = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_KNOWN_URLS, ( True, 'exact_match', '', '' ) )
KNOWN_URL_DOMAIN = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_KNOWN_URLS, ( True, 'domain', '', '' ) )
@ -238,6 +241,17 @@ class EditPredicatesPanel( ClientGUIScrolledPanels.EditPanel ):
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemAgeDate( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME:
if predicate.IsUIEditable( LAST_VIEWED_DELTA_PRED ):
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemLastViewedDelta( self, predicate ) )
else:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemLastViewedDate( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME:
if predicate.IsUIEditable( MODIFIED_DELTA_PRED ):
@ -434,10 +448,13 @@ class FleshOutPredicatePanel( ClientGUIScrolledPanels.EditPanel ):
self._predicates = []
label = None
editable_pred_panels = []
static_pred_buttons = []
page_name = 'page'
pages = []
recent_predicate_types = [ predicate_type ]
static_pred_buttons = []
editable_pred_panels = []
if predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_AGE:
@ -453,6 +470,37 @@ class FleshOutPredicatePanel( ClientGUIScrolledPanels.EditPanel ):
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemModifiedDelta, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemModifiedDate, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_TIME:
recent_predicate_types = [ ClientSearch.PREDICATE_TYPE_SYSTEM_AGE ]
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ( '<', 'delta', ( 0, 0, 1, 0 ) ) ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ( '<', 'delta', ( 0, 0, 7, 0 ) ) ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ( '<', 'delta', ( 0, 1, 0, 0 ) ) ), ) ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemAgeDelta, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemAgeDate, predicate ) )
pages.append( ( 'import', recent_predicate_types, static_pred_buttons, editable_pred_panels ) )
recent_predicate_types = [ ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME ]
static_pred_buttons = []
editable_pred_panels = []
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemModifiedDelta, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemModifiedDate, predicate ) )
pages.append( ( 'modified', recent_predicate_types, static_pred_buttons, editable_pred_panels ) )
recent_predicate_types = [ ClientSearch.PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME ]
static_pred_buttons = []
editable_pred_panels = []
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemLastViewedDelta, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemLastViewedDate, predicate ) )
page_name = 'last viewed'
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_DIMENSIONS:
recent_predicate_types = [ ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO, ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_PIXELS ]
@ -585,6 +633,8 @@ class FleshOutPredicatePanel( ClientGUIScrolledPanels.EditPanel ):
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemFileViewingStatsViewtime, predicate ) )
pages.append( ( page_name, recent_predicate_types, static_pred_buttons, editable_pred_panels ) )
vbox = QP.VBoxLayout()
if label is not None:
@ -596,40 +646,69 @@ class FleshOutPredicatePanel( ClientGUIScrolledPanels.EditPanel ):
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
recent_predicates = []
page_parent = self
if len( recent_predicate_types ) > 0:
if len( pages ) > 1:
recent_predicates = HG.client_controller.new_options.GetRecentPredicates( recent_predicate_types )
self._notebook = ClientGUICommon.BetterNotebook( self )
if len( recent_predicates ) > 0:
QP.AddToLayout( vbox, self._notebook, CC.FLAGS_EXPAND_BOTH_WAYS )
page_parent = self._notebook
for ( i, ( page_name, recent_predicate_types, static_pred_buttons, editable_pred_panels ) ) in enumerate( pages ):
page_panel = QW.QWidget( page_parent )
page_vbox = QP.VBoxLayout()
recent_predicates = []
if len( recent_predicate_types ) > 0:
recent_predicates_box = ClientGUICommon.StaticBox( self, 'recent' )
recent_predicates = HG.client_controller.new_options.GetRecentPredicates( recent_predicate_types )
for recent_predicate in recent_predicates:
if len( recent_predicates ) > 0:
button = ClientGUIPredicatesSingle.StaticSystemPredicateButton( recent_predicates_box, self, ( recent_predicate, ) )
recent_predicates_box = ClientGUICommon.StaticBox( page_panel, 'recent' )
recent_predicates_box.Add( button, CC.FLAGS_EXPAND_PERPENDICULAR )
for recent_predicate in recent_predicates:
button = ClientGUIPredicatesSingle.StaticSystemPredicateButton( recent_predicates_box, self, ( recent_predicate, ) )
recent_predicates_box.Add( button, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( page_vbox, recent_predicates_box, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, recent_predicates_box, CC.FLAGS_EXPAND_PERPENDICULAR )
for button in static_pred_buttons:
QP.AddToLayout( page_vbox, button, CC.FLAGS_EXPAND_PERPENDICULAR )
for button in static_pred_buttons:
for panel in editable_pred_panels:
QP.AddToLayout( page_vbox, panel, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, button, CC.FLAGS_EXPAND_PERPENDICULAR )
page_panel.setLayout( page_vbox )
for panel in editable_pred_panels:
if i == 0 and len( static_pred_buttons ) > 0 and len( editable_pred_panels ) == 0:
ClientGUIFunctions.SetFocusLater( static_pred_buttons[0] )
QP.AddToLayout( vbox, panel, CC.FLAGS_EXPAND_PERPENDICULAR )
if len( static_pred_buttons ) > 0 and len( editable_pred_panels ) == 0:
ClientGUIFunctions.SetFocusLater( static_pred_buttons[0] )
if len( pages ) > 1:
self._notebook.addTab( page_panel, page_name )
else:
QP.AddToLayout( vbox, page_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
self.widget().setLayout( vbox )

View File

@ -20,14 +20,6 @@ from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import QtPorting as QP
CANVAS_MEDIA_VIEWER = 0
CANVAS_PREVIEW = 1
canvas_str_lookup = {}
canvas_str_lookup[ CANVAS_MEDIA_VIEWER ] = 'media viewer'
canvas_str_lookup[ CANVAS_PREVIEW ] = 'preview'
def AddGridboxStretchSpacer( layout: QW.QGridLayout ):
layout.addItem( QW.QSpacerItem( 10, 10, QW.QSizePolicy.Expanding, QW.QSizePolicy.Fixed ) )

View File

@ -2029,22 +2029,7 @@ class MediaCollection( MediaList, Media ):
def _RecalcFileViewingStats( self ):
preview_views = 0
preview_viewtime = 0.0
media_views = 0
media_viewtime = 0.0
for m in self._sorted_media:
fvsm = m.GetFileViewingStatsManager()
preview_views += fvsm.preview_views
preview_viewtime += fvsm.preview_viewtime
media_views += fvsm.media_views
media_viewtime += fvsm.media_viewtime
self._file_viewing_stats_manager = ClientMediaManagers.FileViewingStatsManager( preview_views, preview_viewtime, media_views, media_viewtime )
self._file_viewing_stats_manager = ClientMediaManagers.FileViewingStatsManager.STATICGenerateCombinedManager( [ m.GetFileViewingStatsManager() for m in self._sorted_media ] )
def _RecalcHashes( self ):
@ -3008,6 +2993,17 @@ class MediaSort( HydrusSerialisable.SerialisableBase ):
return deal_with_none( x.GetLocationsManager().GetFileModifiedTimestamp() )
elif sort_data == CC.SORT_FILES_BY_LAST_VIEWED_TIME:
def sort_key( x ):
fvsm = x.GetFileViewingStatsManager()
# do not do viewtime as a secondary sort here, to allow for user secondary sort to help out
return deal_with_none( fvsm.GetLastViewedTime( CC.CANVAS_MEDIA_VIEWER ) )
elif sort_data == CC.SORT_FILES_BY_HEIGHT:
def sort_key( x ):
@ -3078,7 +3074,7 @@ class MediaSort( HydrusSerialisable.SerialisableBase ):
# do not do viewtime as a secondary sort here, to allow for user secondary sort to help out
return fvsm.media_views
return fvsm.GetViews( CC.CANVAS_MEDIA_VIEWER )
elif sort_data == CC.SORT_FILES_BY_MEDIA_VIEWTIME:
@ -3089,7 +3085,7 @@ class MediaSort( HydrusSerialisable.SerialisableBase ):
# do not do views as a secondary sort here, to allow for user secondary sort to help out
return fvsm.media_viewtime
return fvsm.GetViewtime( CC.CANVAS_MEDIA_VIEWER )
@ -3140,6 +3136,7 @@ class MediaSort( HydrusSerialisable.SerialisableBase ):
sort_string_lookup[ CC.SORT_FILES_BY_HAS_AUDIO ] = ( 'audio first', 'silent first', CC.SORT_ASC )
sort_string_lookup[ CC.SORT_FILES_BY_IMPORT_TIME ] = ( 'oldest first', 'newest first', CC.SORT_DESC )
sort_string_lookup[ CC.SORT_FILES_BY_FILE_MODIFIED_TIMESTAMP ] = ( 'oldest first', 'newest first', CC.SORT_DESC )
sort_string_lookup[ CC.SORT_FILES_BY_LAST_VIEWED_TIME ] = ( 'oldest first', 'newest first', CC.SORT_DESC )
sort_string_lookup[ CC.SORT_FILES_BY_MIME ] = ( 'filetype', 'filetype', CC.SORT_ASC )
sort_string_lookup[ CC.SORT_FILES_BY_RANDOM ] = ( 'random', 'random', CC.SORT_ASC )
sort_string_lookup[ CC.SORT_FILES_BY_WIDTH ] = ( 'slimmest first', 'widest first', CC.SORT_ASC )

View File

@ -80,36 +80,139 @@ class FileViewingStatsManager( object ):
def __init__(
self,
preview_views: int,
preview_viewtime: int,
media_views: int,
media_viewtime: int
view_rows: typing.Collection
):
self.preview_views = preview_views
self.preview_viewtime = preview_viewtime
self.media_views = media_views
self.media_viewtime = media_viewtime
self.last_viewed_timestamps = {}
self.views = collections.Counter()
self.viewtimes = collections.Counter()
for ( canvas_type, last_viewed_timestamp, views, viewtime ) in view_rows:
if last_viewed_timestamp is not None:
self.last_viewed_timestamps[ canvas_type ] = last_viewed_timestamp
if views != 0:
self.views[ canvas_type ] = views
if viewtime != 0:
self.viewtimes[ canvas_type ] = viewtime
def Duplicate( self ):
def Duplicate( self ) -> "FileViewingStatsManager":
return FileViewingStatsManager( self.preview_views, self.preview_viewtime, self.media_views, self.media_viewtime )
view_rows = []
for canvas_type in ( CC.CANVAS_MEDIA_VIEWER, CC.CANVAS_PREVIEW ):
if canvas_type in self.last_viewed_timestamps:
last_viewed_timestamp = self.last_viewed_timestamps[ canvas_type ]
else:
last_viewed_timestamp = None
views = self.views[ canvas_type ]
viewtime = self.viewtimes[ canvas_type ]
view_rows.append( ( canvas_type, last_viewed_timestamp, views, viewtime ) )
return FileViewingStatsManager( view_rows )
def GetPrettyCombinedLine( self ):
def GetLastViewedTime( self, canvas_type: int ) -> typing.Optional[ int ]:
return 'viewed ' + HydrusData.ToHumanInt( self.media_views + self.preview_views ) + ' times, totalling ' + HydrusData.TimeDeltaToPrettyTimeDelta( self.media_viewtime + self.preview_viewtime )
if canvas_type in self.last_viewed_timestamps:
return self.last_viewed_timestamps[ canvas_type ]
else:
return None
def GetPrettyMediaLine( self ):
def GetPrettyViewsLine( self, canvas_types: int ) -> str:
return 'viewed ' + HydrusData.ToHumanInt( self.media_views ) + ' times in media viewer, totalling ' + HydrusData.TimeDeltaToPrettyTimeDelta( self.media_viewtime )
if len( canvas_types ) == 2:
info_string = ''
elif CC.CANVAS_MEDIA_VIEWER in canvas_types:
info_string = ' in media viewer'
elif CC.CANVAS_PREVIEW in canvas_types:
info_string = ' in preview window'
views_total = sum( ( self.views[ canvas_type ] for canvas_type in canvas_types ) )
viewtime_total = sum( ( self.viewtimes[ canvas_type ] for canvas_type in canvas_types ) )
if views_total == 0:
return 'no view record{}'.format( info_string )
last_viewed_times = []
for canvas_type in canvas_types:
if canvas_type in self.last_viewed_timestamps:
last_viewed_times.append( self.last_viewed_timestamps[ canvas_type ] )
if len( last_viewed_times ) == 0:
last_viewed_string = 'no recorded last view time'
else:
last_viewed_string = 'last {}'.format( HydrusData.TimestampToPrettyTimeDelta( max( last_viewed_times ) ) )
return 'viewed {} times{}, totalling {}, {}'.format( HydrusData.ToHumanInt( views_total ), info_string, HydrusData.TimeDeltaToPrettyTimeDelta( viewtime_total ), last_viewed_string )
def GetPrettyPreviewLine( self ):
def GetViews( self, canvas_type: int ) -> int:
return 'viewed ' + HydrusData.ToHumanInt( self.preview_views ) + ' times in preview window, totalling ' + HydrusData.TimeDeltaToPrettyTimeDelta( self.preview_viewtime )
return self.views[ canvas_type ]
def GetViewtime( self, canvas_type: int ) -> int:
return self.viewtimes[ canvas_type ]
def MergeCounts( self, file_viewing_stats_manager: "FileViewingStatsManager" ):
for ( canvas_type, last_viewed_timestamp ) in file_viewing_stats_manager.last_viewed_timestamps.items():
if canvas_type in self.last_viewed_timestamps:
self.last_viewed_timestamps[ canvas_type ] = max( self.last_viewed_timestamps[ canvas_type ], last_viewed_timestamp )
else:
self.last_viewed_timestamps[ canvas_type ] = last_viewed_timestamp
self.views.update( file_viewing_stats_manager.views )
self.viewtimes.update( file_viewing_stats_manager.viewtimes )
def ProcessContentUpdate( self, content_update ):
@ -118,19 +221,21 @@ class FileViewingStatsManager( object ):
if action == HC.CONTENT_UPDATE_ADD:
( hash, preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) = row
( hash, canvas_type, view_timestamp, views_delta, viewtime_delta ) = row
self.preview_views += preview_views_delta
self.preview_viewtime += preview_viewtime_delta
self.media_views += media_views_delta
self.media_viewtime += media_viewtime_delta
if view_timestamp is not None:
self.last_viewed_timestamps[ canvas_type ] = view_timestamp
self.views[ canvas_type ] += views_delta
self.viewtimes[ canvas_type ] += viewtime_delta
elif action == HC.CONTENT_UPDATE_DELETE:
self.preview_views = 0
self.preview_viewtime = 0
self.media_views = 0
self.media_viewtime = 0
self.last_viewed_timestamps = {}
self.views = collections.Counter()
self.viewtimes = collections.Counter()
@ -141,10 +246,7 @@ class FileViewingStatsManager( object ):
for sub_fvsm in sub_fvsms:
fvsm.preview_views += sub_fvsm.preview_views
fvsm.preview_viewtime += sub_fvsm.preview_viewtime
fvsm.media_views += sub_fvsm.media_views
fvsm.media_viewtime += sub_fvsm.media_viewtime
fvsm.MergeCounts( sub_fvsm )
return fvsm
@ -153,7 +255,7 @@ class FileViewingStatsManager( object ):
@staticmethod
def STATICGenerateEmptyManager():
return FileViewingStatsManager( 0, 0, 0, 0 )
return FileViewingStatsManager( [] )
class LocationsManager( object ):

View File

@ -71,7 +71,7 @@ class MediaResult( object ):
return self._file_info_manager
def GetFileViewingStatsManager( self ):
def GetFileViewingStatsManager( self ) -> ClientMediaManagers.FileViewingStatsManager:
return self._file_viewing_stats_manager

View File

@ -81,7 +81,7 @@ options = {}
# Misc
NETWORK_VERSION = 20
SOFTWARE_VERSION = 470
SOFTWARE_VERSION = 471
CLIENT_API_VERSION = 25
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -238,30 +238,6 @@ class DBBase( object ):
self._c.executemany( query, args_iterator )
def _ExecuteManySelectSingleParam( self, query, single_param_iterator ):
select_args_iterator = ( ( param, ) for param in single_param_iterator )
return self._ExecuteManySelect( query, select_args_iterator )
def _ExecuteManySelect( self, query, select_args_iterator ):
# back in python 2, we did batches of 256 hash_ids/whatever at a time in big "hash_id IN (?,?,?,?,...)" predicates.
# this was useful to get over some 100,000 x fetchall() call overhead, but it would sometimes throw the SQLite query planner off and do non-optimal queries
# (basically, the "hash_id in (256)" would weight the hash_id index request x 256 vs another when comparing the sqlite_stat1 tables, which could lead to WEWLAD for some indices with low median very-high mean skewed distribution
# python 3 is better about call overhead, so we'll go back to what is pure
# cursor.executemany SELECT when
for select_args in select_args_iterator:
for result in self._Execute( query, select_args ):
yield result
def _GenerateIndexName( self, table_name, columns ):
return '{}_{}_index'.format( table_name, '_'.join( columns ) )

View File

@ -624,29 +624,49 @@ def GenerateKey():
def Get64BitHammingDistance( perceptual_hash1, perceptual_hash2 ):
# old way of doing this was:
# old slow strategy:
#while xor > 0:
#
# distance += 1
# xor &= xor - 1
#
# convert to unsigned long long, then xor
# then through the power of stackexchange magic, we get number of bits in record time
# ---------------------
# cool stackexchange strategy:
# Here it is: https://stackoverflow.com/questions/9829578/fast-way-of-counting-non-zero-bits-in-positive-integer/9830282#9830282
n = struct.unpack( '!Q', perceptual_hash1 )[0] ^ struct.unpack( '!Q', perceptual_hash2 )[0]
# n = struct.unpack( '!Q', perceptual_hash1 )[0] ^ struct.unpack( '!Q', perceptual_hash2 )[0]
n = ( n & 0x5555555555555555 ) + ( ( n & 0xAAAAAAAAAAAAAAAA ) >> 1 ) # 10101010, 01010101
n = ( n & 0x3333333333333333 ) + ( ( n & 0xCCCCCCCCCCCCCCCC ) >> 2 ) # 11001100, 00110011
n = ( n & 0x0F0F0F0F0F0F0F0F ) + ( ( n & 0xF0F0F0F0F0F0F0F0 ) >> 4 ) # 11110000, 00001111
n = ( n & 0x00FF00FF00FF00FF ) + ( ( n & 0xFF00FF00FF00FF00 ) >> 8 ) # etc...
n = ( n & 0x0000FFFF0000FFFF ) + ( ( n & 0xFFFF0000FFFF0000 ) >> 16 )
n = ( n & 0x00000000FFFFFFFF ) + ( n >> 32 )
# n = ( n & 0x5555555555555555 ) + ( ( n & 0xAAAAAAAAAAAAAAAA ) >> 1 ) # 10101010, 01010101
# n = ( n & 0x3333333333333333 ) + ( ( n & 0xCCCCCCCCCCCCCCCC ) >> 2 ) # 11001100, 00110011
# n = ( n & 0x0F0F0F0F0F0F0F0F ) + ( ( n & 0xF0F0F0F0F0F0F0F0 ) >> 4 ) # 11110000, 00001111
# n = ( n & 0x00FF00FF00FF00FF ) + ( ( n & 0xFF00FF00FF00FF00 ) >> 8 ) # etc...
# n = ( n & 0x0000FFFF0000FFFF ) + ( ( n & 0xFFFF0000FFFF0000 ) >> 16 )
# n = ( n & 0x00000000FFFFFFFF ) + ( n >> 32 )
# you technically are going n & 0xFFFFFFFF00000000 at the end, but that's a no-op with the >> 32 afterwards, so can be omitted
return n
# ---------------------
# lame but about 9% faster than the stackexchange using timeit (0.1286 vs 0.1383 for 100000 comparisons) (when including the xor and os.urandom to generate phashes)
# n = struct.unpack( '!Q', perceptual_hash1 )[0] ^ struct.unpack( '!Q', perceptual_hash2 )[0]
# return bin( n ).count( '1' )
# collapsed because that also boosts by another 1% or so
return bin( struct.unpack( '!Q', perceptual_hash1 )[0] ^ struct.unpack( '!Q', perceptual_hash2 )[0] ).count( '1' )
# ---------------------
# once python 3.10 rolls around apparently you can just do int.bit_count(), which _may_ be six times faster
# not sure how that handles signed numbers, but shouldn't matter here
# another option is https://www.valuedlessons.com/2009/01/popcount-in-python-with-benchmarks.html, which is just an array where the byte value is an address on a list to the answer
def GetNicelyDivisibleNumberForZoom( zoom, no_bigger_than ):
@ -1904,7 +1924,7 @@ class ContentUpdate( object ):
if self._action == HC.CONTENT_UPDATE_ADD:
( hash, preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) = self._row
( hash, canvas_type, view_timestamp, views_delta, viewtime_delta ) = self._row
hashes = { hash }

View File

@ -292,13 +292,9 @@ def GetFileInfo( path, mime = None, ok_to_look_for_hydrus_updates = False ):
return ( size, mime, width, height, duration, num_frames, has_audio, num_words )
def GetFileModifiedTimestamp( path ):
def GetFileModifiedTimestamp( path ) -> int:
s = os.stat( path )
file_modified_timestamp = int( s.st_mtime )
return file_modified_timestamp
return int( os.path.getmtime( path ) )
def GetHashFromPath( path ):

View File

@ -222,19 +222,23 @@ def GenerateNumPyImage( path, mime, force_pil = False ) -> numpy.array:
raise HydrusExceptions.UnsupportedFileException()
if pil_image.mode == 'LAB':
# I and F are some sort of 32-bit monochrome or whatever, doesn't seem to work in PIL well, with or without ICC
if pil_image.mode not in ( 'I', 'F' ):
force_pil = True
if HasICCProfile( pil_image ):
if HG.media_load_report_mode:
if pil_image.mode == 'LAB':
HydrusData.ShowText( 'Image has ICC, so switching to PIL' )
force_pil = True
force_pil = True
if HasICCProfile( pil_image ):
if HG.media_load_report_mode:
HydrusData.ShowText( 'Image has ICC, so switching to PIL' )
force_pil = True
except HydrusExceptions.UnsupportedFileException:

View File

@ -115,7 +115,7 @@ class HydrusTagArchive( object ):
def _InitDB( self ):
self._c.execute( 'CREATE TABLE hash_type ( hash_type INTEGER );', )
self._c.execute( 'CREATE TABLE hash_type ( hash_type INTEGER );' )
self._c.execute( 'CREATE TABLE hashes ( hash_id INTEGER PRIMARY KEY, hash BLOB_BYTES );' )
self._c.execute( 'CREATE UNIQUE INDEX hashes_hash_index ON hashes ( hash );' )

View File

@ -77,6 +77,7 @@ class Predicate(Enum):
FILETYPE = auto()
HASH = auto()
MOD_DATE = auto()
LAST_VIEWED_TIME = auto()
TIME_IMPORTED = auto()
DURATION = auto()
FILE_SERVICE = auto()
@ -162,7 +163,8 @@ SYSTEM_PREDICATES = {
'file ?type': (Predicate.FILETYPE, Operators.ONLY_EQUAL, Value.FILETYPE_LIST, None),
'hash': (Predicate.HASH, Operators.ONLY_EQUAL, Value.HASHLIST_WITH_ALGORITHM, None),
'modified date|date modified': (Predicate.MOD_DATE, Operators.RELATIONAL, Value.DATE_OR_TIME_INTERVAL, None),
'time imported': (Predicate.TIME_IMPORTED, Operators.RELATIONAL, Value.DATE_OR_TIME_INTERVAL, None),
'last viewed time|last view time': (Predicate.LAST_VIEWED_TIME, Operators.RELATIONAL, Value.DATE_OR_TIME_INTERVAL, None),
'time imported|import time': (Predicate.TIME_IMPORTED, Operators.RELATIONAL, Value.DATE_OR_TIME_INTERVAL, None),
'duration': (Predicate.DURATION, Operators.RELATIONAL, Value.TIME_SEC_MSEC, None),
'file service': (Predicate.FILE_SERVICE, Operators.FILESERVICE_STATUS, Value.ANY_STRING, None),
'num(ber of)? file relationships': (Predicate.NUM_FILE_RELS, Operators.RELATIONAL, Value.NATURAL, Units.FILE_RELATIONSHIP_TYPE),
@ -442,7 +444,13 @@ examples = [
"system:time imported < 1 day",
"system:time imported < 0 years 1 month 1 day 1 hour",
" system:time imported ~= 2011-1-3 ",
"system:time imported ~= 1996-05-2",
"system:import time < 7 years 45 days 70h",
"system:import time > 2011-06-04",
"system:import time > 7 years 2 months",
"system:import time < 1 day",
"system:import time < 0 years 1 month 1 day 1 hour",
" system:import time ~= 2011-1-3 ",
"system:import time ~= 1996-05-2",
"system:duration < 5 seconds",
"system:duration ~= 5 sec 6000 msecs",
"system:duration > 3 milliseconds",

View File

@ -997,9 +997,10 @@ class DB( HydrusDB.HydrusDB ):
def _GetHashes( self, master_hash_ids ):
select_statement = 'SELECT hash FROM hashes WHERE master_hash_id = ?;'
return [ hash for ( hash, ) in self._ExecuteManySelectSingleParam( select_statement, master_hash_ids ) ]
with self._MakeTemporaryIntegerTable( master_hash_ids, 'master_hash_id' ) as temp_hash_ids_table_name:
return self._STL( self._Execute( 'SELECT hash FROM {} CROSS JOIN hashes USING ( master_hash_id );'.format( temp_hash_ids_table_name ) ) )
def _GetMasterHashId( self, hash ):
@ -1785,9 +1786,10 @@ class DB( HydrusDB.HydrusDB ):
else:
select_statement = 'SELECT service_hash_id FROM ' + deleted_mappings_table_name + ' WHERE service_tag_id = ' + str( service_tag_id ) + ' AND service_hash_id = ?;'
deleted_service_hash_ids = self._STI( self._ExecuteManySelectSingleParam( select_statement, service_hash_ids ) )
with self._MakeTemporaryIntegerTable( service_hash_ids, 'service_hash_id' ) as temp_hash_ids_table_name:
deleted_service_hash_ids = self._STS( self._Execute( 'SELECT service_hash_id FROM {} CROSS JOIN {} USING ( service_hash_id ) WHERE service_tag_id = ?;'.format( temp_hash_ids_table_name, deleted_mappings_table_name ), ( service_tag_id, ) ) )
service_hash_ids = set( service_hash_ids ).difference( deleted_service_hash_ids )
@ -2007,9 +2009,10 @@ class DB( HydrusDB.HydrusDB ):
( current_files_table_name, deleted_files_table_name, pending_files_table_name, petitioned_files_table_name, ip_addresses_table_name ) = GenerateRepositoryFilesTableNames( service_id )
select_statement = 'SELECT service_hash_id FROM ' + current_files_table_name + ' WHERE service_hash_id = ?;'
valid_service_hash_ids = self._STL( self._ExecuteManySelectSingleParam( select_statement, service_hash_ids ) )
with self._MakeTemporaryIntegerTable( service_hash_ids, 'service_hash_id' ) as temp_hash_ids_table_name:
valid_service_hash_ids = self._STL( self._Execute( 'SELECT service_hash_id FROM {} CROSS JOIN {} USING ( service_hash_id );'.format( temp_hash_ids_table_name, current_files_table_name ) ) )
self._RepositoryRewardFilePetitioners( service_id, valid_service_hash_ids, 1 )
@ -2018,7 +2021,7 @@ class DB( HydrusDB.HydrusDB ):
self._ExecuteMany( 'INSERT OR IGNORE INTO ' + deleted_files_table_name + ' ( service_hash_id, account_id, file_timestamp ) VALUES ( ?, ?, ? );', ( ( service_hash_id, account_id, timestamp ) for service_hash_id in valid_service_hash_ids ) )
master_hash_ids = self._RepositoryGetMasterHashIds( service_id, service_hash_ids )
master_hash_ids = self._RepositoryGetMasterHashIds( service_id, valid_service_hash_ids )
self._DeferFilesDeleteIfNowOrphan( master_hash_ids )
@ -2027,9 +2030,10 @@ class DB( HydrusDB.HydrusDB ):
( current_mappings_table_name, deleted_mappings_table_name, pending_mappings_table_name, petitioned_mappings_table_name ) = GenerateRepositoryMappingsTableNames( service_id )
select_statement = 'SELECT service_hash_id FROM ' + current_mappings_table_name + ' WHERE service_tag_id = ' + str( service_tag_id ) + ' AND service_hash_id = ?;'
valid_service_hash_ids = self._STL( self._ExecuteManySelectSingleParam( select_statement, service_hash_ids ) )
with self._MakeTemporaryIntegerTable( service_hash_ids, 'service_hash_id' ) as temp_hash_ids_table_name:
valid_service_hash_ids = self._STL( self._Execute( 'SELECT service_hash_id FROM {} CROSS JOIN {} USING ( service_hash_id ) WHERE service_tag_id = ?;'.format( temp_hash_ids_table_name, current_mappings_table_name ), ( service_tag_id, ) ) )
self._RepositoryRewardMappingPetitioners( service_id, service_tag_id, valid_service_hash_ids, 1 )
@ -3095,9 +3099,10 @@ class DB( HydrusDB.HydrusDB ):
( current_files_table_name, deleted_files_table_name, pending_files_table_name, petitioned_files_table_name, ip_addresses_table_name ) = GenerateRepositoryFilesTableNames( service_id )
select_statement = 'SELECT service_hash_id FROM ' + current_files_table_name + ' WHERE service_hash_id = ?;'
valid_service_hash_ids = [ service_hash_id for ( service_hash_id, ) in self._ExecuteManySelectSingleParam( select_statement, service_hash_ids ) ]
with self._MakeTemporaryIntegerTable( service_hash_ids, 'service_hash_id' ) as temp_hash_ids_table_name:
valid_service_hash_ids = self._STL( self._Execute( 'SELECT service_hash_id FROM {} CROSS JOIN {} USING ( service_hash_id );'.format( temp_hash_ids_table_name, current_files_table_name ) ) )
self._ExecuteMany( 'REPLACE INTO ' + petitioned_files_table_name + ' ( service_hash_id, account_id, reason_id ) VALUES ( ?, ?, ? );', ( ( service_hash_id, account_id, reason_id ) for service_hash_id in valid_service_hash_ids ) )
@ -3106,9 +3111,10 @@ class DB( HydrusDB.HydrusDB ):
( current_mappings_table_name, deleted_mappings_table_name, pending_mappings_table_name, petitioned_mappings_table_name ) = GenerateRepositoryMappingsTableNames( service_id )
select_statement = 'SELECT service_hash_id FROM ' + current_mappings_table_name + ' WHERE service_tag_id = ' + str( service_tag_id ) + ' AND service_hash_id = ?;'
valid_service_hash_ids = [ service_hash_id for ( service_hash_id, ) in self._ExecuteManySelectSingleParam( select_statement, service_hash_ids ) ]
with self._MakeTemporaryIntegerTable( service_hash_ids, 'service_hash_id' ) as temp_hash_ids_table_name:
valid_service_hash_ids = self._STL( self._Execute( 'SELECT service_hash_id FROM {} CROSS JOIN {} USING ( service_hash_id ) WHERE service_tag_id = ?;'.format( temp_hash_ids_table_name, current_mappings_table_name ), ( service_tag_id, ) ) )
self._ExecuteMany( 'REPLACE INTO ' + petitioned_mappings_table_name + ' ( service_tag_id, service_hash_id, account_id, reason_id ) VALUES ( ?, ?, ?, ? );', [ ( service_tag_id, service_hash_id, account_id, reason_id ) for service_hash_id in valid_service_hash_ids ] )
@ -3449,13 +3455,14 @@ class DB( HydrusDB.HydrusDB ):
( current_files_table_name, deleted_files_table_name, pending_files_table_name, petitioned_files_table_name, ip_addresses_table_name ) = GenerateRepositoryFilesTableNames( service_id )
select_statement = 'SELECT account_id, COUNT( * ) FROM ' + petitioned_files_table_name + ' WHERE service_hash_id = ? GROUP BY account_id;'
counter = collections.Counter()
for ( account_id, count ) in self._ExecuteManySelectSingleParam( select_statement, service_hash_ids ):
with self._MakeTemporaryIntegerTable( service_hash_ids, 'service_hash_id' ) as temp_hash_ids_table_name:
counter[ account_id ] += count
for ( account_id, count ) in self._Execute( 'SELECT account_id, COUNT( * ) FROM {} CROSS JOIN {} USING ( service_hash_id ) GROUP BY account_id;'.format( temp_hash_ids_table_name, petitioned_files_table_name ) ):
counter[ account_id ] += count
scores = [ ( account_id, count * multiplier ) for ( account_id, count ) in counter.items() ]
@ -3467,13 +3474,14 @@ class DB( HydrusDB.HydrusDB ):
( current_mappings_table_name, deleted_mappings_table_name, pending_mappings_table_name, petitioned_mappings_table_name ) = GenerateRepositoryMappingsTableNames( service_id )
select_statement = 'SELECT account_id, COUNT( * ) FROM ' + petitioned_mappings_table_name + ' WHERE service_tag_id = ' + str( service_tag_id ) + ' AND service_hash_id = ? GROUP BY account_id;'
counter = collections.Counter()
for ( account_id, count ) in self._ExecuteManySelectSingleParam( select_statement, service_hash_ids ):
with self._MakeTemporaryIntegerTable( service_hash_ids, 'service_hash_id' ) as temp_hash_ids_table_name:
counter[ account_id ] += count
for ( account_id, count ) in self._Execute( 'SELECT account_id, COUNT( * ) FROM {} CROSS JOIN {} USING ( service_hash_id ) WHERE service_tag_id = ? GROUP BY account_id;'.format( temp_hash_ids_table_name, petitioned_mappings_table_name ), ( service_tag_id, ) ):
counter[ account_id ] += count
scores = [ ( account_id, count * multiplier ) for ( account_id, count ) in counter.items() ]
@ -3587,6 +3595,9 @@ class DB( HydrusDB.HydrusDB ):
def _RepositorySuperBan( self, service_id, admin_account_id, subject_account_ids, timestamp ):
# this is pending a rewrite, nothing calls it atm, executemanysingleparam no longer exists
pass
'''
( current_files_table_name, deleted_files_table_name, pending_files_table_name, petitioned_files_table_name, ip_addresses_table_name ) = GenerateRepositoryFilesTableNames( service_id )
select_statement = 'SELECT service_hash_id FROM ' + current_files_table_name + ' WHERE account_id = ?;'
@ -3639,7 +3650,7 @@ class DB( HydrusDB.HydrusDB ):
self._RepositoryDeleteTagSibling( service_id, admin_account_id, bad_service_tag_id, good_service_tag_id, timestamp )
'''
def _RewardAccounts( self, service_id, score_type, scores ):

View File

@ -2417,7 +2417,7 @@ class TestClientAPI( unittest.TestCase ):
locations_manager = ClientMediaManagers.LocationsManager( { random_file_service_hex_current : current_import_timestamp }, { random_file_service_hex_deleted : ( deleted_deleted_timestamp, deleted_import_timestamp ) }, set(), set(), inbox = False, urls = urls, file_modified_timestamp = file_modified_timestamp )
ratings_manager = ClientMediaManagers.RatingsManager( {} )
notes_manager = ClientMediaManagers.NotesManager( {} )
file_viewing_stats_manager = ClientMediaManagers.FileViewingStatsManager( 0, 0, 0, 0 )
file_viewing_stats_manager = ClientMediaManagers.FileViewingStatsManager.STATICGenerateEmptyManager()
media_result = ClientMediaResult.MediaResult( file_info_manager, tags_manager, locations_manager, ratings_manager, notes_manager, file_viewing_stats_manager )
@ -2754,7 +2754,7 @@ class TestClientAPI( unittest.TestCase ):
locations_manager = ClientMediaManagers.LocationsManager( dict(), dict(), set(), set() )
ratings_manager = ClientMediaManagers.RatingsManager( {} )
notes_manager = ClientMediaManagers.NotesManager( {} )
file_viewing_stats_manager = ClientMediaManagers.FileViewingStatsManager( 0, 0, 0, 0 )
file_viewing_stats_manager = ClientMediaManagers.FileViewingStatsManager.STATICGenerateEmptyManager()
media_result = ClientMediaResult.MediaResult( file_info_manager, tags_manager, locations_manager, ratings_manager, notes_manager, file_viewing_stats_manager )

View File

@ -776,7 +776,7 @@ class TestClientDB( unittest.TestCase ):
predicates.append( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_EVERYTHING, count = ClientSearch.PredicateCount.STATICCreateCurrentCount( 1 ) ) )
predicates.append( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_INBOX, count = ClientSearch.PredicateCount.STATICCreateCurrentCount( 1 ) ) )
predicates.append( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_ARCHIVE, count = ClientSearch.PredicateCount.STATICCreateCurrentCount( 0 ) ) )
predicates.extend( [ ClientSearch.Predicate( predicate_type ) for predicate_type in [ ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT, ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE, ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME, ClientSearch.PREDICATE_TYPE_SYSTEM_KNOWN_URLS, ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_AUDIO, ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_ICC_PROFILE, ClientSearch.PREDICATE_TYPE_SYSTEM_HASH, ClientSearch.PREDICATE_TYPE_SYSTEM_DIMENSIONS, ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.PREDICATE_TYPE_SYSTEM_NOTES, ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.PREDICATE_TYPE_SYSTEM_MIME, ClientSearch.PREDICATE_TYPE_SYSTEM_RATING, ClientSearch.PREDICATE_TYPE_SYSTEM_SIMILAR_TO, ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_SERVICE, ClientSearch.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER, ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS, ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS ] ] )
predicates.extend( [ ClientSearch.Predicate( predicate_type ) for predicate_type in [ ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT, ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE, ClientSearch.PREDICATE_TYPE_SYSTEM_TIME, ClientSearch.PREDICATE_TYPE_SYSTEM_KNOWN_URLS, ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_AUDIO, ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_ICC_PROFILE, ClientSearch.PREDICATE_TYPE_SYSTEM_HASH, ClientSearch.PREDICATE_TYPE_SYSTEM_DIMENSIONS, ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.PREDICATE_TYPE_SYSTEM_NOTES, ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.PREDICATE_TYPE_SYSTEM_MIME, ClientSearch.PREDICATE_TYPE_SYSTEM_RATING, ClientSearch.PREDICATE_TYPE_SYSTEM_SIMILAR_TO, ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_SERVICE, ClientSearch.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER, ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS, ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS ] ] )
self.assertEqual( set( result ), set( predicates ) )

View File

@ -381,7 +381,7 @@ def GetNotesMediaResult( hash, names_to_notes ):
locations_manager = ClientMediaManagers.LocationsManager( dict(), dict(), set(), set(), inbox = True )
ratings_manager = ClientMediaManagers.RatingsManager( {} )
notes_manager = ClientMediaManagers.NotesManager( names_to_notes )
file_viewing_stats_manager = ClientMediaManagers.FileViewingStatsManager( 0, 0, 0, 0 )
file_viewing_stats_manager = ClientMediaManagers.FileViewingStatsManager.STATICGenerateEmptyManager()
media_result = ClientMediaResult.MediaResult( file_info_manager, tags_manager, locations_manager, ratings_manager, notes_manager, file_viewing_stats_manager )
@ -518,7 +518,7 @@ def GetTagsMediaResult( hash, in_inbox, service_key, deleted_tags ):
locations_manager = ClientMediaManagers.LocationsManager( dict(), dict(), set(), set(), inbox = in_inbox )
ratings_manager = ClientMediaManagers.RatingsManager( {} )
notes_manager = ClientMediaManagers.NotesManager( {} )
file_viewing_stats_manager = ClientMediaManagers.FileViewingStatsManager( 0, 0, 0, 0 )
file_viewing_stats_manager = ClientMediaManagers.FileViewingStatsManager.STATICGenerateEmptyManager()
media_result = ClientMediaResult.MediaResult( file_info_manager, tags_manager, locations_manager, ratings_manager, notes_manager, file_viewing_stats_manager )

View File

@ -1713,19 +1713,19 @@ class TestTagObjects( unittest.TestCase ):
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ( '<', 'delta', ( 1, 2, 3, 4 ) ) )
self.assertEqual( p.ToString(), 'system:time imported: since 1 year 2 months ago' )
self.assertEqual( p.ToString(), 'system:import time: since 1 year 2 months ago' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ( CC.UNICODE_ALMOST_EQUAL_TO, 'delta', ( 1, 2, 3, 4 ) ) )
self.assertEqual( p.ToString(), 'system:time imported: around 1 year 2 months ago' )
self.assertEqual( p.ToString(), 'system:import time: around 1 year 2 months ago' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
p = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ( '>', 'delta', ( 1, 2, 3, 4 ) ) )
self.assertEqual( p.ToString(), 'system:time imported: before 1 year 2 months ago' )
self.assertEqual( p.ToString(), 'system:import time: before 1 year 2 months ago' )
self.assertEqual( p.GetNamespace(), 'system' )
self.assertEqual( p.GetTextsAndNamespaces( render_for_user ), [ ( p.ToString(), p.GetNamespace() ) ] )
@ -2001,13 +2001,22 @@ class TestTagObjects( unittest.TestCase ):
( 'system:modified time: before 7 years 2 months ago', "system:date modified > 7 years 2 months" ),
( 'system:modified time: since 1 day ago', "system:date modified < 1 day" ),
( 'system:modified time: since 1 month 1 day ago', "system:date modified < 0 years 1 month 1 day 1 hour" ),
( 'system:time imported: since 7 years 1 month ago', "system:time_imported < 7 years 45 days 70h" ),
( 'system:time imported: since 2011-06-04', "system:time imported > 2011-06-04" ),
( 'system:time imported: before 7 years 2 months ago', "system:time imported > 7 years 2 months" ),
( 'system:time imported: since 1 day ago', "system:time imported < 1 day" ),
( 'system:time imported: since 1 month 1 day ago', "system:time imported < 0 years 1 month 1 day 1 hour" ),
( 'system:time imported: a month either side of 2011-01-03', " system:time imported ~= 2011-1-3 " ),
( 'system:time imported: a month either side of 1996-05-02', "system:time imported ~= 1996-05-2" ),
( 'system:last view time: since 7 years 1 month ago', "system:last viewed time < 7 years 45 days 70h" ),
( 'system:last view time: since 7 years 1 month ago', "system:last view time < 7 years 45 days 70h" ),
( 'system:import time: since 7 years 1 month ago', "system:time_imported < 7 years 45 days 70h" ),
( 'system:import time: since 2011-06-04', "system:time imported > 2011-06-04" ),
( 'system:import time: before 7 years 2 months ago', "system:time imported > 7 years 2 months" ),
( 'system:import time: since 1 day ago', "system:time imported < 1 day" ),
( 'system:import time: since 1 month 1 day ago', "system:time imported < 0 years 1 month 1 day 1 hour" ),
( 'system:import time: a month either side of 2011-01-03', " system:time imported ~= 2011-1-3 " ),
( 'system:import time: a month either side of 1996-05-02', "system:time imported ~= 1996-05-2" ),
( 'system:import time: since 7 years 1 month ago', "system:import_time < 7 years 45 days 70h" ),
( 'system:import time: since 2011-06-04', "system:import time > 2011-06-04" ),
( 'system:import time: before 7 years 2 months ago', "system:import time > 7 years 2 months" ),
( 'system:import time: since 1 day ago', "system:import time < 1 day" ),
( 'system:import time: since 1 month 1 day ago', "system:import time < 0 years 1 month 1 day 1 hour" ),
( 'system:import time: a month either side of 2011-01-03', " system:import time ~= 2011-1-3 " ),
( 'system:import time: a month either side of 1996-05-02', "system:import time ~= 1996-05-2" ),
( 'system:duration < 5.0 seconds', "system:duration < 5 seconds" ),
( 'system:duration \u2248 11.0 seconds', "system:duration ~= 5 sec 6000 msecs" ),
( 'system:duration > 3 milliseconds', "system:duration > 3 milliseconds" ),

View File

@ -120,3 +120,23 @@ QPushButton#HydrusOnOffButton[hydrus_on=false]
color: #800000;
}
/*
Custom Controls
These are drawn by hydev on a blank canvas, so they work a little different.
*/
/*
The scanbar beneath video/audio in the media viewer.
*/
QWidget#HydrusAnimationBar
{
qproperty-hab_border: #000000;
qproperty-hab_background: #f0f0f0;
qproperty-hab_nub: #606060;
}