contact thumbserver for video thumbs

This commit is contained in:
dave 2021-08-20 18:00:13 -07:00
parent 44e7bc81d6
commit 613124dbf9
3 changed files with 48 additions and 12 deletions

View File

@ -601,6 +601,7 @@ def main():
default=int(os.environ.get("PHOTOLIB_PORT", 8080)), type=int) 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('-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('-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 # https://docs.sqlalchemy.org/en/13/core/engines.html
parser.add_argument('-s', '--database', help="sqlalchemy database connection uri", parser.add_argument('-s', '--database', help="sqlalchemy database connection uri",
default=os.environ.get("DATABASE_URL")), default=os.environ.get("DATABASE_URL")),
@ -612,6 +613,9 @@ def main():
args = parser.parse_args() 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: if not args.database:
parser.error("--database or DATABASE_URL is required") parser.error("--database or DATABASE_URL is required")
@ -621,8 +625,8 @@ def main():
if not args.cache: if not args.cache:
parser.error("--cache or CACHE_URL is required") parser.error("--cache or CACHE_URL is required")
logging.basicConfig(level=logging.INFO if args.debug else logging.WARNING, if not args.thumb_service:
format="%(asctime)-15s %(levelname)-8s %(filename)s:%(lineno)d %(message)s") logging.warning("THUMB_SERVICE_URL not set. Video thumbnails will be unavailable")
# Get database connection # Get database connection
engine = get_db_engine(args.database) engine = get_db_engine(args.database)
@ -634,7 +638,7 @@ def main():
# Create various internal tools # Create various internal tools
library_storage = uri_to_storage(args.library) library_storage = uri_to_storage(args.library)
library_manager = LibraryManager(library_storage) 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 # Setup and mount web ui
tpl_dir = os.path.join(APPROOT, "templates") if not args.debug else "templates" tpl_dir = os.path.join(APPROOT, "templates") if not args.debug else "templates"

View File

@ -4,9 +4,11 @@ import cherrypy
import shutil import shutil
import tempfile import tempfile
import traceback import traceback
import requests
from threading import Thread from threading import Thread
from contextlib import closing from contextlib import closing
from queue import Queue, Empty from queue import Queue, Empty
from shutil import copyfileobj
from subprocess import check_call from subprocess import check_call
from photoapp.dbutils import SAEnginePlugin, SATool, db, get_db_engine, create_db_sessionmaker from photoapp.dbutils import SAEnginePlugin, SATool, db, get_db_engine, create_db_sessionmaker
from photoapp.storage import uri_to_storage from photoapp.storage import uri_to_storage
@ -46,7 +48,7 @@ def setup_thumb_user(engine):
class ThumbWorker(Thread): class ThumbWorker(Thread):
def __init__(self, engine, library, cache, thumbtool): def __init__(self, engine, library, cache):
super().__init__() super().__init__()
self.daemon = True self.daemon = True
@ -120,6 +122,20 @@ class ThumbServiceWeb(object):
yield "ok" 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 # TODO dedupe me
def validate_password(realm, username, password): def validate_password(realm, username, password):
if db.query(User).filter(User.name == username, User.password == pwhash(password)).first(): 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_storage = uri_to_storage(args.library)
library_manager = LibraryManager(library_storage) library_manager = LibraryManager(library_storage)
cache_storage = uri_to_storage(args.cache) cache_storage = uri_to_storage(args.cache)
thumbnail_tool = ThumbGenerator(library_manager, cache_storage) thumbnail_worker = ThumbWorker(engine, library_storage, cache_storage)
thumbnail_worker = ThumbWorker(engine, library_storage, cache_storage, thumbnail_tool)
thumbnail_worker.start() thumbnail_worker.start()
# Setup and mount web ui # Setup and mount web ui

View File

@ -3,19 +3,22 @@ from collections import defaultdict
import tempfile import tempfile
from shutil import copyfileobj from shutil import copyfileobj
from contextlib import closing from contextlib import closing
from photoapp.types import video_mimes
from photoapp.thumb import thumb_path, image_file_style_process from photoapp.thumb import thumb_path, image_file_style_process
from photoapp.thumbserver import ThumbClient
class ThumbGenerator(object): class ThumbGenerator(object):
def __init__(self, library, storage): def __init__(self, library, storage, thumb_service_url=None):
self.library = library self.library = library
self.storage = storage self.storage = storage
self.thumb_service = ThumbClient(thumb_service_url) if thumb_service_url else None
self._failed_thumbs_cache = defaultdict(dict) self._failed_thumbs_cache = defaultdict(dict)
def make_thumb(self, photo, style_name): def make_thumb(self, photo, style_name):
""" """
Create a thumbnail of the given photo, scaled/cropped to the given named style 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) dest = thumb_path(style_name, photo.uuid)
@ -23,15 +26,23 @@ class ThumbGenerator(object):
return self.storage.open(dest, "rb") return self.storage.open(dest, "rb")
if photo.uuid in self._failed_thumbs_cache[style_name]: 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 # video thumbnails are handled by an external service
# return None 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 have the subprocess download the file
# TODO thundering herd # TODO thundering herd
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
fthumblocal = tempfile.NamedTemporaryFile(delete=True)
fpath = os.path.join(tmpdir, "image") fpath = os.path.join(tmpdir, "image")
with self.library.storage.open(photo.path, 'rb') as fsrc: with self.library.storage.open(photo.path, 'rb') as fsrc:
with open(fpath, 'wb') as ftmpdest: with open(fpath, 'wb') as ftmpdest:
@ -45,4 +56,10 @@ class ThumbGenerator(object):
copyfileobj(fthumblocal, fdest) copyfileobj(fthumblocal, fdest)
fthumblocal.seek(0) fthumblocal.seek(0)
# TODO fthumblocal is leaked if we don't hit this return
return fthumblocal 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)