Blame Raw
permissionBRICK · 9f53616a · · 134 lines (4.6 KB)
1 contributor
1#!/usr/bin/env python3
2"""
3Obsidian ebook exporter.
4
5Watches a mounted Obsidian vault for metadata.md files with `export: true`
6in their YAML frontmatter. When found, it builds DOCX, EPUB, and MOBI files
7in the mounted output folder, then resets the flag back to false.
8"""
9
10import logging
11import os
12import sys
13import time
14from pathlib import Path
15
16from epub_builder import build_outputs, discover_stories, set_output_ownership, write_frontmatter
17
18# ── Configuration from environment variables ──────────────────────────
19
20VAULT_PATH = os.environ.get("VAULT_PATH", "/vault")
21CREATIVE_FOLDER = os.environ.get("CREATIVE_FOLDER", "Creative")
22OUTPUT_DIR = os.environ.get("OUTPUT_DIR", "/output")
23
24LUA_FILTER_PATH = os.environ.get("LUA_FILTER_PATH", "/app/center-v.lua")
25PAGEBREAK_FILTER_PATH = os.environ.get("PAGEBREAK_FILTER_PATH", "/app/pagebreak.lua")
26CSS_FILE_PATH = os.environ.get("CSS_FILE_PATH", "/app/epub-style.css")
27
28# How often to scan for export flags (seconds)
29POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL", "10"))
30
31LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
32
33# ── Logging setup ─────────────────────────────────────────────────────
34
35logging.basicConfig(
36 level=getattr(logging, LOG_LEVEL.upper(), logging.INFO),
37 format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
38 datefmt="%Y-%m-%d %H:%M:%S",
39 handlers=[logging.StreamHandler(sys.stdout)],
40)
41logger = logging.getLogger("epub-exporter")
42
43
44def reset_export_flag(story: dict):
45 """Reset the export flag to false in the metadata.md file."""
46 metadata_file = story["metadata_file"]
47 metadata = story["metadata"]
48 raw_text = story.get("metadata_raw", "")
49
50 metadata["export"] = False
51 new_content = write_frontmatter(metadata, raw_text)
52
53 try:
54 with open(metadata_file, "w", encoding="utf-8") as f:
55 f.write(new_content)
56 logger.info(f"Reset export flag: {metadata_file}")
57 except Exception as e:
58 logger.error(f"Failed to reset export flag in {metadata_file}: {e}")
59
60
61def process_exports(vault_path: Path):
62 """Scan for stories with export: true and build ebook outputs."""
63 stories = discover_stories(vault_path, CREATIVE_FOLDER)
64 to_export = [s for s in stories if s["metadata"].get("export") is True]
65
66 if not to_export:
67 return
68
69 output_path = Path(OUTPUT_DIR)
70 lua_filter = Path(LUA_FILTER_PATH) if LUA_FILTER_PATH else None
71 pagebreak_filter = Path(PAGEBREAK_FILTER_PATH) if PAGEBREAK_FILTER_PATH else None
72 css_file = Path(CSS_FILE_PATH) if CSS_FILE_PATH else None
73
74 for story in to_export:
75 title = story["metadata"].get("title", story["path"].name)
76 logger.info(f"Export requested for: '{title}'")
77
78 built_files = build_outputs(
79 story=story,
80 output_dir=output_path,
81 lua_filter=lua_filter,
82 pagebreak_filter=pagebreak_filter,
83 css_file=css_file if css_file and css_file.exists() else None,
84 )
85
86 if built_files:
87 created = ", ".join(str(path.name) for path in built_files.values())
88 logger.info(f"Successfully exported: {created}")
89 reset_export_flag(story)
90 else:
91 logger.error(f"Failed to export '{title}'")
92
93
94def main():
95 logger.info("=" * 60)
96 logger.info("Obsidian ebook exporter starting up")
97 logger.info("=" * 60)
98 logger.info(f"Vault path: {VAULT_PATH}")
99 logger.info(f"Creative folder: {CREATIVE_FOLDER}")
100 logger.info(f"Output dir: {OUTPUT_DIR}")
101 logger.info(f"Poll interval: {POLL_INTERVAL}s")
102
103 vault_path = Path(VAULT_PATH)
104 output_path = Path(OUTPUT_DIR)
105 output_path.mkdir(parents=True, exist_ok=True)
106 set_output_ownership(output_path)
107
108 if not vault_path.exists():
109 logger.error(f"Vault path does not exist: {vault_path}")
110 logger.info("Waiting for vault to become available...")
111 while not vault_path.exists():
112 time.sleep(5)
113 logger.info("Vault path is now available")
114
115 # Check for any pending exports on startup
116 logger.info("Checking for pending exports...")
117 process_exports(vault_path)
118
119 # Main loop: poll the filesystem
120 logger.info("Entering watch loop...")
121 while True:
122 try:
123 time.sleep(POLL_INTERVAL)
124 process_exports(vault_path)
125 except KeyboardInterrupt:
126 logger.info("Shutting down gracefully...")
127 break
128 except Exception as e:
129 logger.error(f"Error in main loop: {e}")
130 time.sleep(POLL_INTERVAL)
131
132
133if __name__ == "__main__":
134 main()