From dc32cc7222f81ce70fbae7e7a284f10489219753 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 15 Nov 2025 19:24:59 +0000 Subject: [PATCH] Track posted photos. Cache images. --- README.md | 2 +- static/styles.css | 10 +++++++++ station_announcer/__init__.py | 15 +++++++++----- station_announcer/cache.py | 39 ++++++++++++++++++++++++++++++++++- station_announcer/config.py | 4 +++- station_announcer/routes.py | 13 +++++++++++- templates/compose_select.html | 5 ++++- 7 files changed, 78 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 63e8402..f55aefc 100644 --- a/README.md +++ b/README.md @@ -30,4 +30,4 @@ Station Announcer is a small Flask app that keeps your Immich library and Mastod * Hit **Improve text with ChatGPT** as many times as you like; the model polishes grammar, tone, and UK English spellings while respecting your extra notes. * When ready, choose **Post to Mastodon** to upload the selected media (with their alt text) and publish the refined caption (optionally as a reply) via the configured access token. -The app stores alt text in `instance/alt_text_cache.db`. Remove this file to clear the cache. +The app stores cached data in `instance/station_announcer.db` (or wherever `STATION_DB` points). Remove this file to clear the cache. diff --git a/static/styles.css b/static/styles.css index 471c0c2..0664dbe 100644 --- a/static/styles.css +++ b/static/styles.css @@ -222,6 +222,11 @@ body { cursor: pointer; } +.select-card.used { + background: #122542; + border-color: #2b4c7a; +} + .select-card input[type=\"checkbox\"] { width: 1rem; height: 1rem; @@ -242,6 +247,11 @@ body { overflow: hidden; } +.usage { + font-size: 0.8rem; + color: #8dc5ff; +} + .compose-form { display: flex; flex-direction: column; diff --git a/station_announcer/__init__.py b/station_announcer/__init__.py index d4cbb1d..c942b0a 100644 --- a/station_announcer/__init__.py +++ b/station_announcer/__init__.py @@ -27,12 +27,17 @@ def create_app() -> Flask: secret_key = app.config.get("SECRET_KEY") or "dev-secret-key" app.config["SECRET_KEY"] = secret_key - db_path = app.config.get("ALT_TEXT_DB") - if not db_path: - db_path = str(Path(app.instance_path) / "alt_text_cache.db") - app.config["ALT_TEXT_DB"] = db_path + instance_dir = Path(app.instance_path) + instance_dir.mkdir(parents=True, exist_ok=True) - Path(app.instance_path).mkdir(parents=True, exist_ok=True) + db_path = app.config.get("STATION_DB") + if not db_path: + old_db = instance_dir / "alt_text_cache.db" + new_db = instance_dir / "station_announcer.db" + if old_db.exists() and not new_db.exists(): + old_db.rename(new_db) + db_path = str(new_db) + app.config["STATION_DB"] = db_path app.immich_client = ImmichClient( base_url=app.config["IMMICH_API_URL"], diff --git a/station_announcer/cache.py b/station_announcer/cache.py index f11be6c..206cc20 100644 --- a/station_announcer/cache.py +++ b/station_announcer/cache.py @@ -5,7 +5,7 @@ from __future__ import annotations import sqlite3 from datetime import datetime, timezone from pathlib import Path -from typing import Optional +from typing import Dict, Iterable, Optional class AltTextCache: @@ -33,6 +33,15 @@ class AltTextCache: ) """ ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS asset_usage ( + asset_id TEXT PRIMARY KEY, + last_posted_at TEXT NOT NULL, + last_post_url TEXT + ) + """ + ) conn.commit() def get(self, asset_id: str) -> Optional[str]: @@ -57,3 +66,31 @@ class AltTextCache: (asset_id, alt_text, timestamp), ) conn.commit() + + def mark_posted(self, asset_id: str, post_url: Optional[str] = None) -> None: + timestamp = datetime.now(timezone.utc).isoformat() + with self._connect() as conn: + conn.execute( + """ + INSERT INTO asset_usage(asset_id, last_posted_at, last_post_url) + VALUES(?, ?, ?) + ON CONFLICT(asset_id) + DO UPDATE SET last_posted_at = excluded.last_posted_at, + last_post_url = excluded.last_post_url + """, + (asset_id, timestamp, post_url), + ) + conn.commit() + + def get_usage_map(self, asset_ids: Iterable[str]) -> Dict[str, sqlite3.Row]: + ids = [asset_id for asset_id in asset_ids if asset_id] + if not ids: + return {} + placeholders = ",".join(["?"] * len(ids)) + query = ( + "SELECT asset_id, last_posted_at, last_post_url FROM asset_usage" + f" WHERE asset_id IN ({placeholders})" + ) + with self._connect() as conn: + rows = conn.execute(query, ids).fetchall() + return {row["asset_id"]: row for row in rows} diff --git a/station_announcer/config.py b/station_announcer/config.py index befd2bf..9abb2b2 100644 --- a/station_announcer/config.py +++ b/station_announcer/config.py @@ -19,13 +19,15 @@ def load_settings() -> Dict[str, str]: load_dotenv() + db_path = os.getenv("STATION_DB", "") + settings: Dict[str, str] = { "IMMICH_API_URL": os.getenv("IMMICH_API_URL", DEFAULT_IMMICH_URL).rstrip("/"), "IMMICH_API_KEY": os.getenv("IMMICH_API_KEY", ""), "OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", ""), "RECENT_DAYS": int(os.getenv("RECENT_DAYS", "3")), "OPENAI_MODEL": os.getenv("OPENAI_MODEL", "gpt-4o-mini"), - "ALT_TEXT_DB": os.getenv("ALT_TEXT_DB", ""), + "STATION_DB": db_path, "SECRET_KEY": os.getenv("SECRET_KEY", ""), "MASTODON_BASE_URL": os.getenv( "MASTODON_BASE_URL", "http://localhost:3000" diff --git a/station_announcer/routes.py b/station_announcer/routes.py index fd3eb07..c99ffa4 100644 --- a/station_announcer/routes.py +++ b/station_announcer/routes.py @@ -205,14 +205,21 @@ def compose_select(): except ImmichError as exc: error_message = str(exc) + usage_map = alt_cache.get_usage_map(asset.id for asset in asset_objs) + assets = [] for asset in asset_objs: + usage = usage_map.get(asset.id) assets.append( { "id": asset.id, "file_name": asset.file_name, "captured_display": _humanize_timestamp(asset.captured_at), "alt_text": alt_cache.get(asset.id), + "posted": bool(usage), + "last_posted_display": _humanize_timestamp( + usage["last_posted_at"] if usage else None + ), } ) @@ -369,6 +376,8 @@ def compose_draft(): post_text.strip(), media_ids, in_reply_to_id=reply_id ) status_url = status.get("url") + for asset in assets: + alt_cache.mark_posted(asset.id, status_url) if status_url: flash( f'Post sent to Mastodon: {status_url}', @@ -407,4 +416,6 @@ def asset_proxy(asset_id: str, variant: str): content, mimetype = immich_client.fetch_asset_content(asset_id, variant) except ImmichError as exc: abort(404, description=str(exc)) - return Response(content, mimetype=mimetype) + response = Response(content, mimetype=mimetype) + response.headers.setdefault("Cache-Control", "public, max-age=3600") + return response diff --git a/templates/compose_select.html b/templates/compose_select.html index d5eb4a7..7d8aa48 100644 --- a/templates/compose_select.html +++ b/templates/compose_select.html @@ -18,7 +18,7 @@
{% for asset in assets %} -