mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Added extra metadata support for chaptered M4B files
This commit is contained in:
+4
-2
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user