Fixed a bug that would cause no playback on newloy loaded books. Added .mobi support.
This commit is contained in:
@@ -35,6 +35,7 @@ from src.daisy_parser import DaisyParser
|
|||||||
from src.epub_parser import EpubParser
|
from src.epub_parser import EpubParser
|
||||||
from src.pdf_parser import PdfParser
|
from src.pdf_parser import PdfParser
|
||||||
from src.txt_parser import TxtParser
|
from src.txt_parser import TxtParser
|
||||||
|
from src.mobi_parser import MobiParser
|
||||||
from src.audio_parser import AudioParser
|
from src.audio_parser import AudioParser
|
||||||
from src.folder_audiobook_parser import FolderAudiobookParser
|
from src.folder_audiobook_parser import FolderAudiobookParser
|
||||||
from src.bookmark_manager import BookmarkManager
|
from src.bookmark_manager import BookmarkManager
|
||||||
@@ -122,7 +123,7 @@ class BookReader:
|
|||||||
booksDir = libraryDir
|
booksDir = libraryDir
|
||||||
else:
|
else:
|
||||||
booksDir = self.config.get_books_directory()
|
booksDir = self.config.get_books_directory()
|
||||||
supportedFormats = ['.zip', '.epub', '.pdf', '.txt', '.m4b', '.m4a', '.mp3']
|
supportedFormats = ['.zip', '.epub', '.mobi', '.pdf', '.txt', '.m4b', '.m4a', '.mp3']
|
||||||
self.bookSelector = BookSelector(booksDir, supportedFormats, self.speechEngine)
|
self.bookSelector = BookSelector(booksDir, supportedFormats, self.speechEngine)
|
||||||
|
|
||||||
# Initialize sleep timer menu
|
# Initialize sleep timer menu
|
||||||
@@ -220,6 +221,9 @@ class BookReader:
|
|||||||
if suffix in ['.epub']:
|
if suffix in ['.epub']:
|
||||||
self.parser = EpubParser()
|
self.parser = EpubParser()
|
||||||
self.book = self.parser.parse(self.bookPath)
|
self.book = self.parser.parse(self.bookPath)
|
||||||
|
elif suffix in ['.mobi']:
|
||||||
|
self.parser = MobiParser()
|
||||||
|
self.book = self.parser.parse(self.bookPath)
|
||||||
elif suffix in ['.zip']:
|
elif suffix in ['.zip']:
|
||||||
# Assume DAISY format for zip files
|
# Assume DAISY format for zip files
|
||||||
self.parser = DaisyParser()
|
self.parser = DaisyParser()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ lxml>=4.6.0
|
|||||||
mutagen>=1.45.0
|
mutagen>=1.45.0
|
||||||
pypdf
|
pypdf
|
||||||
mpv
|
mpv
|
||||||
|
requests>=2.25.0
|
||||||
|
|
||||||
# Braille display support (optional)
|
# Braille display support (optional)
|
||||||
# Note: These are system packages, not pip packages
|
# Note: These are system packages, not pip packages
|
||||||
@@ -14,5 +15,6 @@ mpv
|
|||||||
# Optional dependencies
|
# Optional dependencies
|
||||||
# piper-tts: Install separately with voice models to /usr/share/piper-voices/
|
# piper-tts: Install separately with voice models to /usr/share/piper-voices/
|
||||||
# ffmpeg: Install via system package manager for M4B/M4A support
|
# ffmpeg: Install via system package manager for M4B/M4A support
|
||||||
|
# calibre: Install via system package manager for MOBI support (provides ebook-convert)
|
||||||
# brltty: System daemon for Braille display hardware
|
# brltty: System daemon for Braille display hardware
|
||||||
# liblouis: Braille translation library
|
# liblouis: Braille translation library
|
||||||
|
|||||||
101
src/mobi_parser.py
Normal file
101
src/mobi_parser.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
MOBI Parser
|
||||||
|
|
||||||
|
Parses MOBI/Mobipocket format ebooks and extracts text content.
|
||||||
|
Uses Calibre's ebook-convert to convert MOBI to EPUB, then parses the EPUB.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from src.book import Book, Chapter
|
||||||
|
from src.epub_parser import EpubParser
|
||||||
|
|
||||||
|
|
||||||
|
class MobiParser:
|
||||||
|
"""Parser for MOBI/Mobipocket format ebooks"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize MOBI parser"""
|
||||||
|
self.tempDir = None
|
||||||
|
self.epubParser = None
|
||||||
|
|
||||||
|
def parse(self, mobiPath):
|
||||||
|
"""
|
||||||
|
Parse MOBI file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mobiPath: Path to MOBI file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Book object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: If parsing fails
|
||||||
|
"""
|
||||||
|
mobiPath = Path(mobiPath)
|
||||||
|
|
||||||
|
# Create temp directory for conversion
|
||||||
|
self.tempDir = tempfile.mkdtemp(prefix='bookstorm_mobi_')
|
||||||
|
tempPath = Path(self.tempDir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Convert MOBI to EPUB using Calibre's ebook-convert
|
||||||
|
epubPath = tempPath / f"{mobiPath.stem}.epub"
|
||||||
|
|
||||||
|
# Check if ebook-convert is available
|
||||||
|
convertCmd = ['ebook-convert', str(mobiPath), str(epubPath)]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Run ebook-convert with error capture
|
||||||
|
result = subprocess.run(
|
||||||
|
convertCmd,
|
||||||
|
capture_output=True, # pylint: disable=subprocess-run-check
|
||||||
|
text=True,
|
||||||
|
timeout=60
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise Exception(f"ebook-convert failed: {result.stderr}")
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise Exception(
|
||||||
|
"Calibre's ebook-convert tool is required for MOBI support. "
|
||||||
|
"Install Calibre: sudo pacman -S calibre (Arch) or "
|
||||||
|
"sudo apt install calibre (Debian/Ubuntu)"
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
raise Exception("MOBI conversion timed out (>60 seconds)")
|
||||||
|
|
||||||
|
# Verify the EPUB was created
|
||||||
|
if not epubPath.exists():
|
||||||
|
raise Exception("Failed to convert MOBI to EPUB")
|
||||||
|
|
||||||
|
# Parse the converted EPUB
|
||||||
|
self.epubParser = EpubParser()
|
||||||
|
book = self.epubParser.parse(epubPath)
|
||||||
|
|
||||||
|
# Set the original MOBI path as the book path
|
||||||
|
book.path = mobiPath
|
||||||
|
|
||||||
|
return book
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"Error parsing MOBI: {e}")
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""Clean up temporary files"""
|
||||||
|
# Clean up EPUB parser
|
||||||
|
if self.epubParser:
|
||||||
|
self.epubParser.cleanup()
|
||||||
|
|
||||||
|
# Clean up our temp directory
|
||||||
|
if self.tempDir and Path(self.tempDir).exists():
|
||||||
|
try:
|
||||||
|
shutil.rmtree(self.tempDir)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not remove temp directory: {e}")
|
||||||
@@ -382,14 +382,18 @@ class MpvPlayer:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Seek to start position (including 0)
|
# Start playback first (this ensures mpv begins loading/decoding)
|
||||||
# Always seek to ensure we're at the right position, even for position 0
|
|
||||||
if startPosition >= 0:
|
|
||||||
self.player.seek(startPosition, reference='absolute')
|
|
||||||
|
|
||||||
# Start playback
|
|
||||||
self.player.pause = False
|
self.player.pause = False
|
||||||
self.isPaused = False
|
self.isPaused = False
|
||||||
|
|
||||||
|
# Only seek if we have a non-zero start position
|
||||||
|
# Seeking to position 0 immediately after load can fail if file isn't ready
|
||||||
|
if startPosition > 0:
|
||||||
|
# Wait a moment for file to be ready before seeking
|
||||||
|
import time
|
||||||
|
time.sleep(0.1)
|
||||||
|
self.player.seek(startPosition, reference='absolute')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user