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")