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.
|
* 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.
|
* 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;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select-card.used {
|
||||||
|
background: #122542;
|
||||||
|
border-color: #2b4c7a;
|
||||||
|
}
|
||||||
|
|
||||||
.select-card input[type=\"checkbox\"] {
|
.select-card input[type=\"checkbox\"] {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
|
|
@ -242,6 +247,11 @@ body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usage {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #8dc5ff;
|
||||||
|
}
|
||||||
|
|
||||||
.compose-form {
|
.compose-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,17 @@ def create_app() -> Flask:
|
||||||
secret_key = app.config.get("SECRET_KEY") or "dev-secret-key"
|
secret_key = app.config.get("SECRET_KEY") or "dev-secret-key"
|
||||||
app.config["SECRET_KEY"] = secret_key
|
app.config["SECRET_KEY"] = secret_key
|
||||||
|
|
||||||
db_path = app.config.get("ALT_TEXT_DB")
|
instance_dir = Path(app.instance_path)
|
||||||
if not db_path:
|
instance_dir.mkdir(parents=True, exist_ok=True)
|
||||||
db_path = str(Path(app.instance_path) / "alt_text_cache.db")
|
|
||||||
app.config["ALT_TEXT_DB"] = db_path
|
|
||||||
|
|
||||||
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(
|
app.immich_client = ImmichClient(
|
||||||
base_url=app.config["IMMICH_API_URL"],
|
base_url=app.config["IMMICH_API_URL"],
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Dict, Iterable, Optional
|
||||||
|
|
||||||
|
|
||||||
class AltTextCache:
|
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()
|
conn.commit()
|
||||||
|
|
||||||
def get(self, asset_id: str) -> Optional[str]:
|
def get(self, asset_id: str) -> Optional[str]:
|
||||||
|
|
@ -57,3 +66,31 @@ class AltTextCache:
|
||||||
(asset_id, alt_text, timestamp),
|
(asset_id, alt_text, timestamp),
|
||||||
)
|
)
|
||||||
conn.commit()
|
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()
|
load_dotenv()
|
||||||
|
|
||||||
|
db_path = os.getenv("STATION_DB", "")
|
||||||
|
|
||||||
settings: Dict[str, str] = {
|
settings: Dict[str, str] = {
|
||||||
"IMMICH_API_URL": os.getenv("IMMICH_API_URL", DEFAULT_IMMICH_URL).rstrip("/"),
|
"IMMICH_API_URL": os.getenv("IMMICH_API_URL", DEFAULT_IMMICH_URL).rstrip("/"),
|
||||||
"IMMICH_API_KEY": os.getenv("IMMICH_API_KEY", ""),
|
"IMMICH_API_KEY": os.getenv("IMMICH_API_KEY", ""),
|
||||||
"OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", ""),
|
"OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", ""),
|
||||||
"RECENT_DAYS": int(os.getenv("RECENT_DAYS", "3")),
|
"RECENT_DAYS": int(os.getenv("RECENT_DAYS", "3")),
|
||||||
"OPENAI_MODEL": os.getenv("OPENAI_MODEL", "gpt-4o-mini"),
|
"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", ""),
|
"SECRET_KEY": os.getenv("SECRET_KEY", ""),
|
||||||
"MASTODON_BASE_URL": os.getenv(
|
"MASTODON_BASE_URL": os.getenv(
|
||||||
"MASTODON_BASE_URL", "http://localhost:3000"
|
"MASTODON_BASE_URL", "http://localhost:3000"
|
||||||
|
|
|
||||||
|
|
@ -205,14 +205,21 @@ def compose_select():
|
||||||
except ImmichError as exc:
|
except ImmichError as exc:
|
||||||
error_message = str(exc)
|
error_message = str(exc)
|
||||||
|
|
||||||
|
usage_map = alt_cache.get_usage_map(asset.id for asset in asset_objs)
|
||||||
|
|
||||||
assets = []
|
assets = []
|
||||||
for asset in asset_objs:
|
for asset in asset_objs:
|
||||||
|
usage = usage_map.get(asset.id)
|
||||||
assets.append(
|
assets.append(
|
||||||
{
|
{
|
||||||
"id": asset.id,
|
"id": asset.id,
|
||||||
"file_name": asset.file_name,
|
"file_name": asset.file_name,
|
||||||
"captured_display": _humanize_timestamp(asset.captured_at),
|
"captured_display": _humanize_timestamp(asset.captured_at),
|
||||||
"alt_text": alt_cache.get(asset.id),
|
"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
|
post_text.strip(), media_ids, in_reply_to_id=reply_id
|
||||||
)
|
)
|
||||||
status_url = status.get("url")
|
status_url = status.get("url")
|
||||||
|
for asset in assets:
|
||||||
|
alt_cache.mark_posted(asset.id, status_url)
|
||||||
if status_url:
|
if status_url:
|
||||||
flash(
|
flash(
|
||||||
f'Post sent to Mastodon: <a href="{status_url}" target="_blank" rel="noopener">{status_url}</a>',
|
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)
|
content, mimetype = immich_client.fetch_asset_content(asset_id, variant)
|
||||||
except ImmichError as exc:
|
except ImmichError as exc:
|
||||||
abort(404, description=str(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>
|
||||||
<div class="select-grid">
|
<div class="select-grid">
|
||||||
{% for asset in assets %}
|
{% 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 %}>
|
<input type="checkbox" name="asset_ids" value="{{ asset.id }}" {% if selected_ids and asset.id in selected_ids %}checked{% endif %}>
|
||||||
<div class="thumbnail">
|
<div class="thumbnail">
|
||||||
<img src="{{ url_for('main.asset_proxy', asset_id=asset.id, variant='thumbnail') }}" alt="{{ asset.file_name }}">
|
<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 %}
|
{% if asset.alt_text %}
|
||||||
<div class="alt-text-sample">{{ asset.alt_text }}</div>
|
<div class="alt-text-sample">{{ asset.alt_text }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if asset.posted %}
|
||||||
|
<div class="usage">Posted{% if asset.last_posted_display %} {{ asset.last_posted_display }}{% endif %}</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue