diff --git a/bookstorm.py b/bookstorm.py index 5212362..e6accf3 100755 --- a/bookstorm.py +++ b/bookstorm.py @@ -1153,7 +1153,7 @@ class BookReader: # Help if self.book and hasattr(self.book, 'isAudioBook') and self.book.isAudioBook: # Audio book help - self.speechEngine.speak("SPACE: play pause. n: next chapter. p: previous chapter. LEFT RIGHT arrows: seek 5 seconds. UP DOWN arrows: seek 1 minute. SHIFT LEFT RIGHT: seek 30 seconds. SHIFT UP DOWN: seek 5 minutes. s: save bookmark. k: bookmarks menu. r: recent books. b: browse books. a: audiobookshelf. o: options menu. i: current info. Right bracket: increase playback speed. Left bracket: decrease playback speed. Backspace: reset playback speed. SHIFT HOME: clear bookmark and jump to beginning. CONTROL: stop speech. t: time remaining. h: help. q: quit or sleep timer") + self.speechEngine.speak("SPACE: play pause. n: next chapter. p: previous chapter. LEFT RIGHT arrows: seek 5 seconds. UP DOWN arrows: seek 1 minute. SHIFT LEFT RIGHT: seek 30 seconds. SHIFT UP DOWN: seek 5 minutes. s: save bookmark. k: bookmarks menu. r: recent books. b: browse books. a: audiobookshelf. o: options menu. i: current info. Right bracket: increase playback speed. Left bracket: decrease playback speed. Backspace: reset playback speed. 0: increase volume. 9: decrease volume. SHIFT HOME: clear bookmark and jump to beginning. CONTROL: stop speech. t: time remaining. h: help. q: quit or sleep timer") else: # Text book help self.speechEngine.speak("SPACE: play pause. n: next paragraph. p: previous paragraph. Shift N: next chapter. Shift P: previous chapter. LEFT RIGHT arrows: previous next paragraph. SHIFT LEFT RIGHT: previous next chapter. UP DOWN arrows: skip 5 paragraphs. SHIFT UP DOWN: first last paragraph. s: save bookmark. k: bookmarks menu. r: recent books. b: browse books. a: audiobookshelf. o: options menu. i: current info. Page Up Down: adjust speech rate. Right bracket: increase playback speed. Left bracket: decrease playback speed. Backspace: reset playback speed. SHIFT HOME: clear bookmark and jump to beginning. CONTROL: stop speech. t: time remaining. h: help. q: quit or sleep timer") @@ -1219,6 +1219,22 @@ class BookReader: else: self.speechEngine.speak("No book loaded") + elif event.key == pygame.K_0: + # Increase volume (audio books only - mpv handles volume control) + if self.book and hasattr(self.book, 'isAudioBook') and self.book.isAudioBook: + newVolume = self.audioPlayer.increase_volume(5) + self.speechEngine.speak(f"Volume {newVolume}") + else: + self.speechEngine.speak("Volume control only works for audio books") + + elif event.key == pygame.K_9: + # Decrease volume (audio books only - mpv handles volume control) + if self.book and hasattr(self.book, 'isAudioBook') and self.book.isAudioBook: + newVolume = self.audioPlayer.decrease_volume(5) + self.speechEngine.speak(f"Volume {newVolume}") + else: + self.speechEngine.speak("Volume control only works for audio books") + elif event.key == pygame.K_HOME and shiftPressed: # Shift+Home: Clear bookmark and jump to beginning of book if not self.book: @@ -2109,15 +2125,16 @@ class BookReader: Args: bookPath: Path to new book file """ + # Save bookmark for current book BEFORE stopping playback + # (so we can capture the current audio position) + if self.book: + self.save_bookmark(speakFeedback=False) + # Stop current playback self.audioPlayer.stop() self._cancel_buffer() self.isPlaying = False - # Save bookmark for current book - if self.book: - self.save_bookmark() - # Update book path and config self.bookPath = Path(bookPath) self.config.set_last_book(bookPath) diff --git a/src/audio_parser.py b/src/audio_parser.py index 8d37d14..6d54d29 100644 --- a/src/audio_parser.py +++ b/src/audio_parser.py @@ -149,14 +149,9 @@ class AudioParser: """Extract chapter information from audio file""" chapters = [] - # Try MP4 chapter format (M4B, M4A) - if hasattr(audioFile, 'tags') and audioFile.tags: - # MP4 files store chapters in a special way - if hasattr(audioFile.tags, '_DictProxy__dict'): - tagsDict = audioFile.tags._DictProxy__dict - if 'chapters' in tagsDict: - mp4Chapters = tagsDict['chapters'] - chapters = self._parse_mp4_chapters(mp4Chapters) + # Try MP4 chapter format (M4B, M4A) - check chapters attribute directly + if hasattr(audioFile, 'chapters') and audioFile.chapters: + chapters = self._parse_mp4_chapters(audioFile.chapters, audioFile.info.length) # Try MP3 chapter format (ID3 CHAP frames) if not chapters and hasattr(audioFile, 'tags'): @@ -168,30 +163,41 @@ class AudioParser: return chapters - def _parse_mp4_chapters(self, mp4Chapters): - """Parse MP4 chapter list""" + def _parse_mp4_chapters(self, mp4Chapters, totalDuration): + """ + Parse MP4 chapter list + + Args: + mp4Chapters: List of mutagen.mp4.Chapter objects + totalDuration: Total duration of audio file in seconds + + Returns: + List of AudioChapter objects + """ chapters = [] - for i, chapterData in enumerate(mp4Chapters): - if isinstance(chapterData, tuple) and len(chapterData) >= 2: - startTime = chapterData[0] / 1000.0 # Convert ms to seconds - chapterTitle = chapterData[1] if chapterData[1] else f"Chapter {i + 1}" + for i, chapterObj in enumerate(mp4Chapters): + # Chapter objects have .start (float, seconds) and .title (string) attributes + startTime = chapterObj.start + chapterTitle = chapterObj.title if chapterObj.title else f"Chapter {i + 1}" - # Calculate duration (will be updated when we know the next chapter's start) - duration = 0.0 + # Calculate duration (will be updated when we know the next chapter's start) + duration = 0.0 - chapter = AudioChapter( - title=chapterTitle, - startTime=startTime, - duration=duration - ) - chapters.append(chapter) + chapter = AudioChapter( + title=chapterTitle, + startTime=startTime, + duration=duration + ) + chapters.append(chapter) # Calculate durations based on next chapter's start time for i in range(len(chapters) - 1): chapters[i].duration = chapters[i + 1].startTime - chapters[i].startTime - # Last chapter duration will be set by total book duration later + # Last chapter duration extends to end of file + if chapters: + chapters[-1].duration = totalDuration - chapters[-1].startTime return chapters diff --git a/src/mpv_player.py b/src/mpv_player.py index d6b3259..cb77cf7 100644 --- a/src/mpv_player.py +++ b/src/mpv_player.py @@ -165,6 +165,70 @@ class MpvPlayer: """Get current playback speed""" return self.playbackSpeed + def increase_volume(self, step=5): + """ + Increase volume by step amount + + Args: + step: Volume increase amount (default 5) + + Returns: + Current volume level (0-200, allows software amplification) + """ + if not self.isInitialized or not self.player: + return 100 + + try: + # pylint: disable=no-member + currentVolume = self.player.volume if self.player.volume is not None else 100 + # Allow up to 200% for software amplification (useful for quiet audio) + newVolume = min(200, currentVolume + step) + self.player.volume = newVolume + return int(newVolume) + except Exception as e: + print(f"Error increasing volume: {e}") + return 100 + + def decrease_volume(self, step=5): + """ + Decrease volume by step amount + + Args: + step: Volume decrease amount (default 5) + + Returns: + Current volume level (0-100) + """ + if not self.isInitialized or not self.player: + return 100 + + try: + # pylint: disable=no-member + currentVolume = self.player.volume if self.player.volume is not None else 100 + newVolume = max(0, currentVolume - step) + self.player.volume = newVolume + return int(newVolume) + except Exception as e: + print(f"Error decreasing volume: {e}") + return 100 + + def get_volume(self): + """ + Get current volume level + + Returns: + Current volume (0-200, 100 is normal, >100 is software amplification) + """ + if not self.isInitialized or not self.player: + return 100 + + try: + # pylint: disable=no-member + volume = self.player.volume + return int(volume) if volume is not None else 100 + except: + return 100 + def cleanup(self): """Cleanup resources""" if self.isInitialized and self.player: