ppbbww

Pillar Point boats, birds, and waves watcher
git clone git@abtrout.com:ppbbww.git
Log | Files | Refs | README | LICENSE

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:
Mppboatwatch/archive.py | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Appboatwatch/gallery.html | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Appboatwatch/gallery.py | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpyproject.toml | 4+++-
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 %} + &nbsp; + </div> + <div class="column text-center"> + {{ date_time }} + </div> + <div class="column text-right"> + &nbsp; + {% 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"]