summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--flaskapi.py41
-rw-r--r--immich.py20
-rw-r--r--lazycachelist.py5
-rw-r--r--manager.py97
-rw-r--r--pix.py41
-rw-r--r--settings.py39
-rw-r--r--window.py18
8 files changed, 199 insertions, 63 deletions
diff --git a/.gitignore b/.gitignore
index cf1238c..4197899 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
__pycache__
*.jpg
+*.json
diff --git a/flaskapi.py b/flaskapi.py
index a537192..45616ec 100644
--- a/flaskapi.py
+++ b/flaskapi.py
@@ -2,8 +2,14 @@ from flask import Flask, Blueprint, request, send_from_directory, send_file, abo
from flask_socketio import SocketIO, emit
from flask_cors import CORS
+from manager import PixMan
+
+
app = Flask(__name__, static_folder="static/dist", static_url_path="/")
-socketio = SocketIO(app, cors_allowed_origins="*") # TODO remove later
+api = Blueprint("api", __name__)
+socketio = SocketIO(app, cors_allowed_origins="*") # NOTE debug
+CORS(api, origins="*") # NOTE debug
+
@app.route("/")
@app.route("/slideshow")
@@ -12,44 +18,49 @@ socketio = SocketIO(app, cors_allowed_origins="*") # TODO remove later
def home():
return send_from_directory("static/public", "index.html")
-api = Blueprint("api", __name__)
-CORS(api, origins="*") # For debugging TODO remove later
-#@api.route("/seek")
@socketio.on("seek")
def seek(increment):
- pd = app.config["pix_display"]
-
- pd.queue.put(lambda: pd.seek(increment))
- while not pd.queue.empty():
+ if not (display := PixMan().display):
+ return {}
+ display.queue.put(lambda: display.seek(increment))
+ while not display.queue.empty():
pass
- return {
- "imageTime": pd.image_time,
- "imageIndex": pd.current_texture_index,
- }
+ return { "imageIndex": display.current_texture_index }
+
@api.route("/albums")
def get_albums():
- ic = app.config["immich_connector"]
+ if not (ic := PixMan().immich_connector):
+ return {}
keys = [ "albumName", "albumThumbnailAssetId", "id", "startDate", "endDate", "assetCount", "shared", ]
return [{
key: album[key] for key in keys
} for album in ic.load_all_albums() ]
+
@api.route("/asset/<key>/thumbnail", defaults={ "size": "thumbnail" })
@api.route("/asset/<key>", defaults={ "size": "preview" })
def get_asset(key, size):
+ if not (ic := PixMan().immich_connector):
+ return {}
# TODO ensure getting actual album thumb
- ic = app.config["immich_connector"]
image_data, mimetype = ic.load_image(key, size=size)
if image_data is None:
abort(400)
return send_file(image_data, mimetype=mimetype)
+
@api.route("/redirect/<path:path>")
def immich_redirect(path):
- ic = app.config["immich_connector"]
+ if not (ic := PixMan().immich_connector):
+ return {}
return redirect(f"{ic.server_url}/{path}")
+@api.route("/config/update", methods=["POST"])
+def config_update():
+ return { "success": PixMan().update_config(request.json) }
+
+
app.register_blueprint(api, url_prefix="/api")
diff --git a/immich.py b/immich.py
index e73b34e..ad39942 100644
--- a/immich.py
+++ b/immich.py
@@ -3,34 +3,38 @@ from io import BytesIO
from queue import Queue
from texture import ImageTexture
+from manager import PixMan
class ImmichConnector:
- def __init__(self, server_url, api_key):
- self.server_url = server_url.removesuffix("/")
- self.api_key = api_key
+ def __init__(self):
+ config = PixMan().config
+ self.server_url = config.immich_url.removesuffix("/")
+ self.api_key = config.immich_api_key
self.texture_load_queue = Queue()
def _request(self, endpoint):
+ if not self.server_url:
+ return None
return requests.get(f"{self.server_url}/api/{endpoint}", headers={ "x-api-key": self.api_key })
def load_all_albums(self):
response = self._request("albums")
- if response.status_code != 200: return
+ if not response or response.status_code != 200: return
data = response.json()
return data
def load_album_assets(self, key):
response = self._request(f"albums/{key}")
- if response.status_code != 200: return
+ if not response or response.status_code != 200: return
data = response.json()
return data["assets"]
def load_image(self, key, size="preview"):
response = self._request(f"assets/{key}/thumbnail?size={size}")
- if response.status_code != 200: return None, None
+ if not response or response.status_code != 200: return None, None
image_data = BytesIO(response.content)
mimetype = response.headers.get("Content-Type")
@@ -47,3 +51,7 @@ class ImmichConnector:
continue # Texture was never loaded so it doesn't need to be free'd
image_data, _ = self.load_image(image_texture.asset_key, size)
image_texture.initialize(image_data)
+
+ def validate_connection(self):
+ # TODO
+ return True
diff --git a/lazycachelist.py b/lazycachelist.py
index b08b85d..3b2744d 100644
--- a/lazycachelist.py
+++ b/lazycachelist.py
@@ -1,6 +1,7 @@
from dataclasses import dataclass, asdict
from texture import ImageTextureImmichAsset
+from manager import PixMan
@dataclass
class Album:
@@ -34,8 +35,8 @@ class CallbackStateData:
class LazyCachingTextureList():
- def __init__(self, immich_connector, album_ids, max_cache_items=100, change_callback=None):
- self.immich_connector = immich_connector
+ def __init__(self, album_ids, max_cache_items=100, change_callback=None):
+ self.immich_connector = PixMan().immich_connector
assert max_cache_items >= 20, "Minimum cache items is 20" # Double small radius
# Ring buffer
diff --git a/manager.py b/manager.py
new file mode 100644
index 0000000..a7d8ebd
--- /dev/null
+++ b/manager.py
@@ -0,0 +1,97 @@
+import os
+import sys
+import signal
+from threading import Thread
+from OpenGL.GLUT import glutLeaveMainLoop
+
+
+class PixMan:
+ _instance = None
+ _initialized = False
+
+ @staticmethod
+ def handle_sigint(sig, frame):
+ try:
+ # Threads are started as daemons so will automatically shut down
+ glutLeaveMainLoop()
+ sys.exit(0)
+ except:
+ pass
+ finally:
+ print("Exiting...")
+
+ def __new__(cls, *args, **kwargs):
+ if cls._instance is None:
+ cls._instance = super().__new__(cls)
+ else:
+ assert not args and not kwargs, f"Singleton {cls.__name__} cannot be called with arguments more than once"
+ return cls._instance
+
+ def __init__(self):
+ pass
+
+ @classmethod
+ def initialize(cls, configfile, app, socketio, host, port):
+ assert not cls._initialized, "Already initialized"
+ if cls._initialized:
+ return
+ cls._initialized = True
+ self = cls()
+
+ self.app = app
+ self.socketio = socketio
+ self.immich_connector = None
+ self.texture_list = None
+ self.display = None
+ self.t_flask = None
+ self.t_idle_download = None
+
+ signal.signal(signal.SIGINT, PixMan.handle_sigint)
+
+ self.configfile = configfile
+ config = Config.load(self.configfile) if os.path.exists(self.configfile) else Config(file=self.configfile)
+
+ self.init_web(host, port)
+ self.update_config(config)
+
+ def init_web(self, host, port):
+ self.t_flask = Thread(target=self.app.run, kwargs={ "host": host, "port": port })
+ self.t_flask.start()
+
+ def init_window(self):
+ # Initialize immich connector
+ self.immich_connector = ImmichConnector()
+ if not self.immich_connector.validate_connection():
+ self.immich_connector = None
+ return
+
+ # Initialize texture list
+ change_callback = lambda d: self.socketio.emit("seek", d)
+ album_keys = [ "38617851-6b57-44f1-b5f7-82577606afc4" ]
+ self.texture_list = LazyCachingTextureList(album_keys, max_cache_items=self.config.max_cache_assets, change_callback=change_callback)
+
+ # Begin downloading images
+ self.t_idle_download = Thread(target=self.immich_connector.idle, daemon=True)
+ self.t_idle_download.start()
+
+ # Create display
+ self.display = PixDisplay(self.texture_list)
+ self.display.main({}) # TODO glut args
+
+ def update_config(self, config):
+ #old_config = self.config
+ self.config = config
+
+ # Initialize window if immich parameters are valid
+ if config.immich_url and config.immich_api_key and not self.display:
+ self.init_window()
+
+ # If all goes well
+ config.save(self.configfile)
+ return True
+
+
+from lazycachelist import LazyCachingTextureList
+from window import PixDisplay
+from immich import ImmichConnector
+from settings import Config
diff --git a/pix.py b/pix.py
index 45e3f70..236d2f1 100644
--- a/pix.py
+++ b/pix.py
@@ -1,38 +1,15 @@
-import sys
-import signal
-from threading import Thread
-from OpenGL.GLUT import glutLeaveMainLoop
+import argparse
-from lazycachelist import LazyCachingTextureList
-from window import PixDisplay
-from immich import ImmichConnector
from flaskapi import app, socketio
-
-
-def handle_sigint(sig, frame):
- try:
- glutLeaveMainLoop()
- sys.exit(0)
- except:
- pass
- finally:
- print("Exiting on Ctrl+C")
+from settings import Config
+from manager import PixMan
if __name__ == "__main__":
- immich_connector = ImmichConnector("http://192.168.1.13", "m5nqOoBc4uhAba21gZdCP3z8D3JT4GPxDXL2psd52EA")
- album_keys = [ "38617851-6b57-44f1-b5f7-82577606afc4" ]
- lazy_texture_list = LazyCachingTextureList(immich_connector, album_keys, 30, lambda d: socketio.emit("seek", d))
- pd = PixDisplay(lazy_texture_list)
-
- t1 = Thread(target=immich_connector.idle, daemon=True)
- t1.start()
-
- app.config["pix_display"] = pd
- app.config["immich_connector"] = immich_connector
- app.config["textures"] = lazy_texture_list
- flask_thread = Thread(target=app.run, daemon=True, kwargs={ "port": 5000 })
- flask_thread.start()
+ p = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+ p.add_argument("--config", type=str, help="set config file path", default="config.json")
+ p.add_argument("--host", type=str, help="set web interface host", default="0.0.0.0")
+ p.add_argument("--port", type=int, help="set web interface port", default=80)
+ args = p.parse_args()
- signal.signal(signal.SIGINT, handle_sigint)
- pd.main(sys.argv)
+ PixMan.initialize(args.config, app, socketio, args.host, args.port)
diff --git a/settings.py b/settings.py
new file mode 100644
index 0000000..43cb024
--- /dev/null
+++ b/settings.py
@@ -0,0 +1,39 @@
+from dataclasses import dataclass, asdict, field
+import json
+
+
+@dataclass
+class AlbumList:
+ name: str
+ album_keys: list[str]
+
+
+@dataclass
+class Config:
+ # Immich server
+ immich_url: str = ""
+ immich_api_key: str = ""
+ # Display
+ image_duration: float = 10.0
+ transition_duration: float = 0.5
+ max_framerate: float = 30.0
+ auto_transition: bool = True
+ display_size: str = "preview" # 'fullsize', 'preview', 'thumbnail'
+ # Cache
+ max_cache_assets: int = 100
+ # Albums data
+ album_lists: list[AlbumList] = field(default_factory=list)
+ album_list_selected: int = None
+
+ def __post_init__(self):
+ self.album_lists = [ AlbumList(*a) for a in self.album_lists ]
+
+ @classmethod
+ def load(cls, filepath):
+ with open(filepath, "r") as fp:
+ return cls(**json.load(fp))
+
+ def save(self, filepath):
+ data = asdict(self)
+ with open(filepath, "w") as fp:
+ json.dump(data, fp, indent=2)
diff --git a/window.py b/window.py
index 1523f85..f422381 100644
--- a/window.py
+++ b/window.py
@@ -5,7 +5,7 @@ from time import time
from queue import Queue
from renderer import ImageRenderer, TransitionMix
-
+from manager import PixMan
class PixDisplay:
def __init__(self, textures):
@@ -18,13 +18,14 @@ class PixDisplay:
self.renderer = None
self.win_w = 0
self.win_h = 0
- self.max_framerate = 30
- self.image_duration = 2.0
- self.transition_duration = 0.5
- self.auto_transition = True
- self.transition_reverse = False
+ config = PixMan().config
+ self.max_framerate = config.max_framerate
+ self.image_duration = config.image_duration
+ self.transition_duration = config.transition_duration
+ self.auto_transition = config.auto_transition
+ self.transition_reverse = False
self.text_prev = None
self.tex = None
@@ -33,12 +34,13 @@ class PixDisplay:
@property
def max_framerate(self):
- return self._max_fps
+ #return int(1000/int(1000/self.frame_time))
+ return self.frame_time
@max_framerate.setter
def max_framerate(self, max_fps):
self._max_fps = max_fps # This is just for the getter since otherwise e.g. int(1000/int(1000/60)) would round to 62
- self.frame_time = int(1000 / 60) # In ms
+ self.frame_time = int(1000 / max_fps) # In ms
def increment_texture_index(self, increment):
self.transition_reverse = increment < 0