import re import shutil from pathlib import Path from flask import Blueprint, current_app, jsonify, render_template, request from app.immich import ImmichClient from app.state import load_state, save_state bp = Blueprint("export", __name__) def slugify(text: str) -> str: text = text.lower().strip() text = re.sub(r"[^\w\s-]", "", text) return re.sub(r"[\s_-]+", "-", text).strip("-") def _client(): return ImmichClient( current_app.config["IMMICH_URL"], current_app.config["IMMICH_API_KEY"], ) @bp.get("/export") def export_view(): album_id = request.args["album_id"] state = load_state(album_id, current_app) to_export = [g for g in state.groups if g.status == "written"] skipped = [g for g in state.groups if g.status == "skipped"] return render_template( "phase6.html", state=state, to_export=to_export, skipped=skipped, current_phase="export", album_id=album_id, phase_stale=state.phase_stale, notes_content=state.notes, ) @bp.post("/export/run") def run_export(): body = request.get_json() album_id = body["album_id"] overwrite_ids = set(body.get("overwrite_ids", [])) state = load_state(album_id, current_app) pages_dir = Path(current_app.config["PAGES_DIR"]) client = _client() photo_map = {p.id: p for p in state.photos} results = [] for group in state.groups: if group.status != "written": continue title_slug = slugify(group.title or group.date or "entry") if group.entry_type == "journal": folder_name = f"{group.date}-{title_slug}.entry" dest = pages_dir / "01.trips" / state.grav_trip_slug / "01.dailies" / folder_name md_file = "entry.md" template = "entry" else: folder_name = f"{title_slug}.story" dest = pages_dir / "01.trips" / state.grav_trip_slug / "04.stories" / folder_name md_file = "entry.md" template = "story" if dest.exists() and group.id not in overwrite_ids: results.append({ "group_id": group.id, "needs_overwrite": True, "title": group.title, "dest": str(dest), }) # Mark as exported since destination already exists group.status = "exported" continue if dest.exists(): shutil.rmtree(dest) dest.mkdir(parents=True, exist_ok=True) # Download photos failed = [] hero_filename = None photo_num = 1 for pid in group.photo_ids: photo = photo_map.get(pid) if not photo: continue filename = f"photo-{photo_num}.jpg" try: data = client.get_original(pid) (dest / filename).write_bytes(data) if pid == group.hero_photo_id or photo_num == 1: hero_filename = filename photo_num += 1 except Exception as e: current_app.logger.warning("Failed to download asset %s: %s", pid, e) failed.append({"asset_id": pid, "error": str(e)}) # Build frontmatter date_str = (group.date + " 12:00") if group.date else "" if group.entry_type == "journal": frontmatter = ( f"---\n" f"title: '{group.title}'\n" f"date: '{date_str}'\n" f"template: {template}\n" f"published: true\n" f"location_city: '{group.location_city}'\n" f"location_country: '{group.location_country}'\n" f"hero_image: {hero_filename or ''}\n" f"---\n" ) else: frontmatter = ( f"---\n" f"title: '{group.title}'\n" f"date: '{date_str}'\n" f"template: {template}\n" f"published: true\n" f"hero_image: {hero_filename or ''}\n" f"---\n" ) body_text = group.body or "" if group.shortcode_hints: body_text += f"\n" (dest / md_file).write_text(frontmatter + "\n" + body_text) group.status = "exported" results.append({ "group_id": group.id, "title": group.title, "dest": str(dest), "failed_photos": failed, }) save_state(state, current_app) return jsonify({"ok": True, "results": results}) @bp.post("/export/overwrite") def overwrite_export(): body = request.get_json() album_id = body["album_id"] group_id = body["group_id"] state = load_state(album_id, current_app) pages_dir = Path(current_app.config["PAGES_DIR"]) client = _client() photo_map = {p.id: p for p in state.photos} group = next((g for g in state.groups if g.id == group_id), None) if group is None: return jsonify({"ok": False, "error": "group not found"}), 404 title_slug = slugify(group.title or group.date or "entry") if group.entry_type == "journal": folder_name = f"{group.date}-{title_slug}.entry" dest = pages_dir / "01.trips" / state.grav_trip_slug / "01.dailies" / folder_name md_file = "entry.md" template = "entry" else: folder_name = f"{title_slug}.story" dest = pages_dir / "01.trips" / state.grav_trip_slug / "04.stories" / folder_name md_file = "entry.md" template = "story" if dest.exists(): shutil.rmtree(dest) dest.mkdir(parents=True, exist_ok=True) failed = [] hero_filename = None photo_num = 1 for pid in group.photo_ids: photo = photo_map.get(pid) if not photo: continue filename = f"photo-{photo_num}.jpg" try: data = client.get_original(pid) (dest / filename).write_bytes(data) if pid == group.hero_photo_id or photo_num == 1: hero_filename = filename photo_num += 1 except Exception as e: current_app.logger.warning("Failed to download asset %s: %s", pid, e) failed.append({"asset_id": pid, "error": str(e)}) date_str = (group.date + " 12:00") if group.date else "" if group.entry_type == "journal": frontmatter = ( f"---\n" f"title: '{group.title}'\n" f"date: '{date_str}'\n" f"template: {template}\n" f"published: true\n" f"location_city: '{group.location_city}'\n" f"location_country: '{group.location_country}'\n" f"hero_image: {hero_filename or ''}\n" f"---\n" ) else: frontmatter = ( f"---\n" f"title: '{group.title}'\n" f"date: '{date_str}'\n" f"template: {template}\n" f"published: true\n" f"hero_image: {hero_filename or ''}\n" f"---\n" ) body_text = group.body or "" if group.shortcode_hints: body_text += f"\n" (dest / md_file).write_text(frontmatter + "\n" + body_text) group.status = "exported" save_state(state, current_app) return jsonify({ "ok": True, "exported": 1, "title": group.title, "dest": str(dest), "failed_photos": failed, })