Filter NAS photos by EXIF-aware orientation with persistent dimension cache
This commit is contained in:
@@ -18,6 +18,7 @@ from pigway_frame_flow.errors import ErrorCode as E
|
||||
from pigway_frame_flow.errors import FlowError
|
||||
from pigway_frame_flow.models import StrictModel
|
||||
from pigway_frame_flow.providers.photo_names import path_caption
|
||||
from pigway_frame_flow.providers.photo_orientation import dimensions, orientation
|
||||
from pigway_frame_flow.providers.synology_photos import SynologyPhotos
|
||||
from pigway_frame_flow.settings import secret
|
||||
from pigway_frame_flow.storage.database import encode
|
||||
@@ -33,6 +34,7 @@ class PhotoSourceInput(StrictModel):
|
||||
synology_previews: bool = False
|
||||
organized_only: bool = False
|
||||
folder_caption: bool = False
|
||||
orientation: Literal["auto", "landscape", "portrait"] | None = None
|
||||
people_filter: bool = False
|
||||
person_ids: list[int] = Field(default_factory=list, max_length=50)
|
||||
person_match: Literal["any", "all"] = "any"
|
||||
@@ -50,6 +52,9 @@ class PhotoItem(StrictModel):
|
||||
name: str
|
||||
caption: dict[str, str] | None = None
|
||||
provider_preview: bool = False
|
||||
orientation: Literal["landscape", "portrait", "square"] | None = None
|
||||
width: int | None = None
|
||||
height: int | None = None
|
||||
|
||||
|
||||
class PhotoCatalog(StrictModel):
|
||||
@@ -96,6 +101,11 @@ class PhotoSources:
|
||||
return PhotoSourceInput.model_validate_json(row["payload"])
|
||||
|
||||
def save(self, source_id, source):
|
||||
if source.orientation and source.kind != "local":
|
||||
raise FlowError(
|
||||
E.UNSUPPORTED_CAPABILITY,
|
||||
"Orientation filtering requires a local photo source",
|
||||
)
|
||||
if source.people_filter and source.kind != "local":
|
||||
raise FlowError(
|
||||
E.INVALID_REQUEST,
|
||||
@@ -250,6 +260,13 @@ class PhotoSources:
|
||||
else None
|
||||
)
|
||||
items = []
|
||||
sizes = (
|
||||
{r["path"]: r for r in self.db.all("SELECT * FROM photo_dimensions")}
|
||||
if source.orientation
|
||||
else {}
|
||||
)
|
||||
size_updates = []
|
||||
wanted = "portrait" if source.orientation == "auto" else source.orientation
|
||||
if source.kind == "local":
|
||||
directory = self.local_directory(source.location)
|
||||
root = self.settings.photo_root.resolve()
|
||||
@@ -292,8 +309,45 @@ class PhotoSources:
|
||||
relative = path.relative_to(root).as_posix()
|
||||
if allowed is not None and relative not in allowed:
|
||||
continue
|
||||
info = {}
|
||||
if wanted:
|
||||
key = str(path)
|
||||
stamp = path.stat()
|
||||
cached_size = sizes.get(key)
|
||||
if (
|
||||
cached_size
|
||||
and cached_size["mtime"] == stamp.st_mtime_ns
|
||||
and cached_size["bytes"] == stamp.st_size
|
||||
):
|
||||
width, height = (
|
||||
cached_size["width"],
|
||||
cached_size["height"],
|
||||
)
|
||||
else:
|
||||
size = dimensions(root, path)
|
||||
if size is None:
|
||||
continue
|
||||
width, height = size
|
||||
size_updates.append(
|
||||
(
|
||||
key,
|
||||
stamp.st_mtime_ns,
|
||||
stamp.st_size,
|
||||
width,
|
||||
height,
|
||||
)
|
||||
)
|
||||
direction = orientation(width, height)
|
||||
if direction != wanted:
|
||||
continue
|
||||
info = {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"orientation": direction,
|
||||
}
|
||||
items.append(
|
||||
{
|
||||
**info,
|
||||
"id": relative,
|
||||
"name": name,
|
||||
"provider_preview": preview,
|
||||
@@ -358,6 +412,12 @@ class PhotoSources:
|
||||
break
|
||||
except ElementTree.ParseError:
|
||||
raise FlowError(E.INVALID_REQUEST, "Invalid WebDAV catalog") from None
|
||||
if size_updates:
|
||||
with self.db.transaction() as connection:
|
||||
connection.executemany(
|
||||
"INSERT OR REPLACE INTO photo_dimensions(path,mtime,bytes,width,height) VALUES(?,?,?,?,?)",
|
||||
size_updates,
|
||||
)
|
||||
self.cache[source_id] = (time.monotonic(), items)
|
||||
return items
|
||||
|
||||
|
||||
Reference in New Issue
Block a user