Track posted photos. Cache images.
This commit is contained in:
parent
9fbe130841
commit
dc32cc7222
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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: <a href="{status_url}" target="_blank" rel="noopener">{status_url}</a>',
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
</div>
|
||||
<div class="select-grid">
|
||||
{% for asset in assets %}
|
||||
<label class="select-card">
|
||||
<label class="select-card {% if asset.posted %}used{% endif %}">
|
||||
<input type="checkbox" name="asset_ids" value="{{ asset.id }}" {% if selected_ids and asset.id in selected_ids %}checked{% endif %}>
|
||||
<div class="thumbnail">
|
||||
<img src="{{ url_for('main.asset_proxy', asset_id=asset.id, variant='thumbnail') }}" alt="{{ asset.file_name }}">
|
||||
|
|
@ -31,6 +31,9 @@
|
|||
{% if asset.alt_text %}
|
||||
<div class="alt-text-sample">{{ asset.alt_text }}</div>
|
||||
{% endif %}
|
||||
{% if asset.posted %}
|
||||
<div class="usage">Posted{% if asset.last_posted_display %} {{ asset.last_posted_display }}{% endif %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
|
|
|
|||
Loading…
Reference in a new issue