Version 547

This commit is contained in:
Hydrus Network Developer 2023-10-11 15:46:40 -05:00
parent 484e4f1c25
commit 766becf427
No known key found for this signature in database
GPG Key ID: 76249F053212133C
17 changed files with 372 additions and 119 deletions

View File

@ -41,7 +41,7 @@ You do not need to run the 'regenerate thumbnail' jobs--the client does that aut
If you restored an older file storage backup to a newer database, these would be files that were deleted after the backup was made. If you restored an older database backup to a newer file storage, then these would be files that were imported after the backup was made. In either case, they are files in your file structure that the database does not know about, and we want to collect them together to A) delete them or B) reimport them.
Run _database->db maintenance->clear orphan files_. Choose a location for the files to go to, and then wait for it to finish. Browse through them to verify what you are looking at, and then either delete them or reimport them.
Run _database->file maintenance->clear orphan files_. Choose a location for the files to go to, and then wait for it to finish. Browse through them to verify what you are looking at, and then either delete them or reimport them.
*** Repository Update Files ***

View File

@ -7,6 +7,33 @@ title: Changelog
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
## [Version 547](https://github.com/hydrusnetwork/hydrus/releases/tag/v547)
### mpv crash fixes
* tl;dr: mpv less crashy now
* if mpv fails to load a file but not in an outright 'error' manner (this appears to mean a file using a rare format that a submodule of mpv can't handle), the client now recognises this has happened, either right after the first load, or, if the error takes longer to occur, a subsequent status interrogation, and makes several new steps to restore program stability: disconnecting the mpv window from all commands, freezing the scanbar, loading the default hydrus.png as emergency backstop, and making a popup to let the user know what just happened. previously, Qt would get rapidly unhappy as it asked things to draw on screen over the null-state player, particularly if you show/hid the scanbar several times, and it would, if not removed promptly from screen, typically lead to a program crash
* furthermore, the scanbar now never interrogates the mpv window during its paint event. a mysterious interaction of C++ level objects during error state was causing the underlying instability here, and now I cannot reproduce this even if I try
* I also hardened the mpv window's 'no-media' state. now, rather than showing 'nothing' when media is unloaded, each mpv player now actually idles on a black png lol
* this tech will kick in for more extreme file failures, too, which have a different handler but seem to give the same detectable dump-out state
* fixed a silent-but-for-debug-mode error while destroying damaged mpv windows right when the program is terminating
### misc
* thanks to a user, we now have import support for 'djvu' files. basically an open source PDF style format
* fixed pasting an image into 'system:similar files', which I missed updating in last week's code cleanup!
* a light but spammy legacy job that refreshed every search page's empty autocomplete every five minutes (to get updated system predicates/numbers) now only occurs to autocompletes on the current page. relatedly, when you switch to a search page you haven't looked at in five minutes, it triggers the same update immediately. this should save a tiny bit of idle CPU time and, more importantly, clear out the background job queue on larger-session clients
* I _think_ I fixed some instances of the media viewer notes window initialising with a gigantic width on some OSes. if you often get a super wide notes window when you first open the media viewer, with it fixing itself when you cycle to a different file and back, let me know if things are any better
* when you have a popup message that has a 'show x files' button, usually from a subscription, that routine now excludes files that have been deleted since the button was created. it updates its existing file count on a click, also, to how many files it actually will generate. if you click one of these buttons, delete some files, and then click it again, it should no longer produce ghost files in the new search page. I'm going to add some more tech to optionally handle the system:hash predicate in a page in similar ways, 'locking' it to the current page content and preserving file sort so it works nice with 'remove files' etc..
* fixed a stupid typo that was swapping the 'allow non-local connections' server setting when making the interface for IPv6 hosts. there is a secondary check of all client IPs on every request, so I am confident this was not enabling non-local connections when undesired on IPv6, but it was disabling them by deploying the loopback interface when they should have been allowed! sorry for the trouble, and well done to the person who noticed this
* while pursing an odd and rare problem where a download job can start even though it should be waiting on a login process, I cleaned some of the login code and logic, lowering the timeout for session cookie expiring from 60 to 45 minutes and smoothing out some confusing status-checking in the pre-login stage. I could never reproduce the problem, though, so if you have had this issue, please let me know more and I'll see if I can reproduce this reliably
### simple cleanup
* cleaned up some filetype parsing code that was getting a little messy, also reduced some overhead
* unified the thumbnail/file filetype parsing a little, with better fallback states when a hydrus thumbnail happens for some reason not to be a jpeg or png
* fixed an out of date menu reference in the 'help my media files are broke.txt' document. 'clear orphan files' is under 'file maintenance' now, not 'db maintenance'
## [Version 546](https://github.com/hydrusnetwork/hydrus/releases/tag/v546)
### misc
@ -379,35 +406,3 @@ title: Changelog
* to deal with the deferred delete system clashing with SQLite not allowing index renames, I moved the database index testing and creation system to a dynamic name format. it works but is a little hacky, so maybe we'll move to direct sqlite_master interrogation in future
* unfortunately, the table shrink method I had planned to employ was not feasible (I wanted to do 'delete n rows', but it turns out that isn't compiled by default in all normal SQLite releases wew). I then experimented with several other strategies and settled on the KISS of 'select n, delete these n' in two queries, which worked out far better than my cleverer attempts anyway. the thing doesn't use much CPU time, and it cautiously autothrottles itself, and I've tested it in a bunch of situations, and I'm super happy with the performance, but if you do happen to get noticeable bumps of lag, most likely in PTR removal when the current_mappings giga-table is shrunk, turn off all database maintenance under the menu, for both idle and normal time, and let me know, and we'll figure it out
* refactored APNG parsing code to the new 'HydrusAnimationHandling.py' and took out the ffmpeg code. now OpenCV/PIL figures out the resolution
## [Version 537](https://github.com/hydrusnetwork/hydrus/releases/tag/v537)
### new filetype selector
* I rewrote the expanding checkbox list that selects filetypes in 'system:filetype' and File Import Options into a more normal tree view with checkboxes. it is more compact and scrolls neatly, letting us stack it with all these new filetypes we've been adding and more in future. the 'clicking a category selects all children' logic is preserved
* I re-ordered the actual filetypes in each sublist here. I tried to put the most common filetypes at the top and listed the rest in alphabetical order below, going for the best of both worlds. you don't want to scroll down to find webm, but you don't want to hunt through a giant hydev-written 'popularity' list to find realmedia either. let's see how it works out
* I split all the archive types away from 'applications' into a new 'archives' group
* and I did the same for the 'image project files' like krita and xcf. svg and psd may be significantly more renderable soon, so this category may get further shake-up
* this leaves 'applications' as just flash and pdf for now
* it isn't a big deal, but these new groups are reflected in _options->media_ too
* all file import options and filetype system predicates that previously said 'all applications' _should_ now say 'all applications, image project files, or archives'
### fast database delete
* I have long planned a fix for 'the PTR takes ages to delete' problem. today marks the first step in this
* deleting a huge service like the PTR and deleting/resetting/regeneratting a variety of other large data stores are now essentially instant. the old tables are not deleted instantly, but renamed and moved to a deferred delete zone
* the maintenance task that actually does the deferred background delete is not yet ready, so for now these jobs sit in the landing zone taking up their original hard disk space. I expect to have it done for next week, so bear with me if you need to delete a lot this week
* as this system gets fleshed out, the new UI under _database>db maintenance->review deferred delete table data_ will finish up too
### misc
* fixed a bitrot issue in the v534 update code related to the file maintenance manager not existing at the time of db update. if you got the 'some exif scanning failed to schedule!' popup on update, don't worry about it. everything actually worked ok, it was just a final unimportant reporting step that failed (issue #1414)
* fixed the grid layout on 'migrate tags', which at some point in the recent past went completely bananas
* tightened up some of the code that calculates and schedules deferred physical file delete. it now catches a couple of cases it wasn't and skips some work it should've
* reduced some overhead in the hover window show/hide logic. in very-heavy-session clients, this was causing significant (7ms multiple times a second) lag
* when you ok the 'manage login scripts' dialog, it no longer re-links new entries for all those scripts into the 'manage logins' system. this now only happens once on database initialisation
* the manage login scripts test routine no longer spams test errors to popup dialogs. they are still written to log if you need more data
* silenced a bit of PIL warning logspam when a file with unusual or broken EXIF data is loaded
* silenced the long time logspam that oftens happens when generating flash thumbnails
* fixed a stupid typo error in the routine that schedules downloading files from file repositories
* `nose`, `six`, and `zope` are no longer in any of the requirements.txts. I think these were needed a million years ago as PyInstaller hacks, but the situation is much better these days

View File

@ -34,6 +34,30 @@
<div class="content">
<h1 id="changelog"><a href="#changelog">changelog</a></h1>
<ul>
<li>
<h2 id="version_547"><a href="#version_547">version 547</a></h2>
<ul>
<li><h3>mpv crash fixes</h3></li>
<li>tl;dr: mpv less crashy now</li>
<li>if mpv fails to load a file but not in an outright 'error' manner (this appears to mean a file using a rare format that a submodule of mpv can't handle), the client now recognises this has happened, either right after the first load, or, if the error takes longer to occur, a subsequent status interrogation, and makes several new steps to restore program stability: disconnecting the mpv window from all commands, freezing the scanbar, loading the default hydrus.png as emergency backstop, and making a popup to let the user know what just happened. previously, Qt would get rapidly unhappy as it asked things to draw on screen over the null-state player, particularly if you show/hid the scanbar several times, and it would, if not removed promptly from screen, typically lead to a program crash</li>
<li>furthermore, the scanbar now never interrogates the mpv window during its paint event. a mysterious interaction of C++ level objects during error state was causing the underlying instability here, and now I cannot reproduce this even if I try</li>
<li>I also hardened the mpv window's 'no-media' state. now, rather than showing 'nothing' when media is unloaded, each mpv player now actually idles on a black png lol</li>
<li>this tech will kick in for more extreme file failures, too, which have a different handler but seem to give the same detectable dump-out state</li>
<li>fixed a silent-but-for-debug-mode error while destroying damaged mpv windows right when the program is terminating</li>
<li><h3>misc</h3></li>
<li>thanks to a user, we now have import support for 'djvu' files. basically an open source PDF style format</li>
<li>fixed pasting an image into 'system:similar files', which I missed updating in last week's code cleanup!</li>
<li>a light but spammy legacy job that refreshed every search page's empty autocomplete every five minutes (to get updated system predicates/numbers) now only occurs to autocompletes on the current page. relatedly, when you switch to a search page you haven't looked at in five minutes, it triggers the same update immediately. this should save a tiny bit of idle CPU time and, more importantly, clear out the background job queue on larger-session clients</li>
<li>I _think_ I fixed some instances of the media viewer notes window initialising with a gigantic width on some OSes. if you often get a super wide notes window when you first open the media viewer, with it fixing itself when you cycle to a different file and back, let me know if things are any better</li>
<li>when you have a popup message that has a 'show x files' button, usually from a subscription, that routine now excludes files that have been deleted since the button was created. it updates its existing file count on a click, also, to how many files it actually will generate. if you click one of these buttons, delete some files, and then click it again, it should no longer produce ghost files in the new search page. I'm going to add some more tech to optionally handle the system:hash predicate in a page in similar ways, 'locking' it to the current page content and preserving file sort so it works nice with 'remove files' etc..</li>
<li>fixed a stupid typo that was swapping the 'allow non-local connections' server setting when making the interface for IPv6 hosts. there is a secondary check of all client IPs on every request, so I am confident this was not enabling non-local connections when undesired on IPv6, but it was disabling them by deploying the loopback interface when they should have been allowed! sorry for the trouble, and well done to the person who noticed this</li>
<li>while pursing an odd and rare problem where a download job can start even though it should be waiting on a login process, I cleaned some of the login code and logic, lowering the timeout for session cookie expiring from 60 to 45 minutes and smoothing out some confusing status-checking in the pre-login stage. I could never reproduce the problem, though, so if you have had this issue, please let me know more and I'll see if I can reproduce this reliably</li>
<li><h3>simple cleanup</h3></li>
<li>cleaned up some filetype parsing code that was getting a little messy, also reduced some overhead</li>
<li>unified the thumbnail/file filetype parsing a little, with better fallback states when a hydrus thumbnail happens for some reason not to be a jpeg or png</li>
<li>fixed an out of date menu reference in the 'help my media files are broke.txt' document. 'clear orphan files' is under 'file maintenance' now, not 'db maintenance'</li>
</ul>
</li>
<li>
<h2 id="version_546"><a href="#version_546">version 546</a></h2>
<ul>

View File

@ -1863,11 +1863,11 @@ class Controller( HydrusController.HydrusController ):
if allow_non_local_connections:
interface = '::1'
interface = '::'
else:
interface = '::'
interface = '::1'
if use_https:

View File

@ -337,7 +337,14 @@ class JobKey( object ):
def SetFiles( self, hashes, label ):
self.SetVariable( 'attached_files', ( list( hashes ), label ) )
if len( hashes ) == 0:
self.DeleteFiles()
else:
self.SetVariable( 'attached_files', ( list( hashes ), label ) )
def SetNetworkJob( self, network_job ):

View File

@ -14,6 +14,7 @@ from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
from hydrus.client import ClientLocation
from hydrus.client import ClientThreading
from hydrus.client.gui import ClientGUIAsync
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUITopLevelWindows
@ -306,7 +307,41 @@ class PopupMessage( PopupWindow ):
location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY )
HG.client_controller.pub( 'new_page_query', location_context, initial_hashes = hashes, page_name = attached_files_label )
self._show_files_button.setEnabled( False )
def work_callable():
presented_hashes = HG.client_controller.Read( 'filter_hashes', location_context, hashes )
return presented_hashes
def publish_callable( presented_hashes ):
if len( presented_hashes ) != len( hashes ):
self._job_key.SetFiles( presented_hashes, attached_files_label )
if len( presented_hashes ) > 0:
HG.client_controller.pub( 'new_page_query', location_context, initial_hashes = presented_hashes, page_name = attached_files_label )
self._show_files_button.setEnabled( True )
def errback_callable( etype, value, tb ):
HydrusData.ShowText( 'Sorry, unable to show those files:' )
HydrusData.ShowExceptionTuple( etype, value, tb, do_wait = False )
self._show_files_button.setEnabled( True )
job = ClientGUIAsync.AsyncQtJob( self, work_callable, publish_callable, errback_callable = errback_callable )
job.start()

View File

@ -353,6 +353,9 @@ class RatingNumericalCanvas( ClientGUIRatings.RatingNumerical ):
# Note that I go setFocusPolicy( QC.Qt.TabFocus ) on all the icon buttons in the hover windows
# this means that a user can click a button and not give it focus, allowing the arrow keys and space to still propagate up to the main canvas
TOP_HOVER_PROPORTION = 0.6
SIDE_HOVER_PROPORTIONS = ( 1 - TOP_HOVER_PROPORTION ) / 2
class CanvasHoverFrame( QW.QFrame ):
hoverResizedOrMoved = QC.Signal()
@ -716,14 +719,14 @@ class CanvasHoverFrameTop( CanvasHoverFrame ):
my_width = my_size.width()
my_height = my_size.height()
my_ideal_width = max( int( parent_width * 0.6 ), self.sizeHint().width() )
my_ideal_width = max( int( parent_width * TOP_HOVER_PROPORTION ), self.sizeHint().width() )
my_ideal_height = self.sizeHint().height()
should_resize = my_ideal_width != my_width or my_ideal_height != my_height
ideal_size = QC.QSize( my_ideal_width, my_ideal_height )
ideal_position = QC.QPoint( int( parent_width * 0.2 ), 0 )
ideal_position = QC.QPoint( int( parent_width * SIDE_HOVER_PROPORTIONS ), 0 )
return ( should_resize, ideal_size, ideal_position )
@ -1631,7 +1634,7 @@ class CanvasHoverFrameRightNotes( CanvasHoverFrame ):
my_width = my_size.width()
my_height = my_size.height()
my_ideal_width = self.sizeHint().width()
my_ideal_width = min( self.sizeHint().width(), parent_width * SIDE_HOVER_PROPORTIONS )
ideal_position = QC.QPoint( parent_width - my_width, 0 )
@ -1780,6 +1783,8 @@ class CanvasHoverFrameRightNotes( CanvasHoverFrame ):
self._ResetNotes()
self._position_initialised = False

View File

@ -991,6 +991,7 @@ class AnimationBar( QW.QWidget ):
self._duration_ms = 1000
self._num_frames = 1
self._last_drawn_info = None
self._next_draw_info = None
self._show_text = True
@ -1002,8 +1003,6 @@ class AnimationBar( QW.QWidget ):
self.setProperty( 'playing', False )
new_options = HG.client_controller.new_options
background_colour = self._colours[ 'hab_background' ]
painter.setBackground( background_colour )
@ -1011,11 +1010,6 @@ class AnimationBar( QW.QWidget ):
painter.eraseRect( painter.viewport() )
def _GetAnimationBarStatus( self ):
return self._media_window.GetAnimationBarStatus()
def _GetXFromFrameIndex( self, index, width_offset = 0 ):
if self._num_frames is None or self._num_frames < 2:
@ -1054,9 +1048,11 @@ class AnimationBar( QW.QWidget ):
def _Redraw( self, painter ):
self._last_drawn_info = self._GetAnimationBarStatus()
# making an extra note here: do not under any circumstances query the mpv window during our paint event
# it leads to the QBackingStore::endPaint() errors when mpv is unhappy/unloaded
# always fetch that info and handle various error states in the TIMERAnimationUpdate and just draw cached info here
( current_frame_index, current_timestamp_ms, paused, buffer_indices ) = self._last_drawn_info
( current_frame_index, current_timestamp_ms, paused, buffer_indices ) = self._next_draw_info
self.setProperty( 'playing', not paused )
@ -1076,8 +1072,6 @@ class AnimationBar( QW.QWidget ):
#
my_height = self.height()
if buffer_indices is not None:
( start_index, rendered_to_index, end_index ) = buffer_indices
@ -1181,6 +1175,8 @@ class AnimationBar( QW.QWidget ):
painter.drawRect( 0, 0, my_width - 1, my_height - 1 )
self._last_drawn_info = self._next_draw_info
def _ScanToCurrentMousePos( self ):
@ -1292,7 +1288,7 @@ class AnimationBar( QW.QWidget ):
painter = QG.QPainter( self )
if self._CurrentMediaWindowIsBad():
if self._CurrentMediaWindowIsBad() or self._next_draw_info is None:
self._DrawBlank( painter )
@ -1318,13 +1314,13 @@ class AnimationBar( QW.QWidget ):
self._num_frames = max( num_frames, 1 )
self._last_drawn_info = None
self._currently_in_a_drag = False
self._it_was_playing_before_drag = False
HG.client_controller.gui.RegisterAnimationUpdateWindow( self )
self._next_draw_info = None
self.update()
@ -1347,7 +1343,12 @@ class AnimationBar( QW.QWidget ):
return
if self._last_drawn_info != self._GetAnimationBarStatus():
# we must never call this method in the paintEvent
current_animation_bar_status = self._media_window.GetAnimationBarStatus()
if self._last_drawn_info != current_animation_bar_status:
self._next_draw_info = current_animation_bar_status
self.update()

View File

@ -1,5 +1,6 @@
import locale
import os
import time
import traceback
import typing
@ -106,6 +107,16 @@ def log_handler( loglevel, component, message ):
HydrusData.DebugPrint( '[MPV {}] {}: {}'.format( loglevel, component, message ) )
MPVShutdownEventType = QP.registerEventType()
class MPVShutdownEvent( QC.QEvent ):
def __init__( self ):
QC.QEvent.__init__( self, MPVShutdownEventType )
MPVFileLoadedEventType = QP.registerEventType()
class MPVFileLoadedEvent( QC.QEvent ):
@ -180,6 +191,18 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
self._stop_for_slideshow = False
# ok, if you talk to this object during an eventPaint while it is in various states of 'null', you'll get this instability problem:
# QBackingStore::endPaint() called with active painter; did you forget to destroy it or call QPainter::end() on it?
# simply calling a do-nothing GetAnimationBarStatus stub that returns immediately will cause this, so it must be some C++ wrapper magic triggering some during-paint reset/event-cycle/whatever
#
# #####
# THUS, DO NOT EVER TALK TO THIS GUY DURING A paintEvent. fetch your data and call update() if it changed. Also, we now make sure _something_ is loaded as much as possible, even if it is a black square png
# #####
#
self._black_png_path = os.path.join( HC.STATIC_DIR, 'blacksquare.png' )
self._hydrus_png_path = os.path.join( HC.STATIC_DIR, 'hydrus.png' )
self._currently_in_media_load_error_state = False
global LOCALE_IS_SET
if not LOCALE_IS_SET:
@ -231,7 +254,7 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
self._media = None
self._file_is_loaded = False
self._file_header_is_loaded = False
self._disallow_seek_on_this_file = False
self._have_shown_human_error_on_this_file = False
@ -263,6 +286,8 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
MPVHellBasket.instance().emergencyDumpOut.connect( self.EmergencyDumpOut )
self._player.loadfile( self._black_png_path )
def _GetAudioOptionNames( self ):
@ -284,6 +309,21 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
return ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_GLOBAL ]
def _HandleLoadError( self ):
# file failed to load, and we are going to start getting what seem to be C++ level paintEvent exceptions after the GUI object is touched by code and then asked for repaints
self._file_header_is_loaded = False
self._currently_in_media_load_error_state = True
self._player.loadfile( self._hydrus_png_path )
if self._media is not None:
HydrusData.ShowText( f'The file with hash "{self._media.GetHash().hex()}" seems to have failed to load in mpv. In order to preserve program stability, I have unloaded it immediately!' )
def _InitialiseMPVCallbacks( self ):
player = self._player
@ -301,6 +341,18 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
QW.QApplication.instance().postEvent( self, MPVFileLoadedEvent() )
@player.event_callback( mpv.MpvEventID.SHUTDOWN )
def file_started_event( event ):
app = QW.QApplication.instance()
if app is not None and QP.isValid( self ):
app.postEvent( self, MPVShutdownEvent() )
'''
@player.event_callback( mpv.MpvEventID.LOG_MESSAGE )
def log_event( event ):
@ -309,6 +361,37 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
'''
def _LooksLikeALoadError( self ):
# as an additional note for the error we are handling here, this isn't catching something like 'error: truncated gubbins', but instead the 'verbose' debug level message of 'ffmpeg can't handle this apng's format, update ffmpeg'
# what happens in this state is the media is unloaded and the self._player.path goes from a valid path to None
# the extra fun is that self._player.path starts as None even after self._player.loadfile and may not be the valid path get as of the LoadedEvent. that event is sent when headers are read, not data
# so we need to detect when the data is actually loaded, after the .path was (briefly) something valid, and then switches back to None
# thankfully, it seems on the dump-out unload, the playlist is unset, and this appears to be the most reliable indicator of a problem and an mpv with nothing currently loaded!
if self._player.path is None:
playlist = self._player.playlist
if len( playlist ) == 0:
return True
for item in playlist:
if 'current' in item:
return False
return True
return False
def ClearMedia( self ):
self.SetMedia( None )
@ -320,13 +403,24 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
if event.type() == MPVFileLoadedEventType:
self._file_is_loaded = True
if self._player.path is None:
if self._LooksLikeALoadError():
self._HandleLoadError()
if not self._currently_in_media_load_error_state:
self._file_header_is_loaded = True
return True
elif event.type() == MPVFileSeekedEventType:
if not self._file_is_loaded:
if not self._file_header_is_loaded:
return True
@ -350,6 +444,10 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
return True
elif event.type() == MPVShutdownEventType:
self.setVisible( False )
except Exception as e:
@ -415,13 +513,14 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
def GetAnimationBarStatus( self ):
buffer_indices = None
if self._media is None or not self._file_is_loaded:
if self._file_header_is_loaded and self._LooksLikeALoadError():
current_frame_index = 0
current_timestamp_ms = 0
paused = True
self._HandleLoadError()
if self._media is None or not self._file_header_is_loaded or self._currently_in_media_load_error_state:
return None
else:
@ -455,12 +554,19 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
paused = self._player.pause
buffer_indices = None
return ( current_frame_index, current_timestamp_ms, paused, buffer_indices )
def GotoPreviousOrNextFrame( self, direction ):
if not self._file_is_loaded:
if self._currently_in_media_load_error_state:
return
if not self._file_header_is_loaded:
return
@ -486,21 +592,46 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
def IsPaused( self ):
if self._currently_in_media_load_error_state:
return True
return self._player.pause
def paintEvent(self, event):
return
def Pause( self ):
if self._currently_in_media_load_error_state:
return
self._player.pause = True
def PausePlay( self ):
if self._currently_in_media_load_error_state:
return
self._player.pause = not self._player.pause
def Play( self ):
if self._currently_in_media_load_error_state:
return
self._player.pause = False
@ -567,7 +698,12 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
def Seek( self, time_index_ms ):
if not self._file_is_loaded:
if self._currently_in_media_load_error_state:
return
if not self._file_header_is_loaded:
return
@ -594,7 +730,12 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
def SeekDelta( self, direction, duration_ms ):
if not self._file_is_loaded:
if self._currently_in_media_load_error_state:
return
if not self._file_header_is_loaded:
return
@ -653,37 +794,22 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
return
self._file_is_loaded = False
self._currently_in_media_load_error_state = False
self._file_header_is_loaded = False
self._disallow_seek_on_this_file = False
self._times_to_play_animation = 0
self._current_seek_to_start_count = 0
self._media = media
self._times_to_play_animation = 0
if self._media is not None and self._media.GetMime() in HC.ANIMATIONS and not HG.client_controller.new_options.GetBoolean( 'always_loop_gifs' ):
hash = self._media.GetHash()
mime = self._media.GetMime()
path = HG.client_controller.client_files_manager.GetFilePath( hash, mime )
if mime == HC.ANIMATION_GIF:
self._times_to_play_animation = HydrusAnimationHandling.GetTimesToPlayPILAnimation( path )
elif mime == HC.ANIMATION_APNG:
self._times_to_play_animation = HydrusAnimationHandling.GetTimesToPlayAPNG( path )
self._current_seek_to_start_count = 0
if self._media is None:
self._player.pause = True
self._player.loadfile( self._black_png_path )
# old method. this does 'work', but null seems to be subtly dangerous in these cursed lands
'''
if len( self._player.playlist ) > 0:
try:
@ -700,6 +826,7 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
pass
'''
else:
@ -723,6 +850,18 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
self._player.pause = True
if mime in HC.ANIMATIONS and not HG.client_controller.new_options.GetBoolean( 'always_loop_gifs' ):
if mime == HC.ANIMATION_GIF:
self._times_to_play_animation = HydrusAnimationHandling.GetTimesToPlayPILAnimation( path )
elif mime == HC.ANIMATION_APNG:
self._times_to_play_animation = HydrusAnimationHandling.GetTimesToPlayAPNG( path )
try:
self._player.loadfile( path )
@ -745,11 +884,21 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
def UpdateAudioMute( self ):
if self._currently_in_media_load_error_state:
return
self._player.mute = ClientGUIMediaVolume.GetCorrectCurrentMute( self._canvas_type )
def UpdateAudioVolume( self ):
if self._currently_in_media_load_error_state:
return
self._player.volume = ClientGUIMediaVolume.GetCorrectCurrentVolume( self._canvas_type )

View File

@ -1099,6 +1099,9 @@ class ManagementPanelDuplicateFilter( ManagementPanel ):
self._RefreshDuplicateCounts()
self._tag_autocomplete_1.REPEATINGPageUpdate()
self._tag_autocomplete_2.REPEATINGPageUpdate()
def Search1Changed( self, file_search_context: ClientSearch.FileSearchContext ):
@ -5786,5 +5789,10 @@ class ManagementPanelQuery( ManagementPanel ):
self._UpdateCancelButton()
if self._search_enabled:
self._tag_autocomplete.REPEATINGPageUpdate()
management_panel_types_to_classes[ ClientGUIManagementController.MANAGEMENT_TYPE_QUERY ] = ManagementPanelQuery

View File

@ -3589,7 +3589,7 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
for ( i, page ) in enumerate( self._GetPages() ):
if isinstance( page, QW.QTabWidget ) and page.HasPage( showee ):
if isinstance( page, PagesNotebook ) and page.HasPage( showee ):
self.setCurrentIndex( i )

View File

@ -13,6 +13,7 @@ from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusLists
from hydrus.core import HydrusTags
from hydrus.core import HydrusText
from hydrus.core import HydrusTime
from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
@ -809,6 +810,7 @@ class AutoCompleteDropdown( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
self._float_mode = use_float_mode
self._temporary_focus_widget = None
self._time_results_last_set = 0
self._text_input_panel = QW.QWidget( self )
@ -1002,6 +1004,19 @@ class AutoCompleteDropdown( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
def _DueAutoRefresh( self ):
if self._schedule_results_refresh_job is not None:
if not self._schedule_results_refresh_job.IsWorkComplete():
return False
return HydrusTime.TimeHasPassed( self._time_results_last_set + 300 )
def _HandleEscape( self ):
if self._text_ctrl.text() != '':
@ -1072,7 +1087,7 @@ class AutoCompleteDropdown( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
def _SetResultsToList( self, results, parsed_autocomplete_text ):
raise NotImplementedError()
self._time_results_last_set = HydrusTime.GetNow()
def _ShouldShow( self ):
@ -1418,6 +1433,15 @@ class AutoCompleteDropdown( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
def REPEATINGPageUpdate( self ):
# we could do _GetParsedAutocompleteText to be neat here, but the IsEmpty test is just this, so let's optimise for this frequently-consulted method
if self._DueAutoRefresh() and self._text_ctrl.text() == '':
self._ScheduleResultsRefresh( 0.0 )
def ParentWasScrolled( self ):
self._DropdownHideShow()
@ -1618,6 +1642,8 @@ class AutoCompleteDropdownTags( AutoCompleteDropdown ):
def _SetResultsToList( self, results, parsed_autocomplete_text: ClientSearchAutocomplete.ParsedAutocompleteText, preserve_single_selection = False ):
AutoCompleteDropdown._SetResultsToList( self, results, parsed_autocomplete_text )
self._search_results_list.SetPredicates( results, preserve_single_selection = preserve_single_selection )
self._current_list_parsed_autocomplete_text = parsed_autocomplete_text
@ -2500,13 +2526,6 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
AutoCompleteDropdownTags.SetFetchedResults( self, job_key, parsed_autocomplete_text, results_cache, results )
if parsed_autocomplete_text.IsEmpty():
# refresh system preds after five mins
self._ScheduleResultsRefresh( 300 )
def SetFileSearchContext( self, file_search_context: ClientSearch.FileSearchContext ):

View File

@ -114,7 +114,7 @@ class NetworkLoginManager( HydrusSerialisable.SerialisableBase ):
( login_script_key_and_name, credentials, login_access_type, login_access_text, active, validity, validity_error_text, no_work_until, no_work_until_reason ) = self._domains_to_login_info[ login_domain ]
if active or login_access_type == LOGIN_ACCESS_TYPE_EVERYTHING:
if active:
login_expected = True
@ -1126,11 +1126,9 @@ class LoginScriptDomain( HydrusSerialisable.SerialisableBaseNamed ):
cookies = session.cookies
cookies.clear_expired_cookies()
search_domain = network_context.context_data
for ( cookie_name_string_match, value_string_match ) in list(self._required_cookies_info.items()):
for ( cookie_name_string_match, value_string_match ) in self._required_cookies_info.items():
try:

View File

@ -116,7 +116,7 @@ class NetworkSessionManager( HydrusSerialisable.SerialisableBase ):
SERIALISABLE_NAME = 'Session Manager'
SERIALISABLE_VERSION = 1
SESSION_TIMEOUT = 60 * 60
SESSION_TIMEOUT = 45 * 60
def __init__( self ):

View File

@ -103,7 +103,7 @@ options = {}
# Misc
NETWORK_VERSION = 20
SOFTWARE_VERSION = 546
SOFTWARE_VERSION = 547
CLIENT_API_VERSION = 53
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -504,6 +504,7 @@ def GetHashFromPath( path ):
return h.digest()
# TODO: replace this with a FileTypeChecker class or something that tucks all this messy data away more neatly
headers_and_mime = [
( ( ( [0], [b'\xff\xd8'] ), ), HC.IMAGE_JPEG ),
( ( ( [0], [b'\x89PNG'] ), ), HC.UNDETERMINED_PNG ),
@ -538,13 +539,31 @@ headers_and_mime = [
( ( ( [4], [b'ftypmp4', b'ftypisom', b'ftypM4V', b'ftypMSNV', b'ftypavc1', b'ftypavc1', b'ftypFACE', b'ftypdash'] ), ), HC.UNDETERMINED_MP4 ),
( ( ( [4], [b'ftypqt'] ), ), HC.VIDEO_MOV ),
( ( ( [0], [b'fLaC'] ), ), HC.AUDIO_FLAC ),
( ( ( [0], [b'RIFF'] ), ( 8, b'WAVE' ) ), HC.AUDIO_WAVE ),
( ( ( [0], [b'RIFF'] ), ( [8], [ b'WAVE' ] ) ), HC.AUDIO_WAVE ),
( ( ( [0], [b'wvpk'] ), ), HC.AUDIO_WAVPACK ),
( ( ( [8], [b'AVI '] ), ), HC.VIDEO_AVI ),
( ( ( [0], [b'\x30\x26\xB2\x75\x8E\x66\xCF\x11\xA6\xD9\x00\xAA\x00\x62\xCE\x6C'] ), ), HC.UNDETERMINED_WM ),
( ( ( [0], [b'\x4D\x5A\x90\x00\x03'], ), ), HC.APPLICATION_WINDOWS_EXE )
]
def passes_offsets_and_headers( offsets_and_headers, first_bytes_of_file ) -> bool:
for ( offsets, headers ) in offsets_and_headers:
for offset in offsets:
for header in headers:
if first_bytes_of_file[ offset : offset + len( header ) ] == header:
return True
return False
def GetMime( path, ok_to_look_for_hydrus_updates = False ):
@ -588,9 +607,7 @@ def GetMime( path, ok_to_look_for_hydrus_updates = False ):
for ( offsets_and_headers, mime ) in headers_and_mime:
it_passes = False not in ( True in ( True in (first_bytes_of_file[ offset: ].startswith( header ) for offset in offsets) for header in headers) for ( offsets, headers ) in offsets_and_headers )
if it_passes:
if passes_offsets_and_headers( offsets_and_headers, first_bytes_of_file ):
if mime == HC.APPLICATION_ZIP:
@ -694,13 +711,9 @@ def GetMime( path, ok_to_look_for_hydrus_updates = False ):
return HC.APPLICATION_UNKNOWN
headers_and_mime_thumbnails = [
( ( ( 0, b'\xff\xd8' ), ), HC.IMAGE_JPEG ),
( ( ( 0, b'\x89PNG' ), ), HC.UNDETERMINED_PNG )
]
headers_and_mime_thumbnails = [ ( offsets_and_headers, mime ) for ( offsets_and_headers, mime ) in headers_and_mime if mime in ( HC.IMAGE_JPEG, HC.IMAGE_PNG ) ]
def GetThumbnailMime( path ):
@ -711,12 +724,11 @@ def GetThumbnailMime( path ):
for ( offsets_and_headers, mime ) in headers_and_mime_thumbnails:
it_passes = False not in ( bit_to_check[ offset: ].startswith( header ) for ( offset, header ) in offsets_and_headers )
if it_passes:
if passes_offsets_and_headers( offsets_and_headers, bit_to_check ):
return mime
return HC.APPLICATION_OCTET_STREAM
return GetMime( path )

BIN
static/blacksquare.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B