From ecbddde9cd5a2bb7da87bb43b97412da1d5cd833 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Mon, 27 Oct 2025 18:32:12 -0400 Subject: [PATCH] Fixed a bug that would cause no playback on newloy loaded books. Added .mobi support. --- bookstorm.py | 6 ++- requirements.txt | 2 + src/mobi_parser.py | 101 +++++++++++++++++++++++++++++++++++++++++++++ src/mpv_player.py | 16 ++++--- 4 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 src/mobi_parser.py diff --git a/bookstorm.py b/bookstorm.py index bd74942..6b89219 100755 --- a/bookstorm.py +++ b/bookstorm.py @@ -35,6 +35,7 @@ from src.daisy_parser import DaisyParser from src.epub_parser import EpubParser from src.pdf_parser import PdfParser from src.txt_parser import TxtParser +from src.mobi_parser import MobiParser from src.audio_parser import AudioParser from src.folder_audiobook_parser import FolderAudiobookParser from src.bookmark_manager import BookmarkManager @@ -122,7 +123,7 @@ class BookReader: booksDir = libraryDir else: 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) # Initialize sleep timer menu @@ -220,6 +221,9 @@ class BookReader: if suffix in ['.epub']: self.parser = EpubParser() 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']: # Assume DAISY format for zip files self.parser = DaisyParser() diff --git a/requirements.txt b/requirements.txt index ed75b7d..4a8673d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ lxml>=4.6.0 mutagen>=1.45.0 pypdf mpv +requests>=2.25.0 # Braille display support (optional) # Note: These are system packages, not pip packages @@ -14,5 +15,6 @@ mpv # Optional dependencies # piper-tts: Install separately with voice models to /usr/share/piper-voices/ # 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 # liblouis: Braille translation library diff --git a/src/mobi_parser.py b/src/mobi_parser.py new file mode 100644 index 0000000..5a2fa3f --- /dev/null +++ b/src/mobi_parser.py @@ -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}") diff --git a/src/mpv_player.py b/src/mpv_player.py index 3db2d9f..aec177e 100644 --- a/src/mpv_player.py +++ b/src/mpv_player.py @@ -382,14 +382,18 @@ class MpvPlayer: return False try: - # Seek to start position (including 0) - # 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 + # Start playback first (this ensures mpv begins loading/decoding) self.player.pause = 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 except Exception as e: