import os import logging import cherrypy import shutil import tempfile import traceback from threading import Thread from contextlib import closing from queue import Queue, Empty 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 from photoapp.api import PhotosApi, LibraryManager from photoapp.types import User, Photo from photoapp.utils import require_auth from photoapp.common import pwhash from photoapp.dbsession import DatabaseSession from photoapp.thumb import ThumbGenerator def get_video_thumb(srcpath, outpath): #TODO # download the video file # use `ffmpeg -i filename.flv` to inspect the video, parse output for duration # take a thumb halfway through the video cmd = [ "ffmpeg", "-i", srcpath, "-vframes", "1", # Output one frame "-an", # Disable audio # "-s", "400x222" # Output size "-ss", "1", # grab the frame from 1 second into the video outpath ] check_call(cmd) def setup_thumb_user(engine): #TODO create the internal User used to talk to this service # if user doesnt exist # create # log the password pass class ThumbWorker(Thread): def __init__(self, engine, library, cache, thumbtool): super().__init__() self.daemon = True self.queue = Queue() self.engine = engine self.library = library self.cache = cache self.thumbtool = thumbtool def run(self): while True: try: image_uuid, style_name = self.queue.get(block=True, timeout=5.0) except Empty: continue try: with ( closing(create_db_sessionmaker(self.engine)()) as s, tempfile.TemporaryDirectory() as d, ): self.do_thumb(image_uuid, style_name, s, d) except: traceback.print_exc() #TODO something like _failed_thumbs_cache def do_thumb(self, image_uuid, style_name, session, tmpdir): """ Generate a thumbnail for the given image identified by uuid """ # find the image image = session.query(Photo).filter(Photo.uuid == image_uuid).first() if not image: logging.info("attempted invalid uuid: %s", image_uuid) return # download the image local_src_path = os.path.join(tmpdir, image.fname) # TODO fname isn't sanitized? thumb_path = os.path.join(tmpdir, "thumb.jpg") with ( self.library.open(image.path, "rb") as src, open(local_src_path, "wb") as dest, ): shutil.copyfileobj(src, dest) # generate a still from the image get_video_thumb(local_src_path, thumb_path) # copy thumbnail to cache storage # TODO write thumb.jpg to thumb storage os.system(f"cp {thumb_path} /Users/dave/Desktop/") class ThumbServiceWeb(object): def __init__(self, queue_thumbnail): self.queue_thumbnail = queue_thumbnail @cherrypy.expose def index(self): yield "photoapp thumbnail service OK" @cherrypy.expose # @require_auth def thumb(self, uuid, style): """ Generate a thumbnail for the file identified. Calling this endpoint adds the image to the queue. Duplicate requests are OK and are ignored later """ self.queue_thumbnail((uuid, style, )) yield "ok" # TODO dedupe me def validate_password(realm, username, password): if db.query(User).filter(User.name == username, User.password == pwhash(password)).first(): return True return False def main(): # this is a slimmed down version of daemon.py TODO dedupe me import argparse import signal parser = argparse.ArgumentParser(description="Photod photo server") parser.add_argument('-p', '--port', help="tcp port to listen on", default=int(os.environ.get("THUMB_SERVICE_PORT", 8081)), 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") # 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")), parser.add_argument('--debug', action="store_true", help="enable development options") args = parser.parse_args() if not args.database: parser.error("--database or DATABASE_URL is required") if not args.library: parser.error("--library or STORAGE_URL is required") 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") # Get database connection engine = get_db_engine(args.database) setup_thumb_user(engine) # Setup database in web framework cherrypy.tools.db = SATool() SAEnginePlugin(cherrypy.engine, engine).subscribe() # Create various internal tools 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.start() # Setup and mount web ui web = ThumbServiceWeb(thumbnail_worker.queue.put) cherrypy.tree.mount(web, '/', {'/': {'tools.trailing_slash.on': False, 'tools.db.on': True, }}) # Setup and mount API api = PhotosApi(library_manager) cherrypy.tree.mount(api, '/api', {'/': {'tools.sessions.on': False, 'tools.trailing_slash.on': False, 'tools.auth_basic.on': True, 'tools.auth_basic.realm': 'photolib', 'tools.auth_basic.checkpassword': validate_password, 'tools.db.on': True}}) # General config options cherrypy.config.update({ 'tools.sessions.storage_class': DatabaseSession, 'tools.sessions.on': True, 'tools.sessions.locking': 'explicit', 'tools.sessions.timeout': 525600, 'request.show_tracebacks': True, 'server.socket_port': args.port, 'server.thread_pool': 5, 'server.socket_host': '0.0.0.0', 'server.show_tracebacks': True, 'log.screen': False, 'engine.autoreload.on': args.debug, }) # Setup signal handling and run it. def signal_handler(signum, stack): logging.critical('Got sig {}, exiting...'.format(signum)) cherrypy.engine.exit() signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) try: cherrypy.engine.start() cherrypy.engine.block() finally: logging.info("API has shut down") cherrypy.engine.exit() if __name__ == '__main__': main()