Added extra metadata support for chaptered M4B files

This commit is contained in:
Deniz Şafak
2025-05-21 18:42:41 +03:00
parent fd10aab6f3
commit 669ef26667
4 changed files with 181 additions and 23 deletions
+4 -2
View File
@@ -1,9 +1,11 @@
# v1.0.8 (pre-release) # v1.0.8 (pre-release)
- Added support for AMD GPUs in Linux (Special thanks to @hg000125 for his contribution in #23) - 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. - 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. - Fixed voice preview player keeps playing silently at the background after preview ends.
- Improved input box background color handling, fixed display issues in Linux Wayland. - Improved input box background color handling, fixed display issues in Linux.
- Better sleep state handling for Linux. - Better sleep state handling for Linux.
- Improvements in documentation and code.
# v1.0.7 # v1.0.7
- Improve chaptered audio generation by outputting directly as `m4b` instead of converting from `wav`. - Improve chaptered audio generation by outputting directly as `m4b` instead of converting from `wav`.
+97 -17
View File
@@ -1494,8 +1494,13 @@ class HandlerDialog(QDialog):
authors_text = ", ".join(self.book_metadata["authors"]) authors_text = ", ".join(self.book_metadata["authors"])
html_content += f"<p style='text-align: center; font-style: italic;'>By {authors_text}</p>" html_content += f"<p style='text-align: center; font-style: italic;'>By {authors_text}</p>"
if self.book_metadata["publisher"]: if self.book_metadata["publisher"] or self.book_metadata.get("publication_year"):
html_content += f"<p style='text-align: center;'>Published by {self.book_metadata['publisher']}</p>" 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"<p style='text-align: center;'>{' | '.join(pub_info)}</p>"
html_content += "<hr/>" html_content += "<hr/>"
@@ -1517,24 +1522,51 @@ class HandlerDialog(QDialog):
"description": None, "description": None,
"cover_image": None, "cover_image": None,
"publisher": None, "publisher": None,
"publication_year": None,
} }
if self.file_type == "epub": if self.file_type == "epub":
title_items = self.book.get_metadata("DC", "title") try:
if title_items: title_items = self.book.get_metadata("DC", "title")
metadata["title"] = title_items[0][0] 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") try:
if author_items: author_items = self.book.get_metadata("DC", "creator")
metadata["authors"] = [author[0] for author in author_items] 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") try:
if desc_items: desc_items = self.book.get_metadata("DC", "description")
metadata["description"] = desc_items[0][0] 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") try:
if publisher_items: publisher_items = self.book.get_metadata("DC", "publisher")
metadata["publisher"] = publisher_items[0][0] 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): for item in self.book.get_items_of_type(ebooklib.ITEM_COVER):
metadata["cover_image"] = item.get_content() metadata["cover_image"] = item.get_content()
@@ -1565,6 +1597,18 @@ class HandlerDialog(QDialog):
metadata["publisher"] = pdf_info.get("creator", None) 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: if len(self.pdf_doc) > 0:
try: try:
pix = self.pdf_doc[0].get_pixmap(matrix=fitz.Matrix(2, 2)) pix = self.pdf_doc[0].get_pixmap(matrix=fitz.Matrix(2, 2))
@@ -1580,10 +1624,43 @@ class HandlerDialog(QDialog):
else: else:
return self._get_pdf_selected_text() 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"<<METADATA_TITLE:{title}>>",
f"<<METADATA_ARTIST:{authors_text}>>",
f"<<METADATA_ALBUM:{title} ({chapter_text})>>",
f"<<METADATA_YEAR:{year}>>",
f"<<METADATA_ALBUM_ARTIST:{album_artist}>>"
]
return "\n".join(metadata_tags)
def _get_epub_selected_text(self): def _get_epub_selected_text(self):
all_checked_identifiers = set() all_checked_identifiers = set()
chapter_texts = [] chapter_texts = []
# Add metadata tags at the beginning
metadata_tags = self._format_metadata_tags()
item_order_counter = 0 item_order_counter = 0
ordered_checked_items = [] ordered_checked_items = []
@@ -1608,7 +1685,7 @@ class HandlerDialog(QDialog):
marker = f"<<CHAPTER_MARKER:{title}>>" marker = f"<<CHAPTER_MARKER:{title}>>"
chapter_texts.append(marker + "\n" + text) 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 return full_text, all_checked_identifiers
def _get_pdf_selected_text(self): def _get_pdf_selected_text(self):
@@ -1617,6 +1694,9 @@ class HandlerDialog(QDialog):
section_titles = [] section_titles = []
all_content = [] all_content = []
# Add metadata tags at the beginning
metadata_tags = self._format_metadata_tags()
pdf_has_no_bookmarks = ( pdf_has_no_bookmarks = (
hasattr(self, "has_pdf_bookmarks") and not self.has_pdf_bookmarks hasattr(self, "has_pdf_bookmarks") and not self.has_pdf_bookmarks
) )
@@ -1641,7 +1721,7 @@ class HandlerDialog(QDialog):
if text: if text:
all_content.append(text) all_content.append(text)
included_text_ids.add(page_id) 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) iterator = QTreeWidgetItemIterator(self.treeWidget)
while iterator.value(): while iterator.value():
@@ -1699,7 +1779,7 @@ class HandlerDialog(QDialog):
included_text_ids.add(identifier) included_text_ids.add(identifier)
iterator += 1 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): def on_save_chapters_changed(self, state):
self.save_chapters_separately = bool(state) self.save_chapters_separately = bool(state)
+78 -4
View File
@@ -221,6 +221,10 @@ class ConversionThread(QThread):
# Clean up text using utility function # Clean up text using utility function
text = clean_text(text) text = clean_text(text)
# Remove metadata markers from the text to be processed
metadata_pattern = r"<<METADATA_[^:]+:[^>]*>>"
text = re.sub(metadata_pattern, "", text)
# --- Chapter splitting logic --- # --- Chapter splitting logic ---
chapter_pattern = r"<<CHAPTER_MARKER:(.*?)>>" chapter_pattern = r"<<CHAPTER_MARKER:(.*?)>>"
chapter_splits = list(re.finditer(chapter_pattern, text)) chapter_splits = list(re.finditer(chapter_pattern, text))
@@ -511,7 +515,7 @@ class ConversionThread(QThread):
[ [
"ffmpeg", "ffmpeg",
"-y", "-y",
"-thread_queue_size", "1024", # Increased thread_queue_size for chapter opus "-thread_queue_size", "32768", # Increased thread_queue_size for chapter opus
"-f", "-f",
"f32le", "f32le",
"-ar", "-ar",
@@ -597,7 +601,7 @@ class ConversionThread(QThread):
opus_out_path = f"{base_filepath_no_ext}.opus" opus_out_path = f"{base_filepath_no_ext}.opus"
ffmpeg_cmd_opus = [ ffmpeg_cmd_opus = [
"ffmpeg", "-y", "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", "-f", "f32le", "-ar", "24000", "-ac", "1", "-i", "pipe:0",
"-c:a", "libopus", "-b:a", "24000", # Original bitrate "-c:a", "libopus", "-b:a", "24000", # Original bitrate
opus_out_path, opus_out_path,
@@ -701,13 +705,20 @@ class ConversionThread(QThread):
static_ffmpeg.add_paths() static_ffmpeg.add_paths()
ffmpeg_cmd = [ ffmpeg_cmd = [
"ffmpeg", "-y", "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", "-f", "f32le", "-ar", "24000", "-ac", "1", "-i", "pipe:0",
"-i", chapters_info_path, "-i", chapters_info_path,
"-map", "0:a", "-map", "0:a",
"-map_metadata", "1", "-map_metadata", "1",
"-map_chapters", "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, output_m4b_path,
] ]
@@ -742,6 +753,69 @@ class ConversionThread(QThread):
except Exception as e_clean: except Exception as e_clean:
self.log_updated.emit((f"Warning: Could not delete temporary chapter file {chapters_info_path}: {e_clean}", "orange")) 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"<<METADATA_TITLE:([^>]*)>>", text)
artist_match = re.search(r"<<METADATA_ARTIST:([^>]*)>>", text)
album_match = re.search(r"<<METADATA_ALBUM:([^>]*)>>", text)
year_match = re.search(r"<<METADATA_YEAR:([^>]*)>>", text)
album_artist_match = re.search(r"<<METADATA_ALBUM_ARTIST:([^>]*)>>", 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): def _srt_time(self, t):
"""Helper function to format time for SRT files""" """Helper function to format time for SRT files"""
h = int(t // 3600) h = int(t // 3600)
+2
View File
@@ -938,10 +938,12 @@ class abogen(QWidget):
title = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('title', 'Unknown'))) title = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('title', 'Unknown')))
publisher = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('publisher', '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'])] 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"Title: {title}\n")
f.write(f"Authors: {', '.join(authors)}\n") f.write(f"Authors: {', '.join(authors)}\n")
f.write(f"Publisher: {publisher}\n") f.write(f"Publisher: {publisher}\n")
f.write(f"Publication Year: {publication_year}\n")
if dialog.book_metadata.get('description'): if dialog.book_metadata.get('description'):
description = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('description'))) description = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('description')))
f.write(f"\nDescription:\n{description}\n") f.write(f"\nDescription:\n{description}\n")