From 613124dbf9ab21de4216bd2c82ec5e21113a2b7a Mon Sep 17 00:00:00 2001 From: dave Date: Fri, 20 Aug 2021 18:00:13 -0700 Subject: [PATCH] contact thumbserver for video thumbs --- photoapp/daemon.py | 10 +++++++--- photoapp/thumbserver.py | 21 ++++++++++++++++++--- photoapp/thumbtool.py | 29 +++++++++++++++++++++++------ 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/photoapp/daemon.py b/photoapp/daemon.py index c1f3445..250da7e 100644 --- a/photoapp/daemon.py +++ b/photoapp/daemon.py @@ -601,6 +601,7 @@ def main(): default=int(os.environ.get("PHOTOLIB_PORT", 8080)), type=int) parser.add_argument('-l', '--library', default=os.environ.get("STORAGE_URL"), help="library path") parser.add_argument('-c', '--cache', default=os.environ.get("CACHE_URL"), help="cache url") + parser.add_argument('-t', '--thumb-service', default=os.environ.get("THUMB_SERVICE_URL"), help="thumbnail service url") # https://docs.sqlalchemy.org/en/13/core/engines.html parser.add_argument('-s', '--database', help="sqlalchemy database connection uri", default=os.environ.get("DATABASE_URL")), @@ -612,6 +613,9 @@ def main(): args = parser.parse_args() + logging.basicConfig(level=logging.INFO if args.debug else logging.WARNING, + format="%(asctime)-15s %(levelname)-8s %(filename)s:%(lineno)d %(message)s") + if not args.database: parser.error("--database or DATABASE_URL is required") @@ -621,8 +625,8 @@ def main(): if not args.cache: parser.error("--cache or CACHE_URL is required") - logging.basicConfig(level=logging.INFO if args.debug else logging.WARNING, - format="%(asctime)-15s %(levelname)-8s %(filename)s:%(lineno)d %(message)s") + if not args.thumb_service: + logging.warning("THUMB_SERVICE_URL not set. Video thumbnails will be unavailable") # Get database connection engine = get_db_engine(args.database) @@ -634,7 +638,7 @@ def main(): # Create various internal tools library_storage = uri_to_storage(args.library) library_manager = LibraryManager(library_storage) - thumbnail_tool = ThumbGenerator(library_manager, uri_to_storage(args.cache)) + thumbnail_tool = ThumbGenerator(library_manager, uri_to_storage(args.cache), args.thumb_service) # Setup and mount web ui tpl_dir = os.path.join(APPROOT, "templates") if not args.debug else "templates" diff --git a/photoapp/thumbserver.py b/photoapp/thumbserver.py index 8c2b0ca..1e36a71 100644 --- a/photoapp/thumbserver.py +++ b/photoapp/thumbserver.py @@ -4,9 +4,11 @@ import cherrypy import shutil import tempfile import traceback +import requests from threading import Thread from contextlib import closing from queue import Queue, Empty +from shutil import copyfileobj from subprocess import check_call from photoapp.dbutils import SAEnginePlugin, SATool, db, get_db_engine, create_db_sessionmaker from photoapp.storage import uri_to_storage @@ -46,7 +48,7 @@ def setup_thumb_user(engine): class ThumbWorker(Thread): - def __init__(self, engine, library, cache, thumbtool): + def __init__(self, engine, library, cache): super().__init__() self.daemon = True @@ -120,6 +122,20 @@ class ThumbServiceWeb(object): yield "ok" +class ThumbClient(object): + """ + Client for interacting with the thumbserver api + """ + def __init__(self, server_url): + self.server_url = server_url + self.session = requests.Session() + a = requests.adapters.HTTPAdapter(max_retries=0) + self.session.mount('http://', a) + + def request_thumb(self, photo_uuid, style_name): + self.session.get(self.server_url, params=dict(uuid=photo_uuid, style=style_name)) + + # TODO dedupe me def validate_password(realm, username, password): if db.query(User).filter(User.name == username, User.password == pwhash(password)).first(): @@ -170,8 +186,7 @@ def main(): library_storage = uri_to_storage(args.library) library_manager = LibraryManager(library_storage) cache_storage = uri_to_storage(args.cache) - thumbnail_tool = ThumbGenerator(library_manager, cache_storage) - thumbnail_worker = ThumbWorker(engine, library_storage, cache_storage, thumbnail_tool) + thumbnail_worker = ThumbWorker(engine, library_storage, cache_storage) thumbnail_worker.start() # Setup and mount web ui diff --git a/photoapp/thumbtool.py b/photoapp/thumbtool.py index 15e176e..96c1013 100644 --- a/photoapp/thumbtool.py +++ b/photoapp/thumbtool.py @@ -3,19 +3,22 @@ from collections import defaultdict import tempfile from shutil import copyfileobj from contextlib import closing +from photoapp.types import video_mimes from photoapp.thumb import thumb_path, image_file_style_process +from photoapp.thumbserver import ThumbClient class ThumbGenerator(object): - def __init__(self, library, storage): + def __init__(self, library, storage, thumb_service_url=None): self.library = library self.storage = storage + self.thumb_service = ThumbClient(thumb_service_url) if thumb_service_url else None self._failed_thumbs_cache = defaultdict(dict) def make_thumb(self, photo, style_name): """ Create a thumbnail of the given photo, scaled/cropped to the given named style - :return: local path to thumbnail file or None if creation failed or was blocked + :return: file-like object of the thumbnail image's data or None if no thumbnail is available """ dest = thumb_path(style_name, photo.uuid) @@ -23,15 +26,23 @@ class ThumbGenerator(object): return self.storage.open(dest, "rb") if photo.uuid in self._failed_thumbs_cache[style_name]: - return None + return - # if photo.width is None: # todo better detection of images that PIL can't open - # return None + # video thumbnails are handled by an external service + if photo.format in video_mimes: + if self.thumb_service is None: # videos thumbs capability is disabled, show placeholder + return + + self.make_video_thumb(photo.uuid, style_name) + # thumbserver will eventually generate a thumb (and the exists call above will return it) + # for now, return None to show a placeholder + return + + fthumblocal = tempfile.NamedTemporaryFile() # TODO have the subprocess download the file # TODO thundering herd with tempfile.TemporaryDirectory() as tmpdir: - fthumblocal = tempfile.NamedTemporaryFile(delete=True) fpath = os.path.join(tmpdir, "image") with self.library.storage.open(photo.path, 'rb') as fsrc: with open(fpath, 'wb') as ftmpdest: @@ -45,4 +56,10 @@ class ThumbGenerator(object): copyfileobj(fthumblocal, fdest) fthumblocal.seek(0) + # TODO fthumblocal is leaked if we don't hit this return return fthumblocal + + def make_video_thumb(self, photo_uuid, style_name): + #TODO make something like ThumbServiceError so we can differentiate requests stuff from other errors without + #having to deal with requests outside of thumb_service's module + self.thumb_service.request_thumb(photo_uuid, style_name)