paulo@124: import csv paulo@124: import datetime paulo@124: import random paulo@124: import os paulo@133: import time paulo@124: paulo@124: import flask paulo@124: import google.cloud.storage paulo@124: from html3.html3 import HTML paulo@124: paulo@124: app = flask.Flask(__name__) paulo@124: paulo@124: GCS_CLIENT = google.cloud.storage.Client() paulo@124: GCS_BUCKET = GCS_CLIENT.get_bucket(os.environ.get("GCS_BUCKET")) paulo@130: PIN = os.environ.get("PIN") paulo@130: paulo@130: paulo@130: class PinFailError(Exception): paulo@130: def __str__(self): paulo@130: return "PIN FAIL!" paulo@130: paulo@130: class PinSetupError(Exception): paulo@130: def __str__(self): paulo@130: return "PIN SETUP ERROR!" paulo@124: paulo@124: paulo@124: class PicsDialect(csv.Dialect): paulo@124: delimiter = '\t' paulo@124: quoting = csv.QUOTE_NONE paulo@124: lineterminator = '\n' paulo@124: paulo@124: PICSDIALECT = PicsDialect() paulo@124: paulo@124: paulo@133: class BlobCache(object): paulo@133: def __init__(self): paulo@133: self._bloblist_cache = {} paulo@133: self._bloblist_ttl = 60*15 # 15 minutes paulo@133: self._public_url_cache = {} paulo@133: paulo@133: def get_list_blobs(self, prefix): paulo@133: if (prefix not in self._bloblist_cache paulo@133: or time.time() > (self._bloblist_cache[prefix][1] + self._bloblist_ttl)): paulo@133: self._bloblist_cache[prefix] = ([i for i in GCS_CLIENT.list_blobs(GCS_BUCKET, prefix=prefix)], paulo@133: time.time()) paulo@133: return self._bloblist_cache[prefix][0] paulo@133: paulo@133: def cache_public_url(self, blob): paulo@133: if blob.name not in self._public_url_cache: paulo@133: self._public_url_cache[blob.name] = blob.public_url paulo@133: paulo@133: def get_public_url(self, blob_name): paulo@133: return self._public_url_cache[blob_name] paulo@133: paulo@133: BLOBCACHE = BlobCache() paulo@133: paulo@133: paulo@124: def _parse_dt(dts): paulo@124: return datetime.datetime.strptime(dts, "%Y%m%d") paulo@124: paulo@124: paulo@124: def _format_dt(dt): paulo@124: return dt.strftime("%Y-%m-%d") paulo@124: paulo@124: paulo@124: def _numeric_pad_basename(path, maxdigits=20): paulo@124: return os.path.basename(path).zfill(maxdigits) paulo@124: paulo@124: paulo@124: def _get_images(d): paulo@124: exts = (".jpg", ".webm") paulo@124: thumb_dir = f"pics/{d}/thumbs" paulo@124: browse_dir = f"pics/{d}/browse" paulo@124: paulo@133: thumb_fns = [] paulo@133: for i in BLOBCACHE.get_list_blobs(thumb_dir): paulo@133: if i.name.endswith(exts): paulo@133: thumb_fns.append(i.name) paulo@133: BLOBCACHE.cache_public_url(i) paulo@124: thumb_fns = sorted(thumb_fns, key=_numeric_pad_basename) paulo@124: paulo@133: browse_contents = set() paulo@133: for i in BLOBCACHE.get_list_blobs(browse_dir): paulo@133: if i.name.endswith(exts): paulo@133: browse_contents.add(i.name) paulo@133: BLOBCACHE.cache_public_url(i) paulo@124: browse_fns = [] paulo@124: for i in thumb_fns: paulo@124: i_basename = os.path.splitext(os.path.basename(i))[0] paulo@124: try: paulo@124: for j in exts: paulo@124: browse_fn = browse_dir + "/" + i_basename + j paulo@124: if browse_fn in browse_contents: paulo@124: browse_fns.append(browse_fn) paulo@124: raise StopIteration paulo@124: except StopIteration: paulo@124: pass paulo@124: else: paulo@124: raise RuntimeError(f"Cannot find browse image for {i}") paulo@124: paulo@124: return zip(thumb_fns, browse_fns) paulo@124: paulo@124: paulo@124: def _get_standard_html_doc(title): paulo@124: root = HTML("html") paulo@124: paulo@124: header = root.head paulo@124: header.link(rel="stylesheet", type="text/css", href=flask.url_for("static", filename="index.css")) paulo@124: header.title(title) paulo@124: paulo@124: body = root.body paulo@124: body.h1(title) paulo@124: paulo@124: return (root, header, body) paulo@124: paulo@124: paulo@124: def _go_thumbnail_links_to_browse_imgs_html_body(body, d, t, b, a_args={}, img_args={}, lazyload=False): paulo@133: thumb_img_url = BLOBCACHE.get_public_url(t) paulo@124: browse_url = flask.url_for("browse", d=d, img=os.path.basename(b)) paulo@124: paulo@124: a = body.a(href=browse_url, **a_args) paulo@124: if lazyload: paulo@124: img_args = dict(img_args) paulo@124: img_args["data-src"] = thumb_img_url paulo@124: a.img(**img_args) paulo@124: else: paulo@124: a.img(src=thumb_img_url, **img_args) paulo@124: paulo@124: body.text(" ") paulo@124: paulo@124: paulo@125: def _go_thumbnail_links_to_thumbs_html_body(body, d, t, a_args={}, img_args={}): paulo@133: thumb_img_url = BLOBCACHE.get_public_url(t) paulo@125: thumbs_url_args = {"from": os.path.basename(t), "_anchor": "selected"} paulo@125: thumbs_url = flask.url_for("thumbs", d=d, **thumbs_url_args) paulo@125: paulo@125: a = body.a(href=thumbs_url, **a_args) paulo@125: a.img(src=thumb_img_url, **img_args) paulo@125: paulo@125: body.text(" ") paulo@125: paulo@125: paulo@124: @app.route("/") paulo@124: def index(): paulo@124: n = 5 # number of thumbnails to display per dir paulo@124: paulo@124: (root, header, body) = _get_standard_html_doc("Pictures") paulo@124: header.script('', type="text/javascript", src=flask.url_for("static", filename="lazyload.js")) paulo@124: paulo@130: if not PIN: paulo@130: raise PinSetupError paulo@130: elif flask.request.cookies.get("lahat") != PIN: paulo@130: raise PinFailError paulo@130: paulo@124: pics_dirs = [] paulo@124: pics_dirs_index_blob = GCS_BUCKET.get_blob("pics/index.tsv") paulo@124: if pics_dirs_index_blob: paulo@124: pics_dirs_index_strlist = pics_dirs_index_blob.download_as_text().splitlines() paulo@124: pics_dirs_index_reader = csv.reader(pics_dirs_index_strlist, PICSDIALECT) paulo@125: pics_dirs = sorted(pics_dirs_index_reader, key=lambda x: x[0], reverse=True) paulo@124: paulo@124: for (dts, d) in pics_dirs: paulo@124: dt = _parse_dt(dts) paulo@125: body.h2.a(d, href=flask.url_for("thumbs", d=d)) paulo@124: body.h3(_format_dt(dt)) paulo@124: paulo@124: imgs = _get_images(d) paulo@124: imgs_idx = [(i, img) for (i, img) in enumerate(imgs)] paulo@124: paulo@124: sampled_imgs_idx = random.sample(imgs_idx, min(len(imgs_idx), n)) paulo@124: sampled_imgs_idx.sort(key=lambda x: x[0]) paulo@124: paulo@124: p = body.p paulo@124: for (i, (t, b)) in sampled_imgs_idx: paulo@124: _go_thumbnail_links_to_browse_imgs_html_body(p, d, t, b, lazyload=True) paulo@124: paulo@124: return str(root).encode("utf-8") paulo@124: paulo@124: paulo@124: @app.route("//browse/") paulo@124: def browse(d, img): paulo@124: browse_img_blob = GCS_BUCKET.get_blob(f"pics/{d}/browse/{img}") paulo@124: if not browse_img_blob: paulo@124: flask.abort(404) paulo@124: paulo@124: imgs = list(_get_images(d)) paulo@124: paulo@124: # thumbnail preview ribbon paulo@124: w = 7 # must be odd paulo@124: v = int(w/2) paulo@124: imgs_circ = [None] * w paulo@124: x = None paulo@124: n = len(imgs) paulo@124: for (i, (t, b)) in enumerate(imgs): paulo@124: if os.path.basename(b) == img: paulo@124: x = i + 1 paulo@124: imgs_circ[v] = (t, b) paulo@124: for j in range(1, v + 1): paulo@124: if (i + j) < n: imgs_circ[v + j] = imgs[i + j] paulo@124: if (i - j) >= 0: imgs_circ[v - j] = imgs[i - j] paulo@124: break paulo@124: paulo@124: if x is None: paulo@124: raise AssertionError paulo@124: paulo@124: (root, header, body) = _get_standard_html_doc(f"{d} \u2014 {x} of {n}") paulo@124: header.script('', type="text/javascript", src=flask.url_for("static", filename="np_keys.js")) paulo@124: paulo@124: browse_img_url = browse_img_blob.public_url paulo@124: ext = os.path.splitext(img)[1] paulo@124: p = body.p paulo@124: if ext == ".webm": paulo@124: p.video(src=browse_img_url, autoplay="true", loop="true") paulo@124: else: paulo@124: p.img(src=browse_img_url) paulo@124: paulo@124: p = body.p paulo@124: for (i, img_c) in enumerate(imgs_circ): paulo@124: if img_c is not None: paulo@124: (t, b) = img_c paulo@124: a_args = {} paulo@124: img_args = {} paulo@125: if os.path.basename(b) == img: paulo@125: a_args = {"id": "up"} paulo@125: img_args = {"klass": "sel"} paulo@125: b = None paulo@124: elif i == v + 1: paulo@124: a_args = {"id": "next"} paulo@124: elif i == v - 1: paulo@124: a_args = {"id": "prev"} paulo@124: paulo@125: if b is None: paulo@125: _go_thumbnail_links_to_thumbs_html_body(p, d, t, a_args, img_args) paulo@125: else: paulo@125: _go_thumbnail_links_to_browse_imgs_html_body(p, d, t, b, a_args, img_args) paulo@124: paulo@124: return str(root).encode("utf-8") paulo@125: paulo@125: paulo@125: @app.route("/") paulo@125: def thumbs(d): paulo@125: args = flask.request.args paulo@125: paulo@125: from_img = args.get("from") paulo@125: paulo@125: imgs = list(_get_images(d)) paulo@125: (root, header, body) = _get_standard_html_doc(d) paulo@125: paulo@125: p = body.p paulo@125: for (t, b) in imgs: paulo@125: img_args = {} paulo@125: if os.path.basename(b) == from_img: paulo@125: img_args={"klass": "sel2", "id":"selected"} paulo@125: _go_thumbnail_links_to_browse_imgs_html_body(p, d, t, b, img_args=img_args) paulo@125: paulo@125: return str(root).encode("utf-8")