Code cleanup and stability sprint. Also, more robust search for calibre.

This commit is contained in:
Storm Dragon
2026-02-27 13:50:58 -05:00
parent 2cfb01549b
commit b5f1ec4bed
6 changed files with 324 additions and 99 deletions
+108 -33
View File
@@ -201,6 +201,9 @@ class BookReader:
# Playback state
self.isRunning = False
self.isPlaying = False
self.hasPendingSpeechCompletion = False
self.pendingSpeechCompletionToken = None
self.expectedSpeechCompletionToken = 0
# Sleep timer fade-out state
self.isFadingOut = False
@@ -902,23 +905,8 @@ class BookReader:
self._handle_pygame_key(event)
elif event.type == SPEECH_FINISHED_EVENT:
# Callback-driven paragraph finished, advance to next
# Don't auto-advance if in any menu
inAnyMenu = (self.optionsMenu.is_in_menu() or
self.bookSelector.is_in_browser() or
self.sleepTimerMenu.is_in_menu() or
self.helpMenu.is_in_menu() or
self.recentBooksMenu.is_in_menu() or
(self.absMenu and self.absMenu.is_in_menu()))
if self.isPlaying and not inAnyMenu and self.book:
if not self.next_paragraph():
self.displayText = "End of book reached"
self.isPlaying = False
self.save_bookmark(speakFeedback=False)
else:
# Start next paragraph
self._start_paragraph_playback()
completionToken = getattr(event, 'completionToken', None)
self._process_speech_completion(completionToken)
# Explicitly delete event objects to help GC
del events
@@ -956,12 +944,9 @@ class BookReader:
isAudioBook = self.book and hasattr(self.book, 'isAudioBook') and self.book.isAudioBook
readerEngine = self.config.get_reader_engine()
# Don't auto-advance if in any menu
inAnyMenu = (self.optionsMenu.is_in_menu() or
self.bookSelector.is_in_browser() or
self.sleepTimerMenu.is_in_menu() or
self.helpMenu.is_in_menu() or
self.recentBooksMenu.is_in_menu() or
(self.absMenu and self.absMenu.is_in_menu()))
inAnyMenu = self._is_in_any_menu()
self._flush_pending_speech_completion()
if self.isPlaying and not inAnyMenu and self.book:
if isAudioBook:
@@ -1015,9 +1000,6 @@ class BookReader:
# Every ~10 seconds (300 frames at 30 FPS) run GC
gcCounter += 1
if gcCounter >= 300:
# Clear any accumulated pygame events before GC
pygame.event.clear()
# Alternate between fast (gen 0) and full GC
# At 300: gen 0 only (fast)
# At 600: full collection (all generations)
@@ -1284,18 +1266,23 @@ class BookReader:
self.speechEngine.speak("No book loaded")
return
isAudioBook = hasattr(self.book, 'isAudioBook') and self.book.isAudioBook
readerEngine = self.config.get_reader_engine()
# Pause playback while saving
wasPaused = False
if readerEngine in ['speechd', 'screenreader']:
if isAudioBook:
wasPaused = self.audioPlayer.is_paused()
if not wasPaused and self.audioPlayer.is_audio_file_playing():
self.audioPlayer.pause_audio_file()
elif readerEngine in ['speechd', 'screenreader']:
wasPaused = self.readingEngine.is_reading_paused()
if not wasPaused and self.readingEngine.is_reading_active():
self.readingEngine.pause_reading()
else:
wasPaused = self.audioPlayer.is_paused()
if not wasPaused and self.audioPlayer.is_playing():
self.audioPlayer.pause()
if not wasPaused and self.audioPlayer.is_audio_file_playing():
self.audioPlayer.pause_audio_file()
# Re-enable auto-saving if it was disabled
self.bookmarkCleared = False
@@ -1305,10 +1292,12 @@ class BookReader:
# Resume playback
if not wasPaused and self.isPlaying:
if readerEngine in ['speechd', 'screenreader']:
if isAudioBook:
self.audioPlayer.resume_audio_file()
elif readerEngine in ['speechd', 'screenreader']:
self.readingEngine.resume_reading()
else:
self.audioPlayer.resume()
self.audioPlayer.resume_audio_file()
elif event.key == pygame.K_PAGEUP:
# Increase speech rate
@@ -1672,6 +1661,73 @@ class BookReader:
else:
self.speechEngine.speak("End of chapter")
def _is_in_any_menu(self):
"""Return True when any modal menu/browser is active."""
return (
self.optionsMenu.is_in_menu() or
self.bookmarksMenu.is_in_menu() or
self.bookSelector.is_in_browser() or
self.sleepTimerMenu.is_in_menu() or
self.helpMenu.is_in_menu() or
self.recentBooksMenu.is_in_menu() or
(self.absMenu and self.absMenu.is_in_menu())
)
def _queue_pending_speech_completion(self, completionToken=None):
"""Store a completion event to process after menus close."""
self.hasPendingSpeechCompletion = True
self.pendingSpeechCompletionToken = completionToken
def _process_speech_completion(self, completionToken=None):
"""Advance paragraph/chapter for callback-driven completion events."""
if not (self.isPlaying and self.book):
return
if hasattr(self.book, 'isAudioBook') and self.book.isAudioBook:
return
readerEngine = self.config.get_reader_engine()
if readerEngine not in ['speechd', 'screenreader']:
return
if completionToken is None:
return
try:
tokenInt = int(completionToken)
except Exception:
return
if tokenInt != self.expectedSpeechCompletionToken:
return
if self._is_in_any_menu():
self._queue_pending_speech_completion(tokenInt)
return
if self.readingEngine and self.readingEngine.is_reading_paused():
return
if self.readingEngine:
tokenCheck = getattr(self.readingEngine, 'is_completion_token_current', None)
if callable(tokenCheck) and not tokenCheck(tokenInt):
return
if not self.next_paragraph():
self.displayText = "End of book reached"
self.isPlaying = False
self.save_bookmark(speakFeedback=False)
else:
self._start_paragraph_playback()
def _flush_pending_speech_completion(self):
"""Process a deferred completion event once menus are closed."""
if not self.hasPendingSpeechCompletion:
return
if self._is_in_any_menu():
return
completionToken = self.pendingSpeechCompletionToken
self.hasPendingSpeechCompletion = False
self.pendingSpeechCompletionToken = None
self._process_speech_completion(completionToken)
def _is_help_key(self, event):
"""Return True when the key event should open or close help."""
if event.key == pygame.K_F1:
@@ -2166,6 +2222,11 @@ class BookReader:
if self.book:
self.save_bookmark(speakFeedback=False)
self._close_abs_session()
if self.readingEngine and hasattr(self.readingEngine, 'cancel_reading'):
self.readingEngine.cancel_reading()
self.expectedSpeechCompletionToken += 1
self.hasPendingSpeechCompletion = False
self.pendingSpeechCompletionToken = None
self.audioPlayer.stop()
if self.audioPlayer.is_audio_file_loaded():
self.audioPlayer.stop_audio_file()
@@ -2549,6 +2610,11 @@ class BookReader:
self._close_abs_session()
# Stop current playback
if self.readingEngine and hasattr(self.readingEngine, 'cancel_reading'):
self.readingEngine.cancel_reading()
self.expectedSpeechCompletionToken += 1
self.hasPendingSpeechCompletion = False
self.pendingSpeechCompletionToken = None
self.audioPlayer.stop()
self._cancel_buffer()
self.isPlaying = False
@@ -2636,14 +2702,23 @@ class BookReader:
if readerEngine in ['speechd', 'screenreader']:
# Use callback-driven reading engines
def on_speech_finished(finishType):
self.expectedSpeechCompletionToken += 1
paragraphToken = self.expectedSpeechCompletionToken
def on_speech_finished(finishType, completionToken=None):
"""
Callback when a paragraph finishes speaking.
Post pygame event instead of mutating state in callback thread.
"""
if finishType == 'COMPLETED' and self.isPlaying:
# Post pygame event to handle in main loop
pygame.event.post(pygame.event.Event(SPEECH_FINISHED_EVENT))
eventData = {'completionToken': paragraphToken}
if completionToken is not None:
try:
eventData['completionToken'] = int(completionToken)
except Exception:
pass
pygame.event.post(pygame.event.Event(SPEECH_FINISHED_EVENT, eventData))
self.readingEngine.speak_reading(paragraph, callback=on_speech_finished)
else: