Add support for embedding cover images in M4B files and update documentation

This commit is contained in:
Deniz Şafak
2025-11-19 01:03:10 +03:00
parent c72c8e777c
commit 10d3f470e7
5 changed files with 116 additions and 23 deletions
+21
View File
@@ -89,6 +89,9 @@ class HandlerDialog(QDialog):
def __init__(self, book_path, file_type=None, checked_chapters=None, parent=None):
super().__init__(parent)
# Normalize path
book_path = os.path.normpath(os.path.abspath(book_path))
# Determine file type if not explicitly provided
if file_type:
self.file_type = file_type
@@ -2211,6 +2214,7 @@ class HandlerDialog(QDialog):
def _format_metadata_tags(self):
"""Format metadata tags for insertion at the beginning of the text"""
import datetime
from abogen.utils import get_user_cache_path
metadata = self.book_metadata
filename = os.path.splitext(os.path.basename(self.book_path))[0]
@@ -2231,6 +2235,20 @@ class HandlerDialog(QDialog):
f"{total_chapters} {'Chapters' if self.file_type == 'epub' else 'Pages'}"
)
# Handle cover image
cover_tag = ""
if metadata.get("cover_image"):
try:
import uuid
cache_dir = get_user_cache_path()
cover_path = os.path.join(cache_dir, f"cover_{uuid.uuid4()}.jpg")
cover_path = os.path.normpath(cover_path)
with open(cover_path, "wb") as f:
f.write(metadata["cover_image"])
cover_tag = f"<<METADATA_COVER_PATH:{cover_path}>>"
except Exception as e:
logging.warning(f"Failed to save cover image: {e}")
# Format metadata tags
metadata_tags = [
f"<<METADATA_TITLE:{title}>>",
@@ -2242,6 +2260,9 @@ class HandlerDialog(QDialog):
f"<<METADATA_GENRE:Audiobook>>",
]
if cover_tag:
metadata_tags.append(cover_tag)
return "\n".join(metadata_tags)
def _get_markdown_selected_text(self):
+87 -19
View File
@@ -1000,7 +1000,7 @@ class ConversionThread(QThread):
static_ffmpeg.add_paths()
merged_out_file = None
ffmpeg_proc = None
metadata_options = (
metadata_options, cover_path = (
self._extract_and_add_metadata_tags_to_ffmpeg_cmd()
)
# Prepare ffmpeg command for m4b output
@@ -1017,13 +1017,32 @@ class ConversionThread(QThread):
"1",
"-i",
"pipe:0",
"-c:a",
"aac",
"-q:a",
"2",
"-movflags",
"+faststart+use_metadata_tags",
]
if cover_path and os.path.exists(cover_path):
cmd.extend(
[
"-i",
cover_path,
"-map",
"0:a",
"-map",
"1",
"-c:v",
"copy",
"-disposition:v",
"attached_pic",
]
)
cmd.extend(
[
"-c:a",
"aac",
"-q:a",
"2",
"-movflags",
"+faststart+use_metadata_tags",
]
)
cmd += metadata_options
cmd.append(merged_out_path)
ffmpeg_proc = create_process(cmd, stdin=subprocess.PIPE, text=False)
@@ -1529,7 +1548,7 @@ class ConversionThread(QThread):
orig_path = merged_out_path
root, ext = os.path.splitext(orig_path)
tmp_path = root + ".tmp" + ext
metadata_options = (
metadata_options, cover_path = (
self._extract_and_add_metadata_tags_to_ffmpeg_cmd()
)
cmd = [
@@ -1539,15 +1558,35 @@ class ConversionThread(QThread):
orig_path,
"-i",
chapters_info_path,
"-map",
"0:a",
"-map_metadata",
"1",
"-map_chapters",
"1",
"-c:a",
"copy",
]
if cover_path and os.path.exists(cover_path):
cmd.extend(
[
"-i",
cover_path,
"-map",
"0:a",
"-map",
"2",
"-c:v",
"copy",
"-disposition:v",
"attached_pic",
]
)
else:
cmd.extend(["-map", "0:a"])
cmd.extend(
[
"-map_metadata",
"1",
"-map_chapters",
"1",
"-c:a",
"copy",
]
)
cmd += metadata_options
cmd.append(tmp_path)
proc = create_process(cmd)
@@ -1659,8 +1698,35 @@ class ConversionThread(QThread):
static_ffmpeg.add_paths()
cmd = ["ffmpeg", "-y", "-thread_queue_size", "32768", "-f", "f32le", "-ar", str(rate), "-ac", "1", "-i", "pipe:0"]
if self.output_format == "m4b":
cmd.extend(["-c:a", "aac", "-q:a", "2", "-movflags", "+faststart+use_metadata_tags"])
cmd.extend(self._extract_and_add_metadata_tags_to_ffmpeg_cmd())
metadata_options, cover_path = (
self._extract_and_add_metadata_tags_to_ffmpeg_cmd()
)
if cover_path and os.path.exists(cover_path):
cmd.extend(
[
"-i",
cover_path,
"-map",
"0:a",
"-map",
"1",
"-c:v",
"copy",
"-disposition:v",
"attached_pic",
]
)
cmd.extend(
[
"-c:a",
"aac",
"-q:a",
"2",
"-movflags",
"+faststart+use_metadata_tags",
]
)
cmd.extend(metadata_options)
elif self.output_format == "opus":
cmd.extend(["-c:a", "libopus", "-b:a", "24000"])
else:
@@ -1901,6 +1967,8 @@ class ConversionThread(QThread):
album_artist_match = re.search(r"<<METADATA_ALBUM_ARTIST:([^>]*)>>", text)
composer_match = re.search(r"<<METADATA_COMPOSER:([^>]*)>>", text)
genre_match = re.search(r"<<METADATA_GENRE:([^>]*)>>", text)
cover_match = re.search(r"<<METADATA_COVER_PATH:([^>]*)>>", text)
cover_path = cover_match.group(1) if cover_match else None
# Use display path or filename as fallback for title
@@ -1964,7 +2032,7 @@ class ConversionThread(QThread):
metadata_options.extend(["-metadata", f"genre=Audiobook"])
# Add these to ffmpeg command
return metadata_options
return metadata_options, cover_path
def _srt_time(self, t):
"""Helper function to format time for SRT files"""
+4 -4
View File
@@ -3635,9 +3635,9 @@ Categories=AudioVideo;Audio;Utility;
# Get the abogen cache directory
cache_dir = get_user_cache_path()
# Find all .txt files in the abogen cache directory
pattern = os.path.join(cache_dir, "*.txt")
cache_files = glob.glob(pattern)
# Find all .txt files and cover images in the abogen cache directory
cache_files = glob.glob(os.path.join(cache_dir, "*.txt"))
cache_files.extend(glob.glob(os.path.join(cache_dir, "cover_*.jpg")))
# Count the files
file_count = len(cache_files)
@@ -3669,7 +3669,7 @@ Categories=AudioVideo;Audio;Utility;
msg_box.setText(msg_text + "\nDo you want to delete them?")
# Add checkbox for preview cache
preview_cache_checkbox = QCheckBox("Clean preview cache", msg_box)
preview_cache_checkbox = QCheckBox("Also clean preview cache", msg_box)
preview_cache_checkbox.setChecked(False)
# Only enable checkbox if preview files exist
preview_cache_checkbox.setEnabled(preview_count > 0)