166 lines
4.9 KiB
Python
166 lines
4.9 KiB
Python
"""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/<asset_id>", 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/<asset_id>/<variant>")
|
|
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)
|