From 5e3b1a2297838cd924e2ab8f1e354c68f16a28ea Mon Sep 17 00:00:00 2001 From: Mike Swanson Date: Wed, 29 Apr 2026 19:50:50 -0700 Subject: [PATCH] radio: add /api/db.sqlite for offline laptop sync Streams the read-only archive.db over the same Tailscale-routed port as the search service. Companion to azcomputerguru/radio-archive-portable which curl-fetches from this endpoint and runs locally on the laptop. Disclosure equivalent to /api/search (which already exposes every transcript), so no auth added. Deployed to Jupiter; verified GET returns 60 MB SQLite blob with all 1,405 classifier rows intact. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../radio-show/audio-processor/server/main.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/projects/radio-show/audio-processor/server/main.py b/projects/radio-show/audio-processor/server/main.py index 502ae67..bff4dbc 100644 --- a/projects/radio-show/audio-processor/server/main.py +++ b/projects/radio-show/audio-processor/server/main.py @@ -20,7 +20,7 @@ from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, HTTPException, Query -from fastapi.responses import HTMLResponse +from fastapi.responses import FileResponse, HTMLResponse DB_PATH = os.environ.get("ARCHIVE_DB", "/data/archive.db") PORT = int(os.environ.get("PORT", "8765")) @@ -218,6 +218,23 @@ def stats(): return {"counts": counts, "by_year": by_year} +@app.get("/api/db.sqlite") +def download_db(): + """Stream the read-only archive.db for offline laptop sync. + + Anyone who can reach /api/search can already read every transcript, + so exposing the underlying SQLite file adds no meaningful disclosure. + Sync side: curl -o archive.db :/api/db.sqlite + """ + if not Path(DB_PATH).exists(): + raise HTTPException(404, "archive db not present") + return FileResponse( + DB_PATH, + media_type="application/vnd.sqlite3", + filename="archive.db", + ) + + @app.get("/", response_class=HTMLResponse) def index(): return INDEX_HTML