view pics3/pics_flask_app.py @ 133:41a6e2d99f68

pics3: add blob cache
author paulo
date Thu, 03 Feb 2022 06:39:44 -0800
parents 06f97e38e1b2
children
line source
1 import csv
2 import datetime
3 import random
4 import os
5 import time
7 import flask
8 import google.cloud.storage
9 from html3.html3 import HTML
11 app = flask.Flask(__name__)
13 GCS_CLIENT = google.cloud.storage.Client()
14 GCS_BUCKET = GCS_CLIENT.get_bucket(os.environ.get("GCS_BUCKET"))
15 PIN = os.environ.get("PIN")
18 class PinFailError(Exception):
19 def __str__(self):
20 return "PIN FAIL!"
22 class PinSetupError(Exception):
23 def __str__(self):
24 return "PIN SETUP ERROR!"
27 class PicsDialect(csv.Dialect):
28 delimiter = '\t'
29 quoting = csv.QUOTE_NONE
30 lineterminator = '\n'
32 PICSDIALECT = PicsDialect()
35 class BlobCache(object):
36 def __init__(self):
37 self._bloblist_cache = {}
38 self._bloblist_ttl = 60*15 # 15 minutes
39 self._public_url_cache = {}
41 def get_list_blobs(self, prefix):
42 if (prefix not in self._bloblist_cache
43 or time.time() > (self._bloblist_cache[prefix][1] + self._bloblist_ttl)):
44 self._bloblist_cache[prefix] = ([i for i in GCS_CLIENT.list_blobs(GCS_BUCKET, prefix=prefix)],
45 time.time())
46 return self._bloblist_cache[prefix][0]
48 def cache_public_url(self, blob):
49 if blob.name not in self._public_url_cache:
50 self._public_url_cache[blob.name] = blob.public_url
52 def get_public_url(self, blob_name):
53 return self._public_url_cache[blob_name]
55 BLOBCACHE = BlobCache()
58 def _parse_dt(dts):
59 return datetime.datetime.strptime(dts, "%Y%m%d")
62 def _format_dt(dt):
63 return dt.strftime("%Y-%m-%d")
66 def _numeric_pad_basename(path, maxdigits=20):
67 return os.path.basename(path).zfill(maxdigits)
70 def _get_images(d):
71 exts = (".jpg", ".webm")
72 thumb_dir = f"pics/{d}/thumbs"
73 browse_dir = f"pics/{d}/browse"
75 thumb_fns = []
76 for i in BLOBCACHE.get_list_blobs(thumb_dir):
77 if i.name.endswith(exts):
78 thumb_fns.append(i.name)
79 BLOBCACHE.cache_public_url(i)
80 thumb_fns = sorted(thumb_fns, key=_numeric_pad_basename)
82 browse_contents = set()
83 for i in BLOBCACHE.get_list_blobs(browse_dir):
84 if i.name.endswith(exts):
85 browse_contents.add(i.name)
86 BLOBCACHE.cache_public_url(i)
87 browse_fns = []
88 for i in thumb_fns:
89 i_basename = os.path.splitext(os.path.basename(i))[0]
90 try:
91 for j in exts:
92 browse_fn = browse_dir + "/" + i_basename + j
93 if browse_fn in browse_contents:
94 browse_fns.append(browse_fn)
95 raise StopIteration
96 except StopIteration:
97 pass
98 else:
99 raise RuntimeError(f"Cannot find browse image for {i}")
101 return zip(thumb_fns, browse_fns)
104 def _get_standard_html_doc(title):
105 root = HTML("html")
107 header = root.head
108 header.link(rel="stylesheet", type="text/css", href=flask.url_for("static", filename="index.css"))
109 header.title(title)
111 body = root.body
112 body.h1(title)
114 return (root, header, body)
117 def _go_thumbnail_links_to_browse_imgs_html_body(body, d, t, b, a_args={}, img_args={}, lazyload=False):
118 thumb_img_url = BLOBCACHE.get_public_url(t)
119 browse_url = flask.url_for("browse", d=d, img=os.path.basename(b))
121 a = body.a(href=browse_url, **a_args)
122 if lazyload:
123 img_args = dict(img_args)
124 img_args["data-src"] = thumb_img_url
125 a.img(**img_args)
126 else:
127 a.img(src=thumb_img_url, **img_args)
129 body.text(" ")
132 def _go_thumbnail_links_to_thumbs_html_body(body, d, t, a_args={}, img_args={}):
133 thumb_img_url = BLOBCACHE.get_public_url(t)
134 thumbs_url_args = {"from": os.path.basename(t), "_anchor": "selected"}
135 thumbs_url = flask.url_for("thumbs", d=d, **thumbs_url_args)
137 a = body.a(href=thumbs_url, **a_args)
138 a.img(src=thumb_img_url, **img_args)
140 body.text(" ")
143 @app.route("/")
144 def index():
145 n = 5 # number of thumbnails to display per dir
147 (root, header, body) = _get_standard_html_doc("Pictures")
148 header.script('', type="text/javascript", src=flask.url_for("static", filename="lazyload.js"))
150 if not PIN:
151 raise PinSetupError
152 elif flask.request.cookies.get("lahat") != PIN:
153 raise PinFailError
155 pics_dirs = []
156 pics_dirs_index_blob = GCS_BUCKET.get_blob("pics/index.tsv")
157 if pics_dirs_index_blob:
158 pics_dirs_index_strlist = pics_dirs_index_blob.download_as_text().splitlines()
159 pics_dirs_index_reader = csv.reader(pics_dirs_index_strlist, PICSDIALECT)
160 pics_dirs = sorted(pics_dirs_index_reader, key=lambda x: x[0], reverse=True)
162 for (dts, d) in pics_dirs:
163 dt = _parse_dt(dts)
164 body.h2.a(d, href=flask.url_for("thumbs", d=d))
165 body.h3(_format_dt(dt))
167 imgs = _get_images(d)
168 imgs_idx = [(i, img) for (i, img) in enumerate(imgs)]
170 sampled_imgs_idx = random.sample(imgs_idx, min(len(imgs_idx), n))
171 sampled_imgs_idx.sort(key=lambda x: x[0])
173 p = body.p
174 for (i, (t, b)) in sampled_imgs_idx:
175 _go_thumbnail_links_to_browse_imgs_html_body(p, d, t, b, lazyload=True)
177 return str(root).encode("utf-8")
180 @app.route("/<d>/browse/<img>")
181 def browse(d, img):
182 browse_img_blob = GCS_BUCKET.get_blob(f"pics/{d}/browse/{img}")
183 if not browse_img_blob:
184 flask.abort(404)
186 imgs = list(_get_images(d))
188 # thumbnail preview ribbon
189 w = 7 # must be odd
190 v = int(w/2)
191 imgs_circ = [None] * w
192 x = None
193 n = len(imgs)
194 for (i, (t, b)) in enumerate(imgs):
195 if os.path.basename(b) == img:
196 x = i + 1
197 imgs_circ[v] = (t, b)
198 for j in range(1, v + 1):
199 if (i + j) < n: imgs_circ[v + j] = imgs[i + j]
200 if (i - j) >= 0: imgs_circ[v - j] = imgs[i - j]
201 break
203 if x is None:
204 raise AssertionError
206 (root, header, body) = _get_standard_html_doc(f"{d} \u2014 {x} of {n}")
207 header.script('', type="text/javascript", src=flask.url_for("static", filename="np_keys.js"))
209 browse_img_url = browse_img_blob.public_url
210 ext = os.path.splitext(img)[1]
211 p = body.p
212 if ext == ".webm":
213 p.video(src=browse_img_url, autoplay="true", loop="true")
214 else:
215 p.img(src=browse_img_url)
217 p = body.p
218 for (i, img_c) in enumerate(imgs_circ):
219 if img_c is not None:
220 (t, b) = img_c
221 a_args = {}
222 img_args = {}
223 if os.path.basename(b) == img:
224 a_args = {"id": "up"}
225 img_args = {"klass": "sel"}
226 b = None
227 elif i == v + 1:
228 a_args = {"id": "next"}
229 elif i == v - 1:
230 a_args = {"id": "prev"}
232 if b is None:
233 _go_thumbnail_links_to_thumbs_html_body(p, d, t, a_args, img_args)
234 else:
235 _go_thumbnail_links_to_browse_imgs_html_body(p, d, t, b, a_args, img_args)
237 return str(root).encode("utf-8")
240 @app.route("/<d>")
241 def thumbs(d):
242 args = flask.request.args
244 from_img = args.get("from")
246 imgs = list(_get_images(d))
247 (root, header, body) = _get_standard_html_doc(d)
249 p = body.p
250 for (t, b) in imgs:
251 img_args = {}
252 if os.path.basename(b) == from_img:
253 img_args={"klass": "sel2", "id":"selected"}
254 _go_thumbnail_links_to_browse_imgs_html_body(p, d, t, b, img_args=img_args)
256 return str(root).encode("utf-8")