Code cleanup and stability sprint. Also, more robust search for calibre.
This commit is contained in:
+108
-33
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user