mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Add support for embedding cover images in M4B files and update documentation
This commit is contained in:
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user