diff --git a/CHANGELOG.md b/CHANGELOG.md
index dca2837..a6c1ffd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,11 @@
# v1.0.8 (pre-release)
- Added support for AMD GPUs in Linux (Special thanks to @hg000125 for his contribution in #23)
+- Added extra metadata support for chaptered M4B files, ensuring better compatibility with audiobook players.
- Skipping PyTorch CUDA installation if GPU is not NVIDIA in WINDOWS_INSTALL.bat script, preventing unnecessary installation of PyTorch.
-- Fixed voice preview player keeps open at the background after stopping the preview.
-- Improved input box background color handling, fixed display issues in Linux Wayland.
+- Fixed voice preview player keeps playing silently at the background after preview ends.
+- Improved input box background color handling, fixed display issues in Linux.
- Better sleep state handling for Linux.
+- Improvements in documentation and code.
# v1.0.7
- Improve chaptered audio generation by outputting directly as `m4b` instead of converting from `wav`.
diff --git a/abogen/book_handler.py b/abogen/book_handler.py
index 46ddd45..5eb63d9 100644
--- a/abogen/book_handler.py
+++ b/abogen/book_handler.py
@@ -1494,8 +1494,13 @@ class HandlerDialog(QDialog):
authors_text = ", ".join(self.book_metadata["authors"])
html_content += f"
By {authors_text}
"
- if self.book_metadata["publisher"]:
- html_content += f"Published by {self.book_metadata['publisher']}
"
+ if self.book_metadata["publisher"] or self.book_metadata.get("publication_year"):
+ pub_info = []
+ if self.book_metadata["publisher"]:
+ pub_info.append(f"Published by {self.book_metadata['publisher']}")
+ if self.book_metadata.get("publication_year"):
+ pub_info.append(f"Year: {self.book_metadata['publication_year']}")
+ html_content += f"{' | '.join(pub_info)}
"
html_content += "
"
@@ -1517,24 +1522,51 @@ class HandlerDialog(QDialog):
"description": None,
"cover_image": None,
"publisher": None,
+ "publication_year": None,
}
if self.file_type == "epub":
- title_items = self.book.get_metadata("DC", "title")
- if title_items:
- metadata["title"] = title_items[0][0]
+ try:
+ title_items = self.book.get_metadata("DC", "title")
+ if title_items and len(title_items) > 0:
+ metadata["title"] = title_items[0][0]
+ except Exception as e:
+ logging.warning(f"Error extracting title metadata: {e}")
- author_items = self.book.get_metadata("DC", "creator")
- if author_items:
- metadata["authors"] = [author[0] for author in author_items]
+ try:
+ author_items = self.book.get_metadata("DC", "creator")
+ if author_items:
+ metadata["authors"] = [author[0] for author in author_items if len(author) > 0]
+ except Exception as e:
+ logging.warning(f"Error extracting author metadata: {e}")
- desc_items = self.book.get_metadata("DC", "description")
- if desc_items:
- metadata["description"] = desc_items[0][0]
+ try:
+ desc_items = self.book.get_metadata("DC", "description")
+ if desc_items and len(desc_items) > 0:
+ metadata["description"] = desc_items[0][0]
+ except Exception as e:
+ logging.warning(f"Error extracting description metadata: {e}")
- publisher_items = self.book.get_metadata("DC", "publisher")
- if publisher_items:
- metadata["publisher"] = publisher_items[0][0]
+ try:
+ publisher_items = self.book.get_metadata("DC", "publisher")
+ if publisher_items and len(publisher_items) > 0:
+ metadata["publisher"] = publisher_items[0][0]
+ except Exception as e:
+ logging.warning(f"Error extracting publisher metadata: {e}")
+
+ # Try to extract publication year
+ try:
+ date_items = self.book.get_metadata("DC", "date")
+ if date_items and len(date_items) > 0:
+ date_str = date_items[0][0]
+ # Try to extract just the year from the date string
+ year_match = re.search(r'\b(19|20)\d{2}\b', date_str)
+ if year_match:
+ metadata["publication_year"] = year_match.group(0)
+ else:
+ metadata["publication_year"] = date_str
+ except Exception as e:
+ logging.warning(f"Error extracting publication date metadata: {e}")
for item in self.book.get_items_of_type(ebooklib.ITEM_COVER):
metadata["cover_image"] = item.get_content()
@@ -1564,6 +1596,18 @@ class HandlerDialog(QDialog):
metadata["description"] = f"Keywords: {keywords}"
metadata["publisher"] = pdf_info.get("creator", None)
+
+ # Try to extract publication date from PDF metadata
+ if "creationDate" in pdf_info:
+ date_str = pdf_info["creationDate"]
+ year_match = re.search(r'D:(\d{4})', date_str)
+ if year_match:
+ metadata["publication_year"] = year_match.group(1)
+ elif "modDate" in pdf_info:
+ date_str = pdf_info["modDate"]
+ year_match = re.search(r'D:(\d{4})', date_str)
+ if year_match:
+ metadata["publication_year"] = year_match.group(1)
if len(self.pdf_doc) > 0:
try:
@@ -1580,10 +1624,43 @@ class HandlerDialog(QDialog):
else:
return self._get_pdf_selected_text()
+ def _format_metadata_tags(self):
+ """Format metadata tags for insertion at the beginning of the text"""
+ import datetime
+
+ metadata = self.book_metadata
+ filename = os.path.splitext(os.path.basename(self.book_path))[0]
+ current_year = str(datetime.datetime.now().year)
+
+ # Get values with fallbacks
+ title = metadata.get("title") or filename
+ authors = metadata.get("authors") or ["Unknown"]
+ authors_text = ", ".join(authors)
+ album_artist = authors_text or "Unknown"
+ year = metadata.get("publication_year") or current_year # Use publication year if available
+
+ # Count chapters/pages
+ total_chapters = len(self.checked_chapters)
+ chapter_text = f"{total_chapters} {'Chapters' if self.file_type == 'epub' else 'Pages'}"
+
+ # Format metadata tags
+ metadata_tags = [
+ f"<>",
+ f"<>",
+ f"<>",
+ f"<>",
+ f"<>"
+ ]
+
+ return "\n".join(metadata_tags)
+
def _get_epub_selected_text(self):
all_checked_identifiers = set()
chapter_texts = []
+ # Add metadata tags at the beginning
+ metadata_tags = self._format_metadata_tags()
+
item_order_counter = 0
ordered_checked_items = []
@@ -1608,7 +1685,7 @@ class HandlerDialog(QDialog):
marker = f"<>"
chapter_texts.append(marker + "\n" + text)
- full_text = "\n\n".join(chapter_texts)
+ full_text = metadata_tags + "\n\n" + "\n\n".join(chapter_texts)
return full_text, all_checked_identifiers
def _get_pdf_selected_text(self):
@@ -1617,6 +1694,9 @@ class HandlerDialog(QDialog):
section_titles = []
all_content = []
+ # Add metadata tags at the beginning
+ metadata_tags = self._format_metadata_tags()
+
pdf_has_no_bookmarks = (
hasattr(self, "has_pdf_bookmarks") and not self.has_pdf_bookmarks
)
@@ -1641,7 +1721,7 @@ class HandlerDialog(QDialog):
if text:
all_content.append(text)
included_text_ids.add(page_id)
- return "\n\n".join(all_content), all_checked_identifiers
+ return metadata_tags + "\n\n" + "\n\n".join(all_content), all_checked_identifiers
iterator = QTreeWidgetItemIterator(self.treeWidget)
while iterator.value():
@@ -1699,7 +1779,7 @@ class HandlerDialog(QDialog):
included_text_ids.add(identifier)
iterator += 1
- return "\n\n".join([t[1] for t in section_titles]), all_checked_identifiers
+ return metadata_tags + "\n\n" + "\n\n".join([t[1] for t in section_titles]), all_checked_identifiers
def on_save_chapters_changed(self, state):
self.save_chapters_separately = bool(state)
diff --git a/abogen/conversion.py b/abogen/conversion.py
index 1c00d5c..779e3e3 100644
--- a/abogen/conversion.py
+++ b/abogen/conversion.py
@@ -220,6 +220,10 @@ class ConversionThread(QThread):
# Clean up text using utility function
text = clean_text(text)
+
+ # Remove metadata markers from the text to be processed
+ metadata_pattern = r"<]*>>"
+ text = re.sub(metadata_pattern, "", text)
# --- Chapter splitting logic ---
chapter_pattern = r"<>"
@@ -511,7 +515,7 @@ class ConversionThread(QThread):
[
"ffmpeg",
"-y",
- "-thread_queue_size", "1024", # Increased thread_queue_size for chapter opus
+ "-thread_queue_size", "32768", # Increased thread_queue_size for chapter opus
"-f",
"f32le",
"-ar",
@@ -597,7 +601,7 @@ class ConversionThread(QThread):
opus_out_path = f"{base_filepath_no_ext}.opus"
ffmpeg_cmd_opus = [
"ffmpeg", "-y",
- "-thread_queue_size", "1024", # Increased thread_queue_size
+ "-thread_queue_size", "32768", # Increased thread_queue_size
"-f", "f32le", "-ar", "24000", "-ac", "1", "-i", "pipe:0",
"-c:a", "libopus", "-b:a", "24000", # Original bitrate
opus_out_path,
@@ -701,13 +705,20 @@ class ConversionThread(QThread):
static_ffmpeg.add_paths()
ffmpeg_cmd = [
"ffmpeg", "-y",
- "-thread_queue_size", "1024", # Increased thread_queue_size
+ "-thread_queue_size", "32768", # Increased thread_queue_size
"-f", "f32le", "-ar", "24000", "-ac", "1", "-i", "pipe:0",
"-i", chapters_info_path,
"-map", "0:a",
"-map_metadata", "1",
"-map_chapters", "1",
- "-c:a", "aac", "-b:a", "128k", # Explicitly AAC with a common bitrate
+ # Add metadata tags
+ "-metadata", "composer=Narrator",
+ "-metadata", "genre=Audiobook",
+ # Extract metadata from text markers if available
+ *self._extract_and_add_metadata_tags_to_ffmpeg_cmd(),
+ "-c:a", "aac",
+ "-q:a", "2", # Quality-based VBR for better quality control (lower = better, range 0.1-2)
+ "-movflags", "+faststart+use_metadata_tags", # Added for better compatibility
output_m4b_path,
]
@@ -742,6 +753,69 @@ class ConversionThread(QThread):
except Exception as e_clean:
self.log_updated.emit((f"Warning: Could not delete temporary chapter file {chapters_info_path}: {e_clean}", "orange"))
+ def _extract_and_add_metadata_tags_to_ffmpeg_cmd(self):
+ """Extract metadata tags from text content and add them to ffmpeg command"""
+ metadata_options = []
+
+ # Get the input text (either direct or from file)
+ text = ""
+ if self.is_direct_text:
+ text = self.file_name
+ else:
+ try:
+ encoding = detect_encoding(self.file_name)
+ with open(self.file_name, "r", encoding=encoding, errors="replace") as file:
+ text = file.read()
+ except Exception as e:
+ self.log_updated.emit(f"Warning: Could not read file for metadata extraction: {e}")
+ return []
+
+ # Extract metadata tags using regex
+ title_match = re.search(r"<]*)>>", text)
+ artist_match = re.search(r"<]*)>>", text)
+ album_match = re.search(r"<]*)>>", text)
+ year_match = re.search(r"<]*)>>", text)
+ album_artist_match = re.search(r"<]*)>>", text)
+
+ # Use display path or filename as fallback for title
+ filename = os.path.splitext(os.path.basename(self.display_path if self.display_path else self.file_name))[0]
+
+ # Add title metadata
+ if title_match:
+ metadata_options.extend(["-metadata", f"title=Dinginliğin Gücü"])
+ else:
+ metadata_options.extend(["-metadata", f"title={filename}"])
+
+ # Add artist metadata
+ if artist_match:
+ metadata_options.extend(["-metadata", f"artist={artist_match.group(1)}"])
+ else:
+ metadata_options.extend(["-metadata", f"artist=Unknown"])
+
+ # Add album metadata
+ if album_match:
+ metadata_options.extend(["-metadata", f"album={album_match.group(1)}"])
+ else:
+ metadata_options.extend(["-metadata", f"album={filename}"])
+
+ # Add year metadata
+ if year_match:
+ metadata_options.extend(["-metadata", f"date={year_match.group(1)}"])
+ else:
+ # Use current year if year is not specified
+ import datetime
+ current_year = datetime.datetime.now().year
+ metadata_options.extend(["-metadata", f"date={current_year}"])
+
+ # Add album artist metadata
+ if album_artist_match:
+ metadata_options.extend(["-metadata", f"album_artist={album_artist_match.group(1)}"])
+ else:
+ metadata_options.extend(["-metadata", f"album_artist=Unknown"])
+
+ # Add these to ffmpeg command
+ return metadata_options
+
def _srt_time(self, t):
"""Helper function to format time for SRT files"""
h = int(t // 3600)
diff --git a/abogen/gui.py b/abogen/gui.py
index 661f95f..485cfc7 100644
--- a/abogen/gui.py
+++ b/abogen/gui.py
@@ -938,10 +938,12 @@ class abogen(QWidget):
title = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('title', 'Unknown')))
publisher = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('publisher', 'Unknown')))
authors = [re.sub(r'<[^>]+>', '', str(author)) for author in dialog.book_metadata.get('authors', ['Unknown'])]
+ publication_year = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('publication_year', 'Unknown')))
f.write(f"Title: {title}\n")
f.write(f"Authors: {', '.join(authors)}\n")
f.write(f"Publisher: {publisher}\n")
+ f.write(f"Publication Year: {publication_year}\n")
if dialog.book_metadata.get('description'):
description = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('description')))
f.write(f"\nDescription:\n{description}\n")