"""Flask routes for the alt text generator.""" from __future__ import annotations import base64 from datetime import datetime from flask import ( Blueprint, Response, abort, current_app, flash, redirect, render_template, request, url_for, ) from .immich import ImmichError from .openai_client import AltTextGenerationError bp = Blueprint("main", __name__) def _parse_timestamp(raw: str | None) -> datetime | None: if not raw: return None try: normalized = raw.replace("Z", "+00:00") return datetime.fromisoformat(normalized) except ValueError: return None def _humanize_timestamp(raw: str | None) -> str | None: dt = _parse_timestamp(raw) if not dt: return raw return dt.strftime("%d %b %Y / %H:%M") def _timestamp_with_timezone(raw: str | None) -> str | None: dt = _parse_timestamp(raw) if not dt: return raw label = dt.strftime("%d %b %Y %H:%M") tzname = dt.tzname() if tzname: return f"{label} {tzname}" offset = dt.utcoffset() if offset is not None: total_minutes = int(offset.total_seconds() // 60) hours, minutes = divmod(abs(total_minutes), 60) sign = "+" if total_minutes >= 0 else "-" return f"{label} UTC{sign}{hours:02d}:{minutes:02d}" return f"{label} (timezone unknown)" @bp.route("/") def index(): immich_client = current_app.immich_client alt_cache = current_app.alt_text_cache days = current_app.config.get("RECENT_DAYS", 3) assets = [] error_message = None try: for asset in immich_client.get_recent_assets(days=days): assets.append( { "id": asset.id, "file_name": asset.file_name, "captured_at": asset.captured_at, "captured_display": _humanize_timestamp(asset.captured_at), "thumbnail_url": asset.thumbnail_url, "alt_text": alt_cache.get(asset.id), } ) except ImmichError as exc: error_message = str(exc) return render_template("index.html", assets=assets, error_message=error_message) @bp.route("/assets/", methods=["GET", "POST"]) def asset_detail(asset_id: str): immich_client = current_app.immich_client alt_cache = current_app.alt_text_cache generator = current_app.alt_text_generator error_message = None notes = "" try: asset = immich_client.get_asset(asset_id) except ImmichError as exc: return render_template( "detail.html", asset=None, alt_text=None, error_message=str(exc), notes=notes, ) alt_text = alt_cache.get(asset_id) if request.method == "POST": notes = request.form.get("notes", "") try: content, mime_type = immich_client.fetch_asset_content(asset_id, "preview") data_url = "data:{};base64,{}".format( mime_type, base64.b64encode(content).decode("ascii") ) location_text = asset.location_label coordinates_text = None if asset.latitude is not None and asset.longitude is not None: coordinates_text = f"{asset.latitude:.5f}, {asset.longitude:.5f}" captured_text = _timestamp_with_timezone(asset.captured_at) generated = generator.generate_alt_text( data_url, notes, captured_at=captured_text, location=location_text, coordinates=coordinates_text, ) alt_cache.set(asset_id, generated) flash("Alt text generated.") return redirect(url_for("main.asset_detail", asset_id=asset_id)) except ImmichError as exc: error_message = f"Failed to fetch image: {exc}" except AltTextGenerationError as exc: error_message = str(exc) formatted_asset = { "id": asset.id, "file_name": asset.file_name, "captured_at": asset.captured_at, "captured_display": _humanize_timestamp(asset.captured_at), "thumbnail_url": asset.thumbnail_url, "preview_url": asset.preview_url, "original_url": asset.original_url, "web_url": asset.web_url, "latitude": asset.latitude, "longitude": asset.longitude, "location": asset.location_label, } return render_template( "detail.html", asset=formatted_asset, alt_text=alt_cache.get(asset_id), error_message=error_message, notes=notes, ) @bp.route("/proxy/assets//") def asset_proxy(asset_id: str, variant: str): immich_client = current_app.immich_client try: content, mimetype = immich_client.fetch_asset_content(asset_id, variant) except ImmichError as exc: abort(404, description=str(exc)) return Response(content, mimetype=mimetype)