5160368407
Implements GET /export summary view and POST /export/run which downloads originals from Immich, writes entry.md with YAML frontmatter, and sets group status to exported. Includes POST /export/overwrite for single-group re-export. All 42 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
235 lines
7.4 KiB
Python
235 lines
7.4 KiB
Python
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<!-- shortcode hints:\n{group.shortcode_hints}\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<!-- shortcode hints:\n{group.shortcode_hints}\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,
|
|
})
|