#!/usr/bin/env python3 """ Obsidian ebook exporter. Watches a mounted Obsidian vault for metadata.md files with `export: true` in their YAML frontmatter. When found, it builds DOCX, EPUB, and MOBI files in the mounted output folder, then resets the flag back to false. """ import logging import os import sys import time from pathlib import Path from epub_builder import build_outputs, discover_stories, set_output_ownership, write_frontmatter # ── Configuration from environment variables ────────────────────────── VAULT_PATH = os.environ.get("VAULT_PATH", "/vault") CREATIVE_FOLDER = os.environ.get("CREATIVE_FOLDER", "Creative") OUTPUT_DIR = os.environ.get("OUTPUT_DIR", "/output") LUA_FILTER_PATH = os.environ.get("LUA_FILTER_PATH", "/app/center-v.lua") PAGEBREAK_FILTER_PATH = os.environ.get("PAGEBREAK_FILTER_PATH", "/app/pagebreak.lua") CSS_FILE_PATH = os.environ.get("CSS_FILE_PATH", "/app/epub-style.css") # How often to scan for export flags (seconds) POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL", "10")) LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO") # ── Logging setup ───────────────────────────────────────────────────── logging.basicConfig( level=getattr(logging, LOG_LEVEL.upper(), logging.INFO), format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", handlers=[logging.StreamHandler(sys.stdout)], ) logger = logging.getLogger("epub-exporter") def reset_export_flag(story: dict): """Reset the export flag to false in the metadata.md file.""" metadata_file = story["metadata_file"] metadata = story["metadata"] raw_text = story.get("metadata_raw", "") metadata["export"] = False new_content = write_frontmatter(metadata, raw_text) try: with open(metadata_file, "w", encoding="utf-8") as f: f.write(new_content) logger.info(f"Reset export flag: {metadata_file}") except Exception as e: logger.error(f"Failed to reset export flag in {metadata_file}: {e}") def process_exports(vault_path: Path): """Scan for stories with export: true and build ebook outputs.""" stories = discover_stories(vault_path, CREATIVE_FOLDER) to_export = [s for s in stories if s["metadata"].get("export") is True] if not to_export: return output_path = Path(OUTPUT_DIR) lua_filter = Path(LUA_FILTER_PATH) if LUA_FILTER_PATH else None pagebreak_filter = Path(PAGEBREAK_FILTER_PATH) if PAGEBREAK_FILTER_PATH else None css_file = Path(CSS_FILE_PATH) if CSS_FILE_PATH else None for story in to_export: title = story["metadata"].get("title", story["path"].name) logger.info(f"Export requested for: '{title}'") built_files = build_outputs( story=story, output_dir=output_path, lua_filter=lua_filter, pagebreak_filter=pagebreak_filter, css_file=css_file if css_file and css_file.exists() else None, ) if built_files: created = ", ".join(str(path.name) for path in built_files.values()) logger.info(f"Successfully exported: {created}") reset_export_flag(story) else: logger.error(f"Failed to export '{title}'") def main(): logger.info("=" * 60) logger.info("Obsidian ebook exporter starting up") logger.info("=" * 60) logger.info(f"Vault path: {VAULT_PATH}") logger.info(f"Creative folder: {CREATIVE_FOLDER}") logger.info(f"Output dir: {OUTPUT_DIR}") logger.info(f"Poll interval: {POLL_INTERVAL}s") vault_path = Path(VAULT_PATH) output_path = Path(OUTPUT_DIR) output_path.mkdir(parents=True, exist_ok=True) set_output_ownership(output_path) if not vault_path.exists(): logger.error(f"Vault path does not exist: {vault_path}") logger.info("Waiting for vault to become available...") while not vault_path.exists(): time.sleep(5) logger.info("Vault path is now available") # Check for any pending exports on startup logger.info("Checking for pending exports...") process_exports(vault_path) # Main loop: poll the filesystem logger.info("Entering watch loop...") while True: try: time.sleep(POLL_INTERVAL) process_exports(vault_path) except KeyboardInterrupt: logger.info("Shutting down gracefully...") break except Exception as e: logger.error(f"Error in main loop: {e}") time.sleep(POLL_INTERVAL) if __name__ == "__main__": main()