Fixed chapter parsing for m4b books. Added volume for audiobooks bound to 9 (decrese) and 0 (increase).

This commit is contained in:
Storm Dragon
2025-10-20 20:50:31 -04:00
parent f9564265fa
commit 409cb61f05
3 changed files with 115 additions and 28 deletions
+22 -5
View File
@@ -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)
+29 -23
View File
@@ -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
+64
View File
@@ -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: