contact thumbserver for video thumbs
This commit is contained in:
parent
44e7bc81d6
commit
613124dbf9
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue