commit aefd0a7d7d6c1e96b4f611795dd3f9be1d3eefcc
parent 0c1a0f38642c0972b5cb579c5760804ab6a53734
Author: david cochran <about.trout@gmail.com>
Date: Sat, 6 Apr 2024 19:29:09 -0700
add gallery curator and generator
Diffstat:
4 files changed, 274 insertions(+), 11 deletions(-)
diff --git a/ppboatwatch/archive.py b/ppboatwatch/archive.py
@@ -5,14 +5,37 @@ from contextlib import closing
INIT_TABLES_SQL = """
-BEGIN;
-CREATE TABLE IF NOT EXISTS matches (ts, filename, label, score, x0, y0, x1, y1);
-COMMIT;
-"""
+CREATE TABLE IF NOT EXISTS matches (
+ ts INTEGER PRIMARY KEY,
+ filename,
+ label,
+ score,
+ x0, y0, x1, y1,
+ gallery DEFAULT NULL
+)"""
ADD_MATCH_SQL = """
-INSERT INTO matches (ts, filename, label, score, x0, y0, x1, y1) VALUES (?, ?, ?, ?, ?, ?, ?, ?);
-"""
+INSERT OR IGNORE INTO matches (ts, filename, label, score, x0, y0, x1, y1)
+VALUES (?, ?, ?, ?, ?, ?, ?, ?)"""
+
+LIST_MATCHES_SQL = """
+SELECT ts, filename, label, score, x0, y0, x1, y1, gallery
+FROM matches ORDER BY ts DESC"""
+
+LIST_GALLERY_MATCHES_SQL = """
+SELECT ts, filename, label, score, x0, y0, x1, y1, gallery
+FROM matches WHERE gallery IS NOT NULL ORDER BY ts DESC"""
+
+UPDATE_GALLERY_SQL = """
+UPDATE matches SET gallery = ? WHERE ts = ?"""
+
+GET_MATCH_PAGE_SQL = """
+SELECT ts, filename, label, score, x0, y0, x1, y1, gallery, previous_ts, next_ts
+FROM (SELECT *,
+ LAG(ts, 1) OVER (ORDER BY ts DESC) AS next_ts,
+ LEAD(ts, 1) OVER (ORDER BY ts DESC) AS previous_ts
+ FROM matches ORDER BY ts DESC
+) WHERE ts = ?"""
class Archive:
@@ -24,7 +47,30 @@ class Archive:
def add_match(self, *, ts, filename, label, score, box):
with closing(self.con.cursor()) as cur:
- try:
- return cur.execute(ADD_MATCH_SQL, (ts, filename, label, score, *box))
- finally:
- self.con.commit()
+ res = cur.execute(ADD_MATCH_SQL, (ts, filename, label, score, *box))
+ self.con.commit()
+ return res
+
+ def list_matches(self):
+ with closing(self.con.cursor()) as cur:
+ return cur.execute(LIST_MATCHES_SQL).fetchall()
+
+ def list_gallery_matches(self):
+ with closing(self.con.cursor()) as cur:
+ return cur.execute(LIST_GALLERY_MATCHES_SQL).fetchall()
+
+ def get_match_page(self, ts):
+ with closing(self.con.cursor()) as cur:
+ return cur.execute(GET_MATCH_PAGE_SQL, (ts,)).fetchone()
+
+ def set_gallery(self, ts):
+ return self.__update_gallery(ts, 1)
+
+ def unset_gallery(self, ts):
+ return self.__update_gallery(ts, None)
+
+ def __update_gallery(self, ts, value):
+ with closing(self.con.cursor()) as cur:
+ res = cur.execute(UPDATE_GALLERY_SQL, (value, ts))
+ self.con.commit()
+ return res
diff --git a/ppboatwatch/gallery.html b/ppboatwatch/gallery.html
@@ -0,0 +1,101 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Ppbbww | {{ date_time }}</title>
+ <style type="text/css">
+ * {
+ padding: 0;
+ margin: 0;
+ }
+ .container {
+ width: 90%;
+ max-width: 800px;
+ margin: 5em auto;
+ font-family: monospace;
+ font-size: 1.5em;
+ color: #626262;
+ }
+ .column {
+ float: left;
+ width: 33.3%;
+ }
+ .row { margin: 10px 0; }
+ .row:after {
+ content: "";
+ display: table;
+ clear: both;
+ }
+ a {
+ font-weight: bold;
+ text-decoration: none;
+ color: #626262;
+ }
+ .text-left { text-align: left; }
+ .text-right { text-align: right; }
+ .text-center { text-align: center; }
+ .title {
+ font-size: 2em;
+ text-decoration: none;
+ color: #626262;
+ }
+ .button-red, .button-green {
+ border: 0;
+ border-radius: 0.75em;
+ padding: 0.5em 1em;
+ margin: 1em 0;
+ font-family: monospace;
+ font-size: 1em;
+ color: #f2f2f2;
+ }
+ .button-green { background-color: green; }
+ .button-red { background-color: red; }
+ </style>
+ </head>
+ <body>
+ <div class="container">
+ <div class="row">
+ <a href="/" class="title">Ppbbww</a>
+ </div>
+ <img src="/{{ filename }}" width=100% height=auto>
+ <div class="row">
+ <div class="column text-left">
+ {% if prev_ts %}
+ <a href="/gallery/{{ prev_ts }}">Older posts</a>
+ {% endif %}
+
+ </div>
+ <div class="column text-center">
+ {{ date_time }}
+ </div>
+ <div class="column text-right">
+
+ {% if next_ts %}
+ <a href="/gallery/{{ next_ts }}">Newer posts</a>
+ {% endif %}
+ <div></div>
+ </div>
+ <div class="row text-center">
+ {% if gallery %}
+ <button type="button" class="button-red" onClick="unsetGallery({{ ts }})">Remove from Gallery</button>
+ {% else %}
+ <button type="button" class="button-green" onClick="setGallery({{ ts }})">Add to Gallery</button>
+ {% endif %}
+ </div>
+ </div>
+ </div>
+ <script type="text/javascript">
+ function setGallery(ts) {
+ const req = new XMLHttpRequest();
+ req.open("PUT", `/gallery/${ts}`)
+ req.onreadystatechange = function() { location.reload() }
+ req.send()
+ }
+ function unsetGallery(ts) {
+ const req = new XMLHttpRequest();
+ req.open("DELETE", `/gallery/${ts}`)
+ req.onreadystatechange = function() { location.reload() }
+ req.send()
+ }
+ </script>
+ </body>
+</html>
diff --git a/ppboatwatch/gallery.py b/ppboatwatch/gallery.py
@@ -0,0 +1,114 @@
+import aiohttp_jinja2
+import jinja2
+import os
+import shutil
+import sys
+
+from aiohttp import web
+from datetime import datetime
+from itertools import groupby
+
+from .archive import Archive
+
+
+class Curator:
+ def __init__(self, db_file):
+ self.archive = Archive(db_file)
+ self.app = web.Application()
+ self.app.add_routes(
+ [
+ web.get("/", self.redirect_latest),
+ web.get("/gallery/{ts}", self.display_match),
+ web.put("/gallery/{ts}", self.set_gallery),
+ web.delete("/gallery/{ts}", self.unset_gallery),
+ web.static("/data", "./data"),
+ ]
+ )
+ aiohttp_jinja2.setup(self.app, loader=jinja2.FileSystemLoader("./ppboatwatch/"))
+
+ def run(self):
+ web.run_app(self.app)
+
+ async def redirect_latest(self, request):
+ ts, _, _, _, _, _, _, _, _ = self.archive.list_matches()[0]
+ raise web.HTTPFound(f"/gallery/{ts}")
+
+ async def display_match(self, request):
+ ts = int(request.match_info["ts"])
+ (ts, filename, label, score, x0, y0, x1, y1, gallery, previous_ts, next_ts) = (
+ self.archive.get_match_page(ts)
+ )
+ dt = datetime.fromtimestamp(int(ts)).strftime("%Y-%m-%d %I:%M:%S")
+ context = {
+ "date_time": dt,
+ "prev_ts": previous_ts,
+ "ts": ts,
+ "next_ts": next_ts,
+ "filename": filename,
+ "label": label,
+ "score": score,
+ "x0": x0,
+ "y0": y0,
+ "x1": x1,
+ "y1": y1,
+ "gallery": True if gallery else False,
+ }
+ return aiohttp_jinja2.render_template("gallery.html", request, context)
+
+ async def set_gallery(self, request):
+ ts = request.match_info["ts"]
+ try:
+ self.archive.set_gallery(int(ts))
+ return web.Response(status=200)
+ except Exception as ex:
+ logging.error(f"Failed to set_gallery: {ex}")
+ return web.Response(status=500)
+
+ async def unset_gallery(self, request):
+ ts = request.match_info["ts"]
+ try:
+ self.archive.unset_gallery(int(ts))
+ return web.Response(status=200)
+ except Exception as ex:
+ logging.error(f"Failed to set_gallery: {ex}")
+ return web.Response(status=500)
+
+
+# Entry point for gallery curation web UI.
+def curate():
+ curator = Curator("archive.db")
+ curator.run()
+
+
+# Entry point for static gallery generation; produces _layouts and
+# assets/img folders for jekyll gh-pages site, based on the `gallery`
+# column in an Archive.
+def generate():
+ if not os.path.exists("gallery"):
+ os.makedirs("gallery")
+ if not os.path.exists("_posts"):
+ os.makedirs("_posts")
+
+ rows = Archive("archive.db").list_gallery_matches()
+ all_matches = list(groupby(rows, key=lambda cols: (cols[0], cols[1])))
+ for i, ((ts, f), matches) in enumerate(all_matches):
+ # Copy frame file to gallery.
+ shutil.copyfile(f, f"gallery/{ts}.jpg")
+ # Make a new post.
+ ymd = datetime.fromtimestamp(int(ts)).strftime("%Y-%m-%d")
+ with open(f"_posts/{ymd}-{ts}.md", "w") as post_file:
+ dt = datetime.fromtimestamp(int(ts)).strftime("%Y-%m-%d %I:%M:%S")
+ # TODO: Could add lag/lead to LIST_GALLERY_MATCHES_SQL query instead.
+ next_ts = all_matches[i - 1][0][0] if i > 0 else None
+ prev_ts = all_matches[i + 1][0][0] if i < len(all_matches) - 1 else None
+ post_file.writelines(
+ [
+ "---\n",
+ "layout: default\n",
+ f"ts: {ts}\n",
+ f"prev_ts: {prev_ts}\n" if prev_ts else "",
+ f"next_ts: {next_ts}\n" if next_ts else "",
+ f'date_time: "{dt}"\n',
+ "---\n",
+ ]
+ )
diff --git a/pyproject.toml b/pyproject.toml
@@ -52,9 +52,11 @@ dependencies = [
]
[project.scripts]
+curate-gallery = "ppboatwatch.gallery:curate"
find-objects = "ppboatwatch.find_objects:main"
-sample-streams = "ppboatwatch.sample_streams:main"
+gen-gallery = "ppboatwatch.gallery:generate"
ppbw = "ppboatwatch.ppbw:main"
+sample-streams = "ppboatwatch.sample_streams:main"
[tool.setuptools]
packages = ["ppboatwatch"]