Add optional photo providers, layout studio and scoped integrations
This commit is contained in:
@@ -15,6 +15,7 @@ from pigway_frame_flow.models import (
|
||||
FutureContent,
|
||||
ImageContent,
|
||||
PhotoSourceContent,
|
||||
ProviderContent,
|
||||
TemplateContent,
|
||||
URLContent,
|
||||
)
|
||||
@@ -37,6 +38,13 @@ class DisplayService:
|
||||
assets,
|
||||
renderer,
|
||||
)
|
||||
from typing import Callable
|
||||
|
||||
self.operation_handlers: dict[str, Callable] = {}
|
||||
self.reconciliation_handlers: dict[str, Callable] = {}
|
||||
from pigway_frame_flow.providers.external import ExternalProviders
|
||||
|
||||
self.providers = ExternalProviders(self)
|
||||
self.lock = threading.RLock()
|
||||
self.retention = Retention(self)
|
||||
self.cloud_library = CloudLibrary(self)
|
||||
@@ -56,8 +64,23 @@ class DisplayService:
|
||||
raise FlowError(
|
||||
E.UNSUPPORTED_CAPABILITY, "Content type is unsupported", 422
|
||||
)
|
||||
if isinstance(request.content, ProviderContent):
|
||||
self.providers.config(request.content.provider_id)
|
||||
self.renderer.info(request.content.template)
|
||||
if isinstance(request.content, TemplateContent):
|
||||
self.renderer.info(request.content.template)
|
||||
if request.content.template in ("collage", "poster"):
|
||||
from pigway_frame_flow.extensions.layout import LayoutPlan
|
||||
|
||||
try:
|
||||
plan = LayoutPlan.model_validate(request.content.data.get("layout"))
|
||||
except ValueError:
|
||||
raise FlowError(
|
||||
E.INVALID_REQUEST,
|
||||
"Create a valid layout in Layout Studio first",
|
||||
) from None
|
||||
for region in plan.regions:
|
||||
self.assets.path(region.asset_id)
|
||||
if isinstance(request.content, PhotoSourceContent):
|
||||
self.photo_sources.get(request.content.source_id)
|
||||
if writing and not self.settings.mock and not self.settings.write_enabled:
|
||||
@@ -168,6 +191,18 @@ class DisplayService:
|
||||
return self.db.one("SELECT * FROM device_state WHERE device_id=?", (id,))
|
||||
|
||||
def content_asset(self, content, device_id=None, operation_id=None):
|
||||
if isinstance(content, ProviderContent):
|
||||
values = {**content.data, **self.providers.get(content.provider_id)}
|
||||
return self.content_asset(
|
||||
TemplateContent(
|
||||
type="template",
|
||||
template=content.template,
|
||||
data=values,
|
||||
profile=content.profile,
|
||||
),
|
||||
device_id,
|
||||
operation_id,
|
||||
)
|
||||
if isinstance(content, TemplateContent):
|
||||
values = self.renderer.resolve(content.template, content.data)
|
||||
data = self.renderer.render(content.template, values, content.profile)
|
||||
@@ -219,6 +254,53 @@ class DisplayService:
|
||||
data, name, caption = self.photo_sources.read(
|
||||
content.source_id, item["id"], target_size=size
|
||||
)
|
||||
if (
|
||||
source.minimum_sharpness
|
||||
or source.minimum_brightness
|
||||
or source.maximum_brightness < 255
|
||||
or source.avoid_duplicates
|
||||
):
|
||||
from pigway_frame_flow.extensions.quality import (
|
||||
passes,
|
||||
photo_quality,
|
||||
)
|
||||
|
||||
quality = photo_quality(data)
|
||||
if not passes(quality, source):
|
||||
raise FlowError(
|
||||
E.INVALID_REQUEST,
|
||||
"Photo fails configured quality thresholds",
|
||||
)
|
||||
previous_quality = self.db.one(
|
||||
"SELECT payload FROM app_config WHERE key=?",
|
||||
("recent-photo:" + content.source_id,),
|
||||
)
|
||||
fingerprints = (
|
||||
json.loads(previous_quality["payload"])
|
||||
if previous_quality
|
||||
else []
|
||||
)
|
||||
if source.avoid_duplicates and any(
|
||||
int(quality["fingerprint"], 16)
|
||||
.__xor__(int(old, 16))
|
||||
.bit_count()
|
||||
<= 4
|
||||
for old in fingerprints
|
||||
):
|
||||
raise FlowError(
|
||||
E.CONFLICT,
|
||||
"Photo resembles a recently selected photo",
|
||||
409,
|
||||
)
|
||||
self.db.execute(
|
||||
"INSERT OR REPLACE INTO app_config VALUES(?,?)",
|
||||
(
|
||||
"recent-photo:" + content.source_id,
|
||||
encode(
|
||||
(fingerprints + [quality["fingerprint"]])[-200:]
|
||||
),
|
||||
),
|
||||
)
|
||||
rendered = (
|
||||
render_captioned_photo(
|
||||
data,
|
||||
@@ -376,6 +458,8 @@ class DisplayService:
|
||||
def _execute(self, op, payload):
|
||||
id, device_id, action = op["id"], op["device_id"], op["action"]
|
||||
driver = self.registry.get(device_id)
|
||||
if action in self.operation_handlers:
|
||||
return self.operation_handlers[action](op, payload)
|
||||
if action in ("render", "local-upload"):
|
||||
self.update(id, "RENDERING")
|
||||
from pydantic import TypeAdapter
|
||||
@@ -699,6 +783,8 @@ class DisplayService:
|
||||
def reconcile(self, id, acknowledge=False):
|
||||
with self.lock:
|
||||
op = self.operation(id)
|
||||
if op["action"] in self.reconciliation_handlers:
|
||||
return self.reconciliation_handlers[op["action"]](op, acknowledge)
|
||||
driver = self.registry.get(op["device_id"])
|
||||
device = driver.device(refresh=True)
|
||||
images = driver.images()
|
||||
|
||||
Reference in New Issue
Block a user