Fixed chapter parsing for m4b books. Added volume for audiobooks bound to 9 (decrese) and 0 (increase).
This commit is contained in:
+22
-5
@@ -1153,7 +1153,7 @@ class BookReader:
|
|||||||
# Help
|
# Help
|
||||||
if self.book and hasattr(self.book, 'isAudioBook') and self.book.isAudioBook:
|
if self.book and hasattr(self.book, 'isAudioBook') and self.book.isAudioBook:
|
||||||
# Audio book help
|
# 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:
|
else:
|
||||||
# Text book help
|
# 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")
|
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:
|
else:
|
||||||
self.speechEngine.speak("No book loaded")
|
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:
|
elif event.key == pygame.K_HOME and shiftPressed:
|
||||||
# Shift+Home: Clear bookmark and jump to beginning of book
|
# Shift+Home: Clear bookmark and jump to beginning of book
|
||||||
if not self.book:
|
if not self.book:
|
||||||
@@ -2109,15 +2125,16 @@ class BookReader:
|
|||||||
Args:
|
Args:
|
||||||
bookPath: Path to new book file
|
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
|
# Stop current playback
|
||||||
self.audioPlayer.stop()
|
self.audioPlayer.stop()
|
||||||
self._cancel_buffer()
|
self._cancel_buffer()
|
||||||
self.isPlaying = False
|
self.isPlaying = False
|
||||||
|
|
||||||
# Save bookmark for current book
|
|
||||||
if self.book:
|
|
||||||
self.save_bookmark()
|
|
||||||
|
|
||||||
# Update book path and config
|
# Update book path and config
|
||||||
self.bookPath = Path(bookPath)
|
self.bookPath = Path(bookPath)
|
||||||
self.config.set_last_book(bookPath)
|
self.config.set_last_book(bookPath)
|
||||||
|
|||||||
+29
-23
@@ -149,14 +149,9 @@ class AudioParser:
|
|||||||
"""Extract chapter information from audio file"""
|
"""Extract chapter information from audio file"""
|
||||||
chapters = []
|
chapters = []
|
||||||
|
|
||||||
# Try MP4 chapter format (M4B, M4A)
|
# Try MP4 chapter format (M4B, M4A) - check chapters attribute directly
|
||||||
if hasattr(audioFile, 'tags') and audioFile.tags:
|
if hasattr(audioFile, 'chapters') and audioFile.chapters:
|
||||||
# MP4 files store chapters in a special way
|
chapters = self._parse_mp4_chapters(audioFile.chapters, audioFile.info.length)
|
||||||
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 MP3 chapter format (ID3 CHAP frames)
|
# Try MP3 chapter format (ID3 CHAP frames)
|
||||||
if not chapters and hasattr(audioFile, 'tags'):
|
if not chapters and hasattr(audioFile, 'tags'):
|
||||||
@@ -168,30 +163,41 @@ class AudioParser:
|
|||||||
|
|
||||||
return chapters
|
return chapters
|
||||||
|
|
||||||
def _parse_mp4_chapters(self, mp4Chapters):
|
def _parse_mp4_chapters(self, mp4Chapters, totalDuration):
|
||||||
"""Parse MP4 chapter list"""
|
"""
|
||||||
|
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 = []
|
chapters = []
|
||||||
|
|
||||||
for i, chapterData in enumerate(mp4Chapters):
|
for i, chapterObj in enumerate(mp4Chapters):
|
||||||
if isinstance(chapterData, tuple) and len(chapterData) >= 2:
|
# Chapter objects have .start (float, seconds) and .title (string) attributes
|
||||||
startTime = chapterData[0] / 1000.0 # Convert ms to seconds
|
startTime = chapterObj.start
|
||||||
chapterTitle = chapterData[1] if chapterData[1] else f"Chapter {i + 1}"
|
chapterTitle = chapterObj.title if chapterObj.title else f"Chapter {i + 1}"
|
||||||
|
|
||||||
# Calculate duration (will be updated when we know the next chapter's start)
|
# Calculate duration (will be updated when we know the next chapter's start)
|
||||||
duration = 0.0
|
duration = 0.0
|
||||||
|
|
||||||
chapter = AudioChapter(
|
chapter = AudioChapter(
|
||||||
title=chapterTitle,
|
title=chapterTitle,
|
||||||
startTime=startTime,
|
startTime=startTime,
|
||||||
duration=duration
|
duration=duration
|
||||||
)
|
)
|
||||||
chapters.append(chapter)
|
chapters.append(chapter)
|
||||||
|
|
||||||
# Calculate durations based on next chapter's start time
|
# Calculate durations based on next chapter's start time
|
||||||
for i in range(len(chapters) - 1):
|
for i in range(len(chapters) - 1):
|
||||||
chapters[i].duration = chapters[i + 1].startTime - chapters[i].startTime
|
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
|
return chapters
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,70 @@ class MpvPlayer:
|
|||||||
"""Get current playback speed"""
|
"""Get current playback speed"""
|
||||||
return self.playbackSpeed
|
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):
|
def cleanup(self):
|
||||||
"""Cleanup resources"""
|
"""Cleanup resources"""
|
||||||
if self.isInitialized and self.player:
|
if self.isInitialized and self.player:
|
||||||
|
|||||||
Reference in New Issue
Block a user